From 51dae418b53ad4c6cca3063df929adddbcd57877 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:41:43 +0000 Subject: [PATCH 01/30] chore(deps): update dependency copyfiles to v2.211.0 --- .ci/azure-pipelines-abi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml index cf74a4201b..cb93c226aa 100644 --- a/.ci/azure-pipelines-abi.yml +++ b/.ci/azure-pipelines-abi.yml @@ -50,7 +50,7 @@ jobs: path: "$(System.ArtifactsDirectory)/new-artifacts" runVersion: "latest" - - task: CopyFiles@2 + - task: CopyFiles@2.211.0 displayName: 'Copy New Assembly Build Artifact' inputs: sourceFolder: $(System.ArtifactsDirectory)/new-artifacts @@ -72,7 +72,7 @@ jobs: runVersion: "latestFromBranch" runBranch: "refs/heads/$(System.PullRequest.TargetBranch)" - - task: CopyFiles@2 + - task: CopyFiles@2.211.0 displayName: 'Copy Reference Assembly Build Artifact' enabled: false inputs: From f39c0c9ab8cbad560e08ee34730397007c95e8a8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 01:04:21 +0000 Subject: [PATCH 02/30] chore(deps): update dependency copyfilesoverssh to v0.212.0 --- .ci/azure-pipelines-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 926d1d3224..f8f2316677 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -69,7 +69,7 @@ jobs: runOptions: 'inline' inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)' - - task: CopyFilesOverSSH@0 + - task: CopyFilesOverSSH@0.212.0 displayName: 'Upload artifacts to repository server' inputs: sshEndpoint: repository @@ -105,7 +105,7 @@ jobs: runOptions: 'inline' inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)' - - task: CopyFilesOverSSH@0 + - task: CopyFilesOverSSH@0.212.0 displayName: 'Upload artifacts to repository server' inputs: sshEndpoint: repository From 10181d542182e199ce592395d9bdf6d6d6aa4273 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 03:47:45 +0000 Subject: [PATCH 03/30] chore(deps): update dependency docker to v2.211.0 --- .ci/azure-pipelines-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 926d1d3224..7f221a3f9f 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -137,7 +137,7 @@ jobs: displayName: Set release version (stable) condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - - task: Docker@2 + - task: Docker@2.211.0 displayName: 'Push Unstable Image' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: @@ -150,7 +150,7 @@ jobs: unstable-$(Build.BuildNumber)-$(BuildConfiguration) unstable-$(BuildConfiguration) - - task: Docker@2 + - task: Docker@2.211.0 displayName: 'Push Stable Image' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: From 90c3815348c998daddd0329e49b4477e669a90cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 03:47:53 +0000 Subject: [PATCH 04/30] chore(deps): update dependency dotnetcorecli to v2.210.0 --- .ci/azure-pipelines-abi.yml | 4 ++-- .ci/azure-pipelines-main.yml | 2 +- .ci/azure-pipelines-package.yml | 4 ++-- .ci/azure-pipelines-test.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml index cf74a4201b..d400202527 100644 --- a/.ci/azure-pipelines-abi.yml +++ b/.ci/azure-pipelines-abi.yml @@ -35,7 +35,7 @@ jobs: packageType: sdk version: ${{ parameters.DotNetSdkVersion }} - - task: DotNetCoreCLI@2 + - task: DotNetCoreCLI@2.210.0 displayName: 'Install ABI CompatibilityChecker Tool' inputs: command: custom @@ -83,7 +83,7 @@ jobs: overWrite: true flattenFolders: true - - task: DotNetCoreCLI@2 + - task: DotNetCoreCLI@2.210.0 displayName: 'Execute ABI Compatibility Check Tool' enabled: false inputs: diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index b7112ba245..875c7a23e6 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -55,7 +55,7 @@ jobs: packageType: sdk version: ${{ parameters.DotNetSdkVersion }} - - task: DotNetCoreCLI@2 + - task: DotNetCoreCLI@2.210.0 displayName: 'Publish Server' inputs: command: publish diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 926d1d3224..43a4ca0d26 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -210,7 +210,7 @@ jobs: packageType: 'sdk' version: '6.0.x' - - task: DotNetCoreCLI@2 + - task: DotNetCoreCLI@2.210.0 displayName: 'Build Stable Nuget packages' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: @@ -225,7 +225,7 @@ jobs: custom: 'pack' arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion) - - task: DotNetCoreCLI@2 + - task: DotNetCoreCLI@2.210.0 displayName: 'Build Unstable Nuget packages' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index cc94dc2c5a..066df89490 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -51,7 +51,7 @@ jobs: organization: 'jellyfin' projectKey: 'jellyfin_jellyfin' - - task: DotNetCoreCLI@2 + - task: DotNetCoreCLI@2.210.0 displayName: 'Run CLI Tests' inputs: command: "test" From 94635917ca110ebb8ca2307655f55c49309eab32 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 05:53:18 +0000 Subject: [PATCH 05/30] chore(deps): update dependency downloadpipelineartifact to v2.198.0 --- .ci/azure-pipelines-abi.yml | 4 ++-- .ci/azure-pipelines-main.yml | 4 ++-- .ci/azure-pipelines-package.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml index cf74a4201b..28b9fae0ca 100644 --- a/.ci/azure-pipelines-abi.yml +++ b/.ci/azure-pipelines-abi.yml @@ -42,7 +42,7 @@ jobs: custom: tool arguments: 'update compatibilitychecker -g' - - task: DownloadPipelineArtifact@2 + - task: DownloadPipelineArtifact@2.198.0 displayName: 'Download New Assembly Build Artifact' inputs: source: 'current' @@ -60,7 +60,7 @@ jobs: overWrite: true flattenFolders: true - - task: DownloadPipelineArtifact@2 + - task: DownloadPipelineArtifact@2.198.0 displayName: 'Download Reference Assembly Build Artifact' enabled: false inputs: diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index b7112ba245..bca217af52 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -20,7 +20,7 @@ jobs: submodules: true persistCredentials: true - - task: DownloadPipelineArtifact@2 + - task: DownloadPipelineArtifact@2.198.0 displayName: 'Download Web Branch' condition: in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion') inputs: @@ -31,7 +31,7 @@ jobs: pipeline: 'Jellyfin Web' runBranch: variables['Build.SourceBranch'] - - task: DownloadPipelineArtifact@2 + - task: DownloadPipelineArtifact@2.198.0 displayName: 'Download Web Target' condition: eq(variables['Build.Reason'], 'PullRequest') inputs: diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 926d1d3224..f28863c806 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -90,7 +90,7 @@ jobs: displayName: Set release version (stable) condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - - task: DownloadPipelineArtifact@2 + - task: DownloadPipelineArtifact@2.198.0 displayName: 'Download OpenAPI Spec' inputs: source: 'current' From 4bb470ea18e1b91513cdc4495b2207f14e4bf608 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 08:22:17 +0000 Subject: [PATCH 06/30] chore(deps): update dependency extractfiles to v1.211.0 --- .ci/azure-pipelines-main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index b7112ba245..8ae5a27e2a 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -42,7 +42,7 @@ jobs: pipeline: 'Jellyfin Web' runBranch: variables['System.PullRequest.TargetBranch'] - - task: ExtractFiles@1 + - task: ExtractFiles@1.211.0 displayName: 'Extract Web Client' inputs: archiveFilePatterns: '$(Agent.TempDirectory)/*.zip' From 62d8369f923e23c6d9be668674bcc523e6b1891b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 14:25:10 +0200 Subject: [PATCH 07/30] chore(deps): update dependency mono.nat to v3.0.4 (#8580) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index b709d1de49..e0f129c3d9 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -30,7 +30,7 @@ - + From 30aed0c092984e831f7c396e3c70f4e1ce98c4b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 21:49:02 +0200 Subject: [PATCH 08/30] chore(deps): update alex-page/github-project-automation-plus action to v0.8.2 (#8576) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/automation.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml index 20294843d5..7749433cff 100644 --- a/.github/workflows/automation.yml +++ b/.github/workflows/automation.yml @@ -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: From fc6ef797d90ab42b1d1d93c8642568511bfc7058 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:50:54 +0000 Subject: [PATCH 09/30] chore(deps): update dependency nugetauthenticate to v0.203.0 --- .ci/azure-pipelines-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 926d1d3224..bb72b41e71 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -256,7 +256,7 @@ jobs: publishFeedCredentials: 'NugetOrg' allowPackageConflicts: true # This ignores an error if the version already exists - - task: NuGetAuthenticate@0 + - task: NuGetAuthenticate@0.203.0 displayName: 'Authenticate to unstable Nuget feed' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') From 8af07151cee09f05de77f60f28dcff88a338591c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 22:17:21 +0200 Subject: [PATCH 10/30] chore(deps): pin dependencies (#8572) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/automation.yml | 2 +- .github/workflows/codeql-analysis.yml | 10 +++++----- .github/workflows/commands.yml | 16 ++++++++-------- .github/workflows/openapi.yml | 22 +++++++++++----------- .github/workflows/repo-stale.yaml | 2 +- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml index 7749433cff..01cd41a085 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' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1dbd7fa367..b551bb5a6e 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 23873706d2..d438e7801d 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@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # 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@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # 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@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # 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@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # 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@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # 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 ceb4e8cdff..c4300b39ab 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@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # tag=v2 if: ${{ steps.read-diff.outputs.body != '' }} with: issue-number: ${{ github.event.pull_request.number }} @@ -112,7 +112,7 @@ jobs: - name: Edit difference comment (unchanged) - uses: peter-evans/create-or-update-comment@v2 + uses: peter-evans/create-or-update-comment@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # 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 2578f82cfe..f7a77f02b1 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 From ac0dbd0b40b51753cb0a431f2fbc1c4e5a843aaf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 22:48:29 +0200 Subject: [PATCH 11/30] chore(deps): update dependency moq to v4.18.2 (#8583) --- .../Emby.Server.Implementations.Fuzz.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7cd98c29ad..81c8f2ba93 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 @@ - + From 64f67d31473aaf7aee85d0848c97d6cfe519f370 Mon Sep 17 00:00:00 2001 From: 0TTA Date: Tue, 18 Oct 2022 21:05:49 +0000 Subject: [PATCH 12/30] Translated using Weblate (Arabic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/ --- Emby.Server.Implementations/Localization/Core/ar.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 9dc2fe7996..ada3c77301 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": "استخراج صور الفصل", From e9e9dce33571a3697b38ffe239dd9ead9d15377c Mon Sep 17 00:00:00 2001 From: bobthebignose Date: Wed, 19 Oct 2022 16:56:00 +0000 Subject: [PATCH 13/30] Translated using Weblate (French (Canada)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr_CA/ --- Emby.Server.Implementations/Localization/Core/fr-CA.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 24ca8f8611..3ee045d89e 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" } From bc958c1f03a0998ffa7d398de52ccbdbe0db8edf Mon Sep 17 00:00:00 2001 From: Csaba Date: Wed, 19 Oct 2022 03:47:01 +0000 Subject: [PATCH 14/30] Translated using Weblate (Hungarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/ --- Emby.Server.Implementations/Localization/Core/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index c7f2f9c85e..62d48cebd8 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" } From 83cd1451d485e6370a562993af50de1257dd3cb5 Mon Sep 17 00:00:00 2001 From: Kmotyn Date: Wed, 19 Oct 2022 23:15:13 +0000 Subject: [PATCH 15/30] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/ --- Emby.Server.Implementations/Localization/Core/pt-BR.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 38a36a7e01..b9b93b7b6f 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" } From c1e3fa3182936942e3e28d5662f557c886e13dd7 Mon Sep 17 00:00:00 2001 From: wolong gl Date: Thu, 20 Oct 2022 02:13:07 +0000 Subject: [PATCH 16/30] Translated using Weblate (Chinese (Simplified)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index a121fc376d..ccfbeef0ce 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": "听力障碍" } From dd637620627cd886ee097e4081363558cca41144 Mon Sep 17 00:00:00 2001 From: Oskari Lavinto Date: Wed, 19 Oct 2022 15:45:55 +0000 Subject: [PATCH 17/30] Translated using Weblate (Finnish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/ --- Emby.Server.Implementations/Localization/Core/fi.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index f0cafd1c0d..ec72d58dd6 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" } From d6cf692490c9d081ce0567a918019b9ab625d216 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 19 Oct 2022 08:25:43 +0000 Subject: [PATCH 18/30] Translated using Weblate (Albanian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sq/ --- Emby.Server.Implementations/Localization/Core/sq.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json index 2766dab06a..d1b73a3eb9 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" } From 53ee43dc199c39d21867e84b6a428f3409c82b6e Mon Sep 17 00:00:00 2001 From: Urtzi Odriozola Date: Wed, 19 Oct 2022 22:23:49 +0000 Subject: [PATCH 19/30] Translated using Weblate (Basque) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eu/ --- Emby.Server.Implementations/Localization/Core/eu.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json index dfedce7b3a..d657ac7b69 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" } From 509c6ec24ca35b2e16561808792cd581c5f9d8fc Mon Sep 17 00:00:00 2001 From: Polaris Date: Tue, 18 Oct 2022 20:05:01 +0000 Subject: [PATCH 20/30] Translated using Weblate (Lojban) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/jbo/ --- Emby.Server.Implementations/Localization/Core/jbo.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/jbo.json b/Emby.Server.Implementations/Localization/Core/jbo.json index 0967ef424b..1b47bb2f23 100644 --- a/Emby.Server.Implementations/Localization/Core/jbo.json +++ b/Emby.Server.Implementations/Localization/Core/jbo.json @@ -1 +1,7 @@ -{} +{ + "Albums": "lo albuma", + "Artists": "lo larpra", + "Books": "lo cukta", + "HeaderAlbumArtists": "lo albuma larpra", + "Playlists": "lo zgipor" +} From b7882db9c72e2a07d7814e7eaf038d69837b4972 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Fri, 21 Oct 2022 10:09:45 +0200 Subject: [PATCH 21/30] Prevent host lookup on GetSmartUrl for HTTP requests --- Emby.Server.Implementations/ApplicationHost.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 909972469e..8db55a6aea 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1088,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); } /// From 7ad0c9ba24a5f248c2f5b0d89ff096779d22a2b8 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Thu, 7 Apr 2022 12:15:25 +0200 Subject: [PATCH 22/30] Migrate MusicBrainz plugin to MetaBrainz.MusicBrainz Co-authored-by: crobibero Co-authored-by: Shadowghost --- .../MediaBrowser.Providers.csproj | 1 + .../Configuration/PluginConfiguration.cs | 67 +- .../MusicBrainzAlbumArtistExternalId.cs | 33 +- .../MusicBrainz/MusicBrainzAlbumExternalId.cs | 33 +- .../MusicBrainz/MusicBrainzAlbumProvider.cs | 873 ++++-------------- .../MusicBrainzArtistExternalId.cs | 33 +- .../MusicBrainz/MusicBrainzArtistProvider.cs | 332 +++---- .../MusicBrainzOtherArtistExternalId.cs | 33 +- .../MusicBrainzReleaseGroupExternalId.cs | 33 +- .../Plugins/MusicBrainz/MusicBrainzTrackId.cs | 33 +- .../Plugins/MusicBrainz/Plugin.cs | 70 +- .../EnumerableExtensions.cs | 70 +- .../Parsers/MusicAlbumNfoProviderTests.cs | 2 +- .../Parsers/MusicArtistNfoParserTests.cs | 2 +- 14 files changed, 502 insertions(+), 1113 deletions(-) diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 3a0e9a225b..b00c036e5a 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -17,6 +17,7 @@ + diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs index 9c27bd7d3f..1d4b88087d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs @@ -1,37 +1,58 @@ -#pragma warning disable CS1591 - using MediaBrowser.Model.Plugins; +using MetaBrainz.MusicBrainz; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; -namespace MediaBrowser.Providers.Plugins.MusicBrainz +/// +/// MusicBrainz plugin configuration. +/// +public class PluginConfiguration : BasePluginConfiguration { - public class PluginConfiguration : BasePluginConfiguration - { - private string _server = Plugin.DefaultServer; + private const string DefaultServer = "musicbrainz.org"; + + private const double DefaultRateLimit = 1.0; - private long _rateLimit = Plugin.DefaultRateLimit; + private string _server = DefaultServer; + + private double _rateLimit = DefaultRateLimit; + + /// + /// Gets or sets the server url. + /// + public string Server + { + get => _server; - public string Server + set { - get => _server; - set => _server = value.TrimEnd('/'); + _server = value.TrimEnd('/'); + Query.DefaultServer = _server; } + } - public long RateLimit + /// + /// Gets or sets the rate limit. + /// + 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; } + Query.DelayBetweenRequests = _rateLimit; + } } + + /// + /// Gets or sets a value indicating whether to replace the artist name. + /// + public bool ReplaceArtistName { get; set; } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs index c54cdda3d3..f7850781e0 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; + +/// +/// MusicBrainz album artist external id. +/// +public class MusicBrainzAlbumArtistExternalId : IExternalId { - public class MusicBrainzAlbumArtistExternalId : IExternalId - { - /// - public string ProviderName => "MusicBrainz"; + /// + public string ProviderName => "MusicBrainz"; - /// - public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString(); + /// + public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString(); - /// - public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; - /// - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + /// + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// - public bool Supports(IHasProviderIds item) => item is Audio; - } + /// + 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 8f7fadd060..a9d4472e7d 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; + +/// +/// MusicBrainz album external id. +/// +public class MusicBrainzAlbumExternalId : IExternalId { - public class MusicBrainzAlbumExternalId : IExternalId - { - /// - public string ProviderName => "MusicBrainz"; + /// + public string ProviderName => "MusicBrainz"; - /// - public string Key => MetadataProvider.MusicBrainzAlbum.ToString(); + /// + public string Key => MetadataProvider.MusicBrainzAlbum.ToString(); - /// - public ExternalIdMediaType? Type => ExternalIdMediaType.Album; + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.Album; - /// - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}"; + /// + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}"; - /// - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } + /// + 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 915fb97fd2..e88a51c197 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -1,805 +1,256 @@ -#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, IHasOrder, IDisposable - { - /// - /// 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. - /// - private const uint MusicBrainzQueryAttempts = 5u; - - /// - /// 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. - /// - private readonly long _musicBrainzQueryIntervalMs; - - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger _logger; - - private readonly string _musicBrainzBaseUrl; - - private SemaphoreSlim _apiRequestLock = new SemaphoreSlim(1, 1); - private Stopwatch _stopWatchMusicBrainz = new Stopwatch(); - - public MusicBrainzAlbumProvider( - IHttpClientFactory httpClientFactory, - ILogger logger) - { - _httpClientFactory = httpClientFactory; - _logger = logger; +using MediaBrowser.Providers.Music; +using MetaBrainz.MusicBrainz; +using MetaBrainz.MusicBrainz.Interfaces.Entities; +using MetaBrainz.MusicBrainz.Interfaces.Searches; - _musicBrainzBaseUrl = Plugin.Instance.Configuration.Server; - _musicBrainzQueryIntervalMs = Plugin.Instance.Configuration.RateLimit; +namespace MediaBrowser.Providers.Plugins.MusicBrainz; - // Use a stopwatch to ensure we don't exceed the MusicBrainz rate limit - _stopWatchMusicBrainz.Start(); +/// +/// Music album metadata provider for MusicBrainz. +/// +public class MusicBrainzAlbumProvider : IRemoteMetadataProvider, IHasOrder, IDisposable +{ + private readonly Query _musicBrainzQuery; - Current = this; - } + /// + /// Initializes a new instance of the class. + /// + public MusicBrainzAlbumProvider() + { + _musicBrainzQuery = new Query(); + } - internal static MusicBrainzAlbumProvider Current { get; private set; } + /// + public string Name => "MusicBrainz"; - /// - public string Name => "MusicBrainz"; + /// + public int Order => 0; - /// - public int Order => 0; + /// + public async Task> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) + { + var releaseId = searchInfo.GetReleaseId(); + var releaseGroupId = searchInfo.GetReleaseGroupId(); - /// - public async Task> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) + if (!string.IsNullOrEmpty(releaseId)) { - 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(); + var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false); + return GetResultFromResponse(releaseResult).SingleItemAsEnumerable(); } - private IEnumerable GetResultsFromResponse(Stream stream) + if (!string.IsNullOrEmpty(releaseGroupId)) { - 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 results = ReleaseResult.Parse(reader); - - return results.Select(i => - { - var result = new RemoteSearchResult - { - Name = i.Title, - ProductionYear = i.Year - }; - - if (i.Artists.Count > 0) - { - result.AlbumArtist = new RemoteSearchResult - { - SearchProviderName = Name, - Name = i.Artists[0].Item1 - }; - - result.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, i.Artists[0].Item2); - } - - if (!string.IsNullOrWhiteSpace(i.ReleaseId)) - { - result.SetProviderId(MetadataProvider.MusicBrainzAlbum, i.ReleaseId); - } - - if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId)) - { - result.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, i.ReleaseGroupId); - } - - return result; - }); + var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.ReleaseGroups, null, cancellationToken).ConfigureAwait(false); + return GetResultsFromResponse(releaseGroupResult.Releases); } - /// - public async Task> GetMetadata(AlbumInfo info, CancellationToken cancellationToken) - { - var releaseId = info.GetReleaseId(); - var releaseGroupId = info.GetReleaseGroupId(); - - var result = new MetadataResult - { - 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); + var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId(); - 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; - } - } + if (!string.IsNullOrWhiteSpace(artistMusicBrainzId)) + { + var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{searchInfo.Name}\" AND arid:{artistMusicBrainzId}", null, null, false, cancellationToken) + .ConfigureAwait(false); - // If we have a release Id but not a release group Id... - if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId)) + if (releaseSearchResults.Results.Count > 0) { - releaseGroupId = await GetReleaseGroupFromReleaseId(releaseId, cancellationToken).ConfigureAwait(false); - result.HasMetadata = true; + return GetResultsFromResponse(releaseSearchResults.Results); } + } + 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); - if (!string.IsNullOrWhiteSpace(releaseId) || !string.IsNullOrWhiteSpace(releaseGroupId)) - { - result.HasMetadata = true; - } + var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{queryName}\" AND artist:\"{searchInfo.GetAlbumArtist()}\"c", 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 GetResultsFromResponse(releaseSearchResults.Results); } - - return result; } - private Task GetReleaseResult(string artistMusicBrainId, string artistName, string albumName, CancellationToken cancellationToken) - { - if (!string.IsNullOrEmpty(artistMusicBrainId)) - { - return GetReleaseResult(albumName, artistMusicBrainId, cancellationToken); - } - - if (string.IsNullOrWhiteSpace(artistName)) - { - return Task.FromResult(new ReleaseResult()); - } + return Enumerable.Empty(); + } - return GetReleaseResultByArtistName(albumName, artistName, cancellationToken); + private IEnumerable GetResultsFromResponse(IEnumerable>? releaseSearchResults) + { + if (releaseSearchResults is null) + { + yield break; } - private async Task GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken) + foreach (var result in releaseSearchResults) { - 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 - }; - - using var reader = XmlReader.Create(oReader, settings); - return ReleaseResult.Parse(reader).FirstOrDefault(); + yield return GetResultFromResponse(result.Item); } + } - private async Task GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken) + private IEnumerable GetResultsFromResponse(IEnumerable? releaseSearchResults) + { + if (releaseSearchResults is null) { - 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 break; } - private static (string Name, string ArtistId) ParseArtistCredit(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 "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 return GetResultFromResponse(result); } + } - private static (string Name, string ArtistId) ParseArtistNameCredit(XmlReader reader) + private RemoteSearchResult GetResultFromResponse(IRelease releaseSearchResult) + { + var searchResult = new RemoteSearchResult { - reader.MoveToContent(); - reader.Read(); + 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 "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(); - } + searchResult.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, releaseSearchResult.ArtistCredit[0].Artist!.Id.ToString()); } - - return (null, null); } - private static (string Name, string ArtistId) ParseArtistArtistCredit(XmlReader reader, string artistId) - { - reader.MoveToContent(); - reader.Read(); - - string name = null; - - // 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": - { - name = reader.ReadElementContentAsString(); - break; - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } + searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString()); - return (name, artistId); + if (releaseSearchResult.ReleaseGroup?.Id is not null) + { + searchResult.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseSearchResult.ReleaseGroup.Id.ToString()); } - private async Task GetReleaseIdFromReleaseGroupId(string releaseGroupId, CancellationToken cancellationToken) - { - var url = "/ws/2/release?release-group=" + releaseGroupId.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 - }; + /// + public async Task> 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); - var result = ReleaseResult.Parse(reader).FirstOrDefault(); + var result = new MetadataResult + { + Item = new MusicAlbum() + }; - return result?.ReleaseId; + // 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.ReleaseGroups, null, cancellationToken).ConfigureAwait(false); + var release = releaseGroup.Releases?.Count > 0 ? releaseGroup.Releases[0] : null; + releaseId = release?.Id.ToString(); + result.HasMetadata = true; } - /// - /// Gets the release group id internal. - /// - /// The release entry id. - /// The cancellation token. - /// Task{System.String}. - private async Task GetReleaseGroupFromReleaseId(string releaseEntryId, CancellationToken cancellationToken) + // If there is no release ID, lookup a release with the info we have + if (string.IsNullOrWhiteSpace(releaseId)) { - var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture); + var artistMusicBrainzId = info.GetMusicBrainzArtistId(); + IRelease? releaseResult = null; - 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 + if (!string.IsNullOrEmpty(artistMusicBrainzId)) { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - Async = true - }; - - using var reader = XmlReader.Create(oReader, settings); - await reader.MoveToContentAsync().ConfigureAwait(false); - await reader.ReadAsync().ConfigureAwait(false); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - 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); - } + 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; - } - - private string GetFirstReleaseGroupId(XmlReader reader) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + else if (!string.IsNullOrEmpty(info.GetAlbumArtist())) { - 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 artist:{info.GetAlbumArtist()}", null, null, false, cancellationToken) + .ConfigureAwait(false); + releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null; } - return null; - } - - /// - /// 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. - /// - /// Address of MusicBrainz server. - /// CancellationToken to use for method. - /// Returns response from MusicBrainz service. - internal async Task GetMusicBrainzResponse(string url, CancellationToken cancellationToken) - { - await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - try + if (releaseResult != null) { - HttpResponseMessage response; - var attempts = 0u; - var requestUrl = _musicBrainzBaseUrl.TrimEnd('/') + url; + releaseId = releaseResult.Id.ToString(); - do + if (releaseResult.ReleaseGroup?.Id is not null) { - 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). + releaseGroupId = releaseResult.ReleaseGroup.Id.ToString(); } - while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable); - // Log error if unable to query MB database due to throttling. - if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable) - { - _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl); - } - - return response; - } - finally - { - _apiRequestLock.Release(); + result.HasMetadata = true; + result.Item.ProductionYear = releaseResult.Date?.Year; + result.Item.Overview = releaseResult.Annotation; } } - /// - public Task 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; } - /// - 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 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(); + result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId); } - private static IEnumerable 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 + /// + public Task 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(); - } - } + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - return result; - } + /// + /// Dispose all resources. + /// + /// Whether to dispose. + 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 941ffea721..e2fb5621bc 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; + +/// +/// MusicBrains Artist ExternalId. +/// +public class MusicBrainzArtistExternalId : IExternalId { - public class MusicBrainzArtistExternalId : IExternalId - { - /// - public string ProviderName => "MusicBrainz"; + /// + public string ProviderName => "MusicBrainz"; - /// - public string Key => MetadataProvider.MusicBrainzArtist.ToString(); + /// + public string Key => MetadataProvider.MusicBrainzArtist.ToString(); - /// - public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; - /// - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + /// + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// - public bool Supports(IHasProviderIds item) => item is MusicArtist; - } + /// + 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 906a42f36d..1d2c9c2c81 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,159 @@ 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 - { - public string Name => "MusicBrainz"; - - /// - public async Task> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) - { - var musicBrainzId = searchInfo.GetMusicBrainzArtistId(); - - if (!string.IsNullOrWhiteSpace(musicBrainzId)) - { - var url = "/ws/2/artist/?query=arid:{0}" + musicBrainzId.ToString(CultureInfo.InvariantCulture); - - 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 - { - // They seem to throw bad request failures on any term with a slash - var nameToSearch = searchInfo.Name.Replace('/', ' '); +using MediaBrowser.Providers.Music; +using MetaBrainz.MusicBrainz; +using MetaBrainz.MusicBrainz.Interfaces.Entities; +using MetaBrainz.MusicBrainz.Interfaces.Searches; - var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch)); +namespace MediaBrowser.Providers.Plugins.MusicBrainz; - 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(); +/// +/// MusicBrainz artist provider. +/// +public class MusicBrainzArtistProvider : IRemoteMetadataProvider, IDisposable +{ + private readonly Query _musicBrainzQuery; - if (results.Count > 0) - { - return results; - } - } + /// + /// Initializes a new instance of the class. + /// + public MusicBrainzArtistProvider() + { + _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)); + /// + 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); - } - } + /// + public async Task> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) + { + var artistId = searchInfo.GetMusicBrainzArtistId(); - return Enumerable.Empty(); + if (!string.IsNullOrWhiteSpace(artistId)) + { + var artistResult = await _musicBrainzQuery.LookupArtistAsync(new Guid(artistId), Include.Artists, null, null, cancellationToken).ConfigureAwait(false); + return GetResultFromResponse(artistResult).SingleItemAsEnumerable(); } - private IEnumerable 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(); + return GetResultsFromResponse(artistSearchResults.Results); } - private IEnumerable 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) + return Enumerable.Empty(); + } + + private IEnumerable GetResultsFromResponse(IEnumerable>? releaseSearchResults) + { + if (releaseSearchResults is null) { - var result = new RemoteSearchResult(); + yield break; + } - reader.MoveToContent(); - reader.Read(); + foreach (var result in releaseSearchResults) + { + yield return GetResultFromResponse(result.Item); + } + } - // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator + private IEnumerable GetResultsFromResponse(IEnumerable? 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); + } + } - 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; - } + /// + public async Task> GetMetadata(ArtistInfo info, CancellationToken cancellationToken) + { + var result = new MetadataResult { Item = new MusicArtist() }; - return result; - } + var musicBrainzId = info.GetMusicBrainzArtistId(); - /// - public async Task> GetMetadata(ArtistInfo info, CancellationToken cancellationToken) + if (string.IsNullOrWhiteSpace(musicBrainzId)) { - var result = new MetadataResult - { - 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; } - /// - /// Encodes an URL. - /// - /// The name. - /// System.String. - 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 GetImageResponse(string url, CancellationToken cancellationToken) + return result; + } + + /// + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose all resources. + /// + /// Whether to dispose. + 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 05db2d98f7..fdaa5574f0 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; + +/// +/// MusicBrainz other artist external id. +/// +public class MusicBrainzOtherArtistExternalId : IExternalId { - public class MusicBrainzOtherArtistExternalId : IExternalId - { - /// - public string ProviderName => "MusicBrainz"; + /// + public string ProviderName => "MusicBrainz"; - /// - public string Key => MetadataProvider.MusicBrainzArtist.ToString(); + /// + public string Key => MetadataProvider.MusicBrainzArtist.ToString(); - /// - public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; - /// - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + /// + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } + /// + 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 acb652fe01..0baab9955d 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; + +/// +/// MusicBrainz release group external id. +/// +public class MusicBrainzReleaseGroupExternalId : IExternalId { - public class MusicBrainzReleaseGroupExternalId : IExternalId - { - /// - public string ProviderName => "MusicBrainz"; + /// + public string ProviderName => "MusicBrainz"; - /// - public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString(); + /// + public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString(); - /// - public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; - /// - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}"; + /// + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}"; - /// - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } + /// + 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 14805b9b79..5c974c4111 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; + +/// +/// MusicBrainz track id. +/// +public class MusicBrainzTrackId : IExternalId { - public class MusicBrainzTrackId : IExternalId - { - /// - public string ProviderName => "MusicBrainz"; + /// + public string ProviderName => "MusicBrainz"; - /// - public string Key => MetadataProvider.MusicBrainzTrack.ToString(); + /// + public string Key => MetadataProvider.MusicBrainzTrack.ToString(); - /// - public ExternalIdMediaType? Type => ExternalIdMediaType.Track; + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.Track; - /// - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}"; + /// + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}"; - /// - public bool Supports(IHasProviderIds item) => item is Audio; - } + /// + 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 cfa10dd648..270d76e6db 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs @@ -1,45 +1,63 @@ -#nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; +using System.Net.Http.Headers; +using System.Reflection; 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 +/// +/// Plugin instance. +/// +public class Plugin : BasePlugin, IHasWebPages { - public class Plugin : BasePlugin, IHasWebPages + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : 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("Jellyfin", Assembly.GetExecutingAssembly().GetName().Version?.ToString(3))); + Query.DefaultUserAgent.Add(new ProductInfoHeaderValue("(apps@jellyfin.org)")); + Query.DelayBetweenRequests = Instance.Configuration.RateLimit; + Query.DefaultServer = Instance.Configuration.Server; + } - public static Plugin Instance { get; private set; } + /// + /// Gets the current plugin instance. + /// + public static Plugin? Instance { get; private set; } - public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"); + /// + public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"); - public override string Name => "MusicBrainz"; + /// + public override string Name => "MusicBrainz"; - public override string Description => "Get artist and album metadata from any MusicBrainz server."; + /// + 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"; + /// + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml"; - public IEnumerable GetPages() + /// + public IEnumerable 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/src/Jellyfin.Extensions/EnumerableExtensions.cs b/src/Jellyfin.Extensions/EnumerableExtensions.cs index a31a57dc65..fd46358a4f 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; + +/// +/// Static extensions for the interface. +/// +public static class EnumerableExtensions { /// - /// Static extensions for the interface. + /// Determines whether the value is contained in the source collection. /// - public static class EnumerableExtensions + /// An instance of the interface. + /// The value to look for in the collection. + /// The string comparison. + /// A value indicating whether the value is contained in the collection. + /// The source is null. + public static bool Contains(this IEnumerable source, ReadOnlySpan value, StringComparison stringComparison) { - /// - /// Determines whether the value is contained in the source collection. - /// - /// An instance of the interface. - /// The value to look for in the collection. - /// The string comparison. - /// A value indicating whether the value is contained in the collection. - /// The source is null. - public static bool Contains(this IEnumerable source, ReadOnlySpan value, StringComparison stringComparison) - { - ArgumentNullException.ThrowIfNull(source); - - if (source is IList 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 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; + } + + /// + /// Gets an IEnumerable from a single item. + /// + /// The item to return. + /// The type of item. + /// The IEnumerable{T}. + public static IEnumerable SingleItemAsEnumerable(this T item) + { + yield return item; } } diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs index eea8cb50a7..8f276d03fe 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 8ca3dd96e3..78183d9ffd 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; From 385f1cc1b8304add4df6e7132d1a9cf54e8dddd4 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 27 Apr 2022 13:44:49 +0200 Subject: [PATCH 23/30] Apply review suggestions --- .../Configuration/PluginConfiguration.cs | 8 +--- .../MusicBrainz/MusicBrainzAlbumProvider.cs | 37 ++++++++++++------- .../MusicBrainzArtistExternalId.cs | 2 +- .../MusicBrainz/MusicBrainzArtistProvider.cs | 21 ++++------- .../Plugins/MusicBrainz/Plugin.cs | 9 +++-- 5 files changed, 37 insertions(+), 40 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs index 1d4b88087d..22229e377d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs @@ -23,11 +23,7 @@ public class PluginConfiguration : BasePluginConfiguration { get => _server; - set - { - _server = value.TrimEnd('/'); - Query.DefaultServer = _server; - } + set => _server = value.TrimEnd('/'); } /// @@ -46,8 +42,6 @@ public class PluginConfiguration : BasePluginConfiguration { _rateLimit = value; } - - Query.DelayBetweenRequests = _rateLimit; } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index e88a51c197..4d9feca6d1 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -28,6 +28,12 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider public MusicBrainzAlbumProvider() { + MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) => + { + Query.DefaultServer = MusicBrainz.Plugin.Instance.Configuration.Server; + Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit; + }; + _musicBrainzQuery = new Query(); } @@ -45,14 +51,14 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider 0) { - return GetResultsFromResponse(releaseSearchResults.Results); + return GetReleaseSearchResult(releaseSearchResults.Results); } } else @@ -77,14 +83,14 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider 0) { - return GetResultsFromResponse(releaseSearchResults.Results); + return GetReleaseSearchResult(releaseSearchResults.Results); } } return Enumerable.Empty(); } - private IEnumerable GetResultsFromResponse(IEnumerable>? releaseSearchResults) + private IEnumerable GetReleaseSearchResult(IEnumerable>? releaseSearchResults) { if (releaseSearchResults is null) { @@ -93,11 +99,11 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider GetResultsFromResponse(IEnumerable? releaseSearchResults) + private IEnumerable GetReleaseGroupResult(IEnumerable? releaseSearchResults) { if (releaseSearchResults is null) { @@ -106,11 +112,11 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider 0 ? releaseGroup.Releases[0] : null; - releaseId = release?.Id.ToString(); - result.HasMetadata = true; + if (release != null) + { + releaseId = release.Id.ToString(); + result.HasMetadata = true; + } } // If there is no release ID, lookup a release with the info we have diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs index e2fb5621bc..b89e67270a 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.MusicBrainz; /// -/// MusicBrains Artist ExternalId. +/// MusicBrainz artist external id. /// public class MusicBrainzArtistExternalId : IExternalId { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index 1d2c9c2c81..2cc3a13bef 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -29,6 +29,12 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider public MusicBrainzArtistProvider() { + MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) => + { + Query.DefaultServer = MusicBrainz.Plugin.Instance.Configuration.Server; + Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit; + }; + _musicBrainzQuery = new Query(); } @@ -42,7 +48,7 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider GetResultsFromResponse(IEnumerable? releaseSearchResults) - { - if (releaseSearchResults is null) - { - yield break; - } - - foreach (var result in releaseSearchResults) - { - yield return GetResultFromResponse(result); - } - } - private RemoteSearchResult GetResultFromResponse(IArtist artist) { var searchResult = new RemoteSearchResult diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs index 270d76e6db..39cfd727f3 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Net.Http.Headers; -using System.Reflection; +using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Plugins; @@ -21,14 +21,15 @@ public class Plugin : BasePlugin, IHasWebPages /// /// Instance of the interface. /// Instance of the interface. - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + /// Instance of the interface. + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, IApplicationHost applicationHost) : base(applicationPaths, xmlSerializer) { Instance = this; // TODO: Change this to "JellyfinMusicBrainzPlugin" once we take it out of the server repo. - Query.DefaultUserAgent.Add(new ProductInfoHeaderValue("Jellyfin", Assembly.GetExecutingAssembly().GetName().Version?.ToString(3))); - Query.DefaultUserAgent.Add(new ProductInfoHeaderValue("(apps@jellyfin.org)")); + 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; } From 2789f8d04e859a531827ed7f63cb087890f5c773 Mon Sep 17 00:00:00 2001 From: DJSweder Date: Fri, 21 Oct 2022 20:17:13 +0000 Subject: [PATCH 24/30] Translated using Weblate (Czech) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/ --- Emby.Server.Implementations/Localization/Core/cs.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 943fc651f7..08db5a30e0 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í" } From 092c87a281f3fad9b0ae9d08c96c43686fcb855b Mon Sep 17 00:00:00 2001 From: Andi Chandler Date: Sat, 22 Oct 2022 22:34:26 +0000 Subject: [PATCH 25/30] Translated using Weblate (English (United Kingdom)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en_GB/ --- Emby.Server.Implementations/Localization/Core/en-GB.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 862410c54b..2436883883 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" } From 96e8583b2cd2996945cc519ebbb18316b7b9178d Mon Sep 17 00:00:00 2001 From: nlahmi Date: Sat, 22 Oct 2022 22:10:45 +0000 Subject: [PATCH 26/30] Translated using Weblate (Hebrew) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/ --- Emby.Server.Implementations/Localization/Core/he.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index c635dab23a..694a3d688c 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": "לקוי שמיעה" } From 4fbead582a3016ab41412a6a4aaef09e22b2e3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xos=C3=A9=20m?= Date: Sat, 22 Oct 2022 04:56:08 +0000 Subject: [PATCH 27/30] Translated using Weblate (Galician) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gl/ --- Emby.Server.Implementations/Localization/Core/gl.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index b433c6f68c..76a98aa54b 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" } From 9b88af1fb4f69d5f5d0feb628eb2bfef60f8bad1 Mon Sep 17 00:00:00 2001 From: Franco Castillo Date: Mon, 24 Oct 2022 04:08:43 +0000 Subject: [PATCH 28/30] Translated using Weblate (Spanish (Argentina)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_AR/ --- Emby.Server.Implementations/Localization/Core/es-AR.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index 1289172bac..8ad9e8c716 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" } From bc4c34386bdd010503fd18008e2418bcf8ba1760 Mon Sep 17 00:00:00 2001 From: Raditya Harya Date: Mon, 24 Oct 2022 14:47:11 +0000 Subject: [PATCH 29/30] Translated using Weblate (Indonesian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/id/ --- Emby.Server.Implementations/Localization/Core/id.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index 3e05525c87..695c0f4048 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" } From 790f67aac11e5c32bad19126d4e35b2afa259006 Mon Sep 17 00:00:00 2001 From: lyaschuchenko Date: Mon, 24 Oct 2022 06:34:54 +0000 Subject: [PATCH 30/30] Translated using Weblate (Ukrainian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/ --- Emby.Server.Implementations/Localization/Core/uk.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 3e0fd11c8e..92ce616f2e 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": "З порушеннями слуху" }