Compare commits

..

1 Commits

Author SHA1 Message Date
Andrew Rabert
3d0b84ce48 ci: use workflow_run pattern for PR workflows 2026-02-01 15:28:27 -05:00
49 changed files with 364 additions and 554 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "10.0.3", "version": "10.0.2",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
] ]

View File

@@ -1,31 +1,17 @@
{ {
"name": "Development Jellyfin Server", "name": "Development Jellyfin Server",
"image": "mcr.microsoft.com/devcontainers/dotnet:10.0-noble", "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
"service": "app", "service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate // restores nuget packages, installs the dotnet workloads and installs the dev https certificate
"postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
// The previous way of installing extensions via the vs command dont work on selfhosted devcontainers // reads the extensions list and installs them
"customizations": { "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"vscode": {
"extensions": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.csdevkit",
"alexcvzz.vscode-sqlite",
"streetsidesoftware.code-spell-checker",
"eamodio.gitlens",
"redhat.vscode-xml"
]
}
},
"features": { "features": {
"ghcr.io/devcontainers/features/dotnet:2": { "ghcr.io/devcontainers/features/dotnet:2": {
"version": "none", "version": "none",
"dotnetRuntimeVersions": "10.0", "dotnetRuntimeVersions": "9.0",
"aspNetCoreRuntimeVersions": "10.0" "aspNetCoreRuntimeVersions": "9.0"
}, },
"ghcr.io/devcontainers-extra/features/apt-packages:1": { "ghcr.io/devcontainers-extra/features/apt-packages:1": {
"preserve_apt_list": false, "preserve_apt_list": false,

View File

@@ -28,13 +28,13 @@ jobs:
dotnet-version: '10.0.x' dotnet-version: '10.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0

55
.github/workflows/ci-compat-build.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: ABI Compatibility Build
on:
pull_request:
permissions: {}
jobs:
abi-head:
name: ABI - HEAD
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Build
run: dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: abi-head
retention-days: 1
if-no-files-found: error
path: out/
abi-base:
name: ABI - BASE
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Build
run: dotnet build Jellyfin.Server -o ./out
- name: Upload Base
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: abi-base
retention-days: 1
if-no-files-found: error
path: out/

View File

@@ -1,87 +1,20 @@
name: ABI Compatibility name: ABI Compatibility
on: on:
pull_request: workflow_run:
workflows: ["ABI Compatibility Build"]
types: [completed]
permissions: {} permissions: {}
jobs: jobs:
abi-head:
name: ABI - HEAD
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Build
run: |
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: abi-head
retention-days: 14
if-no-files-found: error
path: out/
abi-base:
name: ABI - BASE
if: ${{ github.base_ref != '' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Build
run: |
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: abi-base
retention-days: 14
if-no-files-found: error
path: out/
abi-diff: abi-diff:
permissions: permissions:
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment) pull-requests: write
name: ABI - Difference name: ABI - Difference
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs:
- abi-head
- abi-base
steps: steps:
- name: Download abi-head - name: Download abi-head
@@ -89,12 +22,16 @@ jobs:
with: with:
name: abi-head name: abi-head
path: abi-head path: abi-head
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download abi-base - name: Download abi-base
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: abi-base name: abi-base
path: abi-base path: abi-base
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup ApiCompat - name: Setup ApiCompat
run: | run: |
@@ -118,7 +55,7 @@ jobs:
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment id: find-comment
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.workflow_run.pull_requests[0].number }}
direction: last direction: last
body-includes: abi-diff-workflow-comment body-includes: abi-diff-workflow-comment
@@ -126,7 +63,7 @@ jobs:
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.diff.outputs.body != '' }} if: ${{ steps.diff.outputs.body != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.workflow_run.pull_requests[0].number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }} comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace edit-mode: replace
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@@ -145,7 +82,7 @@ jobs:
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.workflow_run.pull_requests[0].number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }} comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace edit-mode: replace
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}

55
.github/workflows/ci-openapi-build.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: OpenAPI Build
on:
pull_request:
permissions: {}
jobs:
openapi-head:
name: OpenAPI - HEAD
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-head
retention-days: 1
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
openapi-base:
name: OpenAPI - BASE
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-base
retention-days: 1
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json

View File

@@ -1,30 +1,31 @@
name: OpenAPI name: OpenAPI
on: on:
push: push:
branches: branches:
- master - master
tags: tags:
- 'v*' - 'v*'
pull_request: workflow_run:
workflows: ["OpenAPI Build"]
types: [completed]
permissions: {} permissions: {}
jobs: jobs:
openapi-head: openapi-head:
name: OpenAPI - HEAD name: OpenAPI - HEAD
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with: with:
dotnet-version: '10.0.x' dotnet-version: '10.0.x'
- name: Generate openapi.json - 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" run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
@@ -36,65 +37,29 @@ jobs:
if-no-files-found: error if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
openapi-base:
name: OpenAPI - BASE
if: ${{ github.base_ref != '' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
openapi-diff: openapi-diff:
permissions: permissions:
pull-requests: write pull-requests: write
name: OpenAPI - Difference name: OpenAPI - Difference
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs:
- openapi-head
- openapi-base
steps: steps:
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download openapi-base - name: Download openapi-base
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: openapi-base name: openapi-base
path: openapi-base path: openapi-base
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Detect OpenAPI changes - name: Detect OpenAPI changes
id: openapi-diff id: openapi-diff
@@ -105,11 +70,12 @@ jobs:
markdown: openapi-changelog.md markdown: openapi-changelog.md
add-pr-comment: true add-pr-comment: true
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
pr-number: ${{ github.event.workflow_run.pull_requests[0].number }}
publish-unstable: publish-unstable:
name: OpenAPI - Publish Unstable Spec name: OpenAPI - Publish Unstable Spec
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }} if: ${{ github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- openapi-head - openapi-head
@@ -170,7 +136,7 @@ jobs:
publish-stable: publish-stable:
name: OpenAPI - Publish Stable Spec name: OpenAPI - Publish Stable Spec
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }} if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- openapi-head - openapi-head

View File

@@ -4,7 +4,7 @@ on:
types: types:
- created - created
- edited - edited
pull_request: pull_request_target:
types: types:
- labeled - labeled
- synchronize - synchronize

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with: with:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true ascending: true

View File

@@ -4,7 +4,7 @@ on:
push: push:
branches: branches:
- master - master
pull_request: pull_request_target:
issue_comment: issue_comment:
permissions: {} permissions: {}

View File

@@ -4,7 +4,7 @@ on:
push: push:
branches: branches:
- master - master
pull_request: pull_request_target:
issue_comment: issue_comment:
permissions: {} permissions: {}
@@ -16,7 +16,7 @@ jobs:
steps: steps:
- name: Apply label - name: Apply label
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request'}} if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with: with:
dirtyLabel: 'merge conflict' dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with: with:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true ascending: true

View File

@@ -287,4 +287,3 @@
- [Martin Reuter](https://github.com/reuterma24) - [Martin Reuter](https://github.com/reuterma24)
- [Michael McElroy](https://github.com/mcmcelro) - [Michael McElroy](https://github.com/mcmcelro)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy) - [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
- [DerMaddis](https://github.com/dermaddis)

View File

@@ -4,7 +4,7 @@
</PropertyGroup> </PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies"> <ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="8.0.2" /> <PackageVersion Include="AsyncKeyedLock" Version="8.0.0" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" /> <PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -13,7 +13,7 @@
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="8.0.0" /> <PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Diacritics" Version="4.1.4" /> <PackageVersion Include="Diacritics" Version="4.1.4" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
@@ -26,27 +26,27 @@
<PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.3" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" />
@@ -74,13 +74,13 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.4.1" /> <PackageVersion Include="Svg.Skia" Version="3.2.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" />
<PackageVersion Include="System.Text.Json" Version="10.0.3" /> <PackageVersion Include="System.Text.Json" Version="10.0.2" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.11.0" /> <PackageVersion Include="z440.atl.core" Version="7.10.0" />
<PackageVersion Include="TMDbLib" Version="3.0.0" /> <PackageVersion Include="TMDbLib" Version="2.3.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />

View File

@@ -18,7 +18,7 @@ public static class TvParserHelpers
/// <param name="status">The status string.</param> /// <param name="status">The status string.</param>
/// <param name="enumValue">The <see cref="SeriesStatus"/>.</param> /// <param name="enumValue">The <see cref="SeriesStatus"/>.</param>
/// <returns>Returns true if parsing was successful.</returns> /// <returns>Returns true if parsing was successful.</returns>
public static bool TryParseSeriesStatus(string? status, out SeriesStatus? enumValue) public static bool TryParseSeriesStatus(string status, out SeriesStatus? enumValue)
{ {
if (Enum.TryParse(status, true, out SeriesStatus seriesStatus)) if (Enum.TryParse(status, true, out SeriesStatus seriesStatus))
{ {

View File

@@ -267,24 +267,22 @@ namespace Emby.Server.Implementations.Images
{ {
var image = item.GetImageInfo(type, 0); var image = item.GetImageInfo(type, 0);
if (image is null) if (image is not null)
{ {
return GetItemsWithImages(item).Count is not 0; if (!image.IsLocalFile)
} {
return false;
}
if (!image.IsLocalFile) if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
{ {
return false; return false;
} }
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) if (!HasChangedByDate(item, image))
{ {
return false; return false;
} }
if (!HasChangedByDate(item, image))
{
return false;
} }
return true; return true;

View File

@@ -37,25 +37,15 @@ namespace Emby.Server.Implementations.Library
while (attributeIndex > -1 && attributeIndex < maxIndex) while (attributeIndex > -1 && attributeIndex < maxIndex)
{ {
var attributeEnd = attributeIndex + attribute.Length; var attributeEnd = attributeIndex + attribute.Length;
if (attributeIndex > 0) if (attributeIndex > 0
&& str[attributeIndex - 1] == '['
&& (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
{ {
var attributeOpener = str[attributeIndex - 1]; var closingIndex = str[attributeEnd..].IndexOf(']');
var attributeCloser = attributeOpener switch // Must be at least 1 character before the closing bracket.
if (closingIndex > 1)
{ {
'[' => ']', return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
'(' => ')',
'{' => '}',
_ => '\0'
};
if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
{
var closingIndex = str[attributeEnd..].IndexOf(attributeCloser);
// Must be at least 1 character before the closing bracket.
if (closingIndex > 1)
{
return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
}
} }
} }

View File

@@ -3,7 +3,7 @@
"Playlists": "Плэй-лісты", "Playlists": "Плэй-лісты",
"Latest": "Апошняе", "Latest": "Апошняе",
"LabelIpAddressValue": "IP-адрас: {0}", "LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} дададзены ў бібліятэку", "ItemAddedWithName": "{0} даданы ў бібліятэку",
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены", "MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана", "NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны", "PluginInstalledWithName": "{0} быў усталяваны",
@@ -14,7 +14,7 @@
"Channels": "Каналы", "Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}", "ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі", "Collections": "Калекцыі",
"Default": радвызначана", "Default": а змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі", "Folders": "Папкі",
"Favorites": "Абранае", "Favorites": "Абранае",
@@ -81,8 +81,8 @@
"NotificationOptionInstallationFailed": "Збой усталёўкі", "NotificationOptionInstallationFailed": "Збой усталёўкі",
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.", "NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана", "NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыя спынена", "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена",
"NotificationOptionAudioPlayback": "Прайграванне аўдыя пачалося", "NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося",
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт", "NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
"NotificationOptionPluginError": "Збой плагіна", "NotificationOptionPluginError": "Збой плагіна",
"NotificationOptionPluginUninstalled": "Плагін выдалены", "NotificationOptionPluginUninstalled": "Плагін выдалены",

View File

@@ -104,7 +104,7 @@
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.", "TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja dels registres", "TaskCleanLogs": "Neteja dels registres",
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.", "TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneja la mediateca", "TaskRefreshLibrary": "Escaneig de les mediateques",
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.", "TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols", "TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.", "TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",

View File

@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}", "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
"Channels": "Kanali", "Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}", "ChapterNameValue": "Poglavlje {0}",
"Collections": "Zbirke", "Collections": "Kolekcije",
"DeviceOfflineWithName": "{0} je prekinuo vezu", "DeviceOfflineWithName": "{0} je prekinuo vezu",
"DeviceOnlineWithName": "{0} je povezan", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}", "FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} neuspjelo", "ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto", "ScheduledTaskStartedWithName": "{0} pokrenuto",
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti", "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
"Shows": "Emisije", "Shows": "Serije",
"Songs": "Pjesme", "Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.", "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}", "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",

View File

@@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Dispositivo: {1}", "AppDeviceValues": "App: {0}, Dispositivo: {1}",
"Application": "Applicazione", "Application": "Applicazione",
"Artists": "Artisti", "Artists": "Artisti",
"AuthenticationSucceededWithUserName": "{0} autenticato correttamente", "AuthenticationSucceededWithUserName": "{0} autenticato con successo",
"Books": "Libri", "Books": "Libri",
"CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}", "CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}",
"Channels": "Canali", "Channels": "Canali",
@@ -11,36 +11,36 @@
"Collections": "Collezioni", "Collections": "Collezioni",
"DeviceOfflineWithName": "{0} si è disconnesso", "DeviceOfflineWithName": "{0} si è disconnesso",
"DeviceOnlineWithName": "{0} è connesso", "DeviceOnlineWithName": "{0} è connesso",
"FailedLoginAttemptWithUserName": "Tentativo di accesso non riuscito da {0}", "FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}",
"Favorites": "Preferiti", "Favorites": "Preferiti",
"Folders": "Cartelle", "Folders": "Cartelle",
"Genres": "Generi", "Genres": "Generi",
"HeaderAlbumArtists": "Artisti dell'album", "HeaderAlbumArtists": "Artisti dell'album",
"HeaderContinueWatching": "Continua a guardare", "HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album preferiti", "HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti preferiti", "HeaderFavoriteArtists": "Artisti Preferiti",
"HeaderFavoriteEpisodes": "Episodi preferiti", "HeaderFavoriteEpisodes": "Episodi Preferiti",
"HeaderFavoriteShows": "Serie TV preferite", "HeaderFavoriteShows": "Serie TV Preferite",
"HeaderFavoriteSongs": "Brani preferiti", "HeaderFavoriteSongs": "Brani Preferiti",
"HeaderLiveTV": "Diretta TV", "HeaderLiveTV": "Diretta TV",
"HeaderNextUp": "Prossimo", "HeaderNextUp": "Prossimo",
"HeaderRecordingGroups": "Gruppi di registrazione", "HeaderRecordingGroups": "Gruppi di Registrazione",
"HomeVideos": "Video personali", "HomeVideos": "Video Personali",
"Inherit": "Eredita", "Inherit": "Eredita",
"ItemAddedWithName": "{0} è stato aggiunto alla libreria", "ItemAddedWithName": "{0} è stato aggiunto alla libreria",
"ItemRemovedWithName": "{0} è stato rimosso dalla libreria", "ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
"LabelIpAddressValue": "Indirizzo IP: {0}", "LabelIpAddressValue": "Indirizzo IP: {0}",
"LabelRunningTimeValue": "Durata: {0}", "LabelRunningTimeValue": "Durata: {0}",
"Latest": "Novità", "Latest": "Novità",
"MessageApplicationUpdated": "Jellyfin Server è stato aggiornato", "MessageApplicationUpdated": "Il Server Jellyfin è stato aggiornato",
"MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}", "MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata", "MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata",
"MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata", "MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata",
"MixedContent": "Contenuto misto", "MixedContent": "Contenuto misto",
"Movies": "Film", "Movies": "Film",
"Music": "Musica", "Music": "Musica",
"MusicVideos": "Video musicali", "MusicVideos": "Video Musicali",
"NameInstallFailed": "{0} installazione non riuscita", "NameInstallFailed": "{0} installazione fallita",
"NameSeasonNumber": "Stagione {0}", "NameSeasonNumber": "Stagione {0}",
"NameSeasonUnknown": "Stagione sconosciuta", "NameSeasonUnknown": "Stagione sconosciuta",
"NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.", "NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.",
@@ -49,37 +49,37 @@
"NotificationOptionAudioPlayback": "La riproduzione audio è iniziata", "NotificationOptionAudioPlayback": "La riproduzione audio è iniziata",
"NotificationOptionAudioPlaybackStopped": "La riproduzione audio è stata interrotta", "NotificationOptionAudioPlaybackStopped": "La riproduzione audio è stata interrotta",
"NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata", "NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata",
"NotificationOptionInstallationFailed": "Installazione non riuscita", "NotificationOptionInstallationFailed": "Installazione fallita",
"NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto", "NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto",
"NotificationOptionPluginError": "Errore del plugin", "NotificationOptionPluginError": "Errore del plugin",
"NotificationOptionPluginInstalled": "Plugin installato", "NotificationOptionPluginInstalled": "Plugin installato",
"NotificationOptionPluginUninstalled": "Plugin disinstallato", "NotificationOptionPluginUninstalled": "Plugin disinstallato",
"NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato", "NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato",
"NotificationOptionServerRestartRequired": "Riavvio del server necessario", "NotificationOptionServerRestartRequired": "Riavvio del server necessario",
"NotificationOptionTaskFailed": "Operazione pianificata non riuscita", "NotificationOptionTaskFailed": "Operazione pianificata fallita",
"NotificationOptionUserLockedOut": "Utente bloccato", "NotificationOptionUserLockedOut": "Utente bloccato",
"NotificationOptionVideoPlayback": "Riproduzione video iniziata", "NotificationOptionVideoPlayback": "Riproduzione video iniziata",
"NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta", "NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
"Photos": "Foto", "Photos": "Foto",
"Playlists": "Scalette", "Playlists": "Playlist",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} è stato installato", "PluginInstalledWithName": "{0} è stato Installato",
"PluginUninstalledWithName": "{0} è stato disinstallato", "PluginUninstalledWithName": "{0} è stato disinstallato",
"PluginUpdatedWithName": "{0} è stato aggiornato", "PluginUpdatedWithName": "{0} è stato aggiornato",
"ProviderValue": "Provider: {0}", "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} non riuscito", "ScheduledTaskFailedWithName": "{0} fallito",
"ScheduledTaskStartedWithName": "{0} avviato", "ScheduledTaskStartedWithName": "{0} avviato",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato", "ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Serie TV", "Shows": "Serie TV",
"Songs": "Brani", "Songs": "Brani",
"StartupEmbyServerIsLoading": "Jellyfin Server si sta avviando. Riprova più tardi.", "StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
"SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}", "SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}",
"Sync": "Sincronizza", "Sync": "Sincronizza",
"System": "Sistema", "System": "Sistema",
"TvShows": "Serie TV", "TvShows": "Serie TV",
"User": "Utente", "User": "Utente",
"UserCreatedWithName": "L'utente {0} è stato creato", "UserCreatedWithName": "L'utente {0} è stato creato",
"UserDeletedWithName": "L'utente {0} è stato eliminato", "UserDeletedWithName": "L'utente {0} è stato rimosso",
"UserDownloadingItemWithValues": "{0} sta scaricando {1}", "UserDownloadingItemWithValues": "{0} sta scaricando {1}",
"UserLockedOutWithName": "L'utente {0} è stato bloccato", "UserLockedOutWithName": "L'utente {0} è stato bloccato",
"UserOfflineFromDevice": "{0} si è disconnesso da {1}", "UserOfflineFromDevice": "{0} si è disconnesso da {1}",
@@ -114,20 +114,20 @@
"TasksLibraryCategory": "Libreria", "TasksLibraryCategory": "Libreria",
"TasksMaintenanceCategory": "Manutenzione", "TasksMaintenanceCategory": "Manutenzione",
"TaskCleanActivityLog": "Attività di Registro Completate", "TaskCleanActivityLog": "Attività di Registro Completate",
"TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell'età configurata.", "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie delletà configurata.",
"Undefined": "Non specificato", "Undefined": "Non Definito",
"Forced": "Forzato", "Forced": "Forzato",
"Default": "Predefinito", "Default": "Predefinito",
"TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.", "TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.",
"TaskOptimizeDatabase": "Ottimizza database", "TaskOptimizeDatabase": "Ottimizza database",
"TaskKeyframeExtractor": "Estrattore di Keyframe", "TaskKeyframeExtractor": "Estrattore di Keyframe",
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori scalette HLS. Questa procedura potrebbe richiedere molto tempo.", "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
"External": "Esterno", "External": "Esterno",
"HearingImpaired": "Non udenti", "HearingImpaired": "Non Udenti",
"TaskRefreshTrickplayImages": "Genera immagini Trickplay", "TaskRefreshTrickplayImages": "Genera immagini Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.", "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
"TaskCleanCollectionsAndPlaylists": "Ripulisci le collezioni e le scalette", "TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist",
"TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle scalette che non esistono più.", "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle playlist che non esistono più.",
"TaskAudioNormalization": "Normalizzazione dell'audio", "TaskAudioNormalization": "Normalizzazione dell'audio",
"TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.", "TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni", "TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",

View File

@@ -43,32 +43,32 @@
"NameInstallFailed": "{0}のインストールに失敗しました", "NameInstallFailed": "{0}のインストールに失敗しました",
"NameSeasonNumber": "シーズン {0}", "NameSeasonNumber": "シーズン {0}",
"NameSeasonUnknown": "シーズン不明", "NameSeasonUnknown": "シーズン不明",
"NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロードできます。", "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。",
"NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります", "NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります",
"NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です", "NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です",
"NotificationOptionAudioPlayback": "オーディオの再生を開始", "NotificationOptionAudioPlayback": "オーディオの再生を開始",
"NotificationOptionAudioPlaybackStopped": "オーディオの再生を停止", "NotificationOptionAudioPlaybackStopped": "オーディオの再生をストップしました",
"NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました", "NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました",
"NotificationOptionInstallationFailed": "インストール失敗", "NotificationOptionInstallationFailed": "インストール失敗",
"NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました", "NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました",
"NotificationOptionPluginError": "プラグインに障害が発生しました", "NotificationOptionPluginError": "プラグインに障害が発生しました",
"NotificationOptionPluginInstalled": "プラグインインストールました", "NotificationOptionPluginInstalled": "プラグインインストールされました",
"NotificationOptionPluginUninstalled": "プラグインアンインストールました", "NotificationOptionPluginUninstalled": "プラグインアンインストールされました",
"NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました", "NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました",
"NotificationOptionServerRestartRequired": "サーバーを再起動してください", "NotificationOptionServerRestartRequired": "サーバーを再起動してください",
"NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗", "NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗",
"NotificationOptionUserLockedOut": "ユーザーはロックされています", "NotificationOptionUserLockedOut": "ユーザーはロックされています",
"NotificationOptionVideoPlayback": "ビデオの再生を開始", "NotificationOptionVideoPlayback": "ビデオの再生を開始しました",
"NotificationOptionVideoPlaybackStopped": "ビデオの再生を停止", "NotificationOptionVideoPlaybackStopped": "ビデオを停止しました",
"Photos": "フォト", "Photos": "フォト",
"Playlists": "プレイリスト", "Playlists": "プレイリスト",
"Plugin": "プラグイン", "Plugin": "プラグイン",
"PluginInstalledWithName": "{0} インストールました", "PluginInstalledWithName": "{0} インストールされました",
"PluginUninstalledWithName": "{0} アンインストールました", "PluginUninstalledWithName": "{0} アンインストールされました",
"PluginUpdatedWithName": "{0} 更新ました", "PluginUpdatedWithName": "{0} 更新されました",
"ProviderValue": "プロバイダ: {0}", "ProviderValue": "プロバイダ: {0}",
"ScheduledTaskFailedWithName": "{0} が失敗しました", "ScheduledTaskFailedWithName": "{0} が失敗しました",
"ScheduledTaskStartedWithName": "{0} 開始", "ScheduledTaskStartedWithName": "{0} 開始されました",
"ServerNameNeedsToBeRestarted": "{0} を再起動してください", "ServerNameNeedsToBeRestarted": "{0} を再起動してください",
"Shows": "番組", "Shows": "番組",
"Songs": "曲", "Songs": "曲",

View File

@@ -124,8 +124,8 @@
"TaskKeyframeExtractor": "Extrator de Quadros-chave", "TaskKeyframeExtractor": "Extrator de Quadros-chave",
"External": "Externo", "External": "Externo",
"HearingImpaired": "Surdo", "HearingImpaired": "Surdo",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay", "TaskRefreshTrickplayImages": "Gerar Imagens de Trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.", "TaskRefreshTrickplayImagesDescription": "Cria ficheiros de trickplay para vídeos nas bibliotecas ativas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução", "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",

View File

@@ -124,8 +124,8 @@
"HearingImpaired": "Problemas auditivos", "HearingImpaired": "Problemas auditivos",
"TaskKeyframeExtractor": "Extrator de quadro-chave", "TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.", "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay", "TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.", "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução", "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",

View File

@@ -198,22 +198,17 @@ namespace Emby.Server.Implementations.Playlists
return Playlist.GetPlaylistItems(items, user, options); return Playlist.GetPlaylistItems(items, user, options);
} }
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, int? position, Guid userId) public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
{ {
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
return AddToPlaylistInternal( return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
playlistId, {
itemIds, EnableImages = true
user, });
new DtoOptions(false)
{
EnableImages = true
},
position);
} }
private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options, int? position = null) private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options)
{ {
// Retrieve the existing playlist // Retrieve the existing playlist
var playlist = _libraryManager.GetItemById(playlistId) as Playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist
@@ -248,30 +243,7 @@ namespace Emby.Server.Implementations.Playlists
} }
// Update the playlist in the repository // Update the playlist in the repository
if (position.HasValue) playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
{
if (position.Value <= 0)
{
playlist.LinkedChildren = [.. childrenToAdd, .. playlist.LinkedChildren];
}
else if (position.Value >= playlist.LinkedChildren.Length)
{
playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
}
else
{
playlist.LinkedChildren = [
.. playlist.LinkedChildren[0..position.Value],
.. childrenToAdd,
.. playlist.LinkedChildren[position.Value..playlist.LinkedChildren.Length]
];
}
}
else
{
playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
}
playlist.DateLastMediaAdded = DateTime.UtcNow; playlist.DateLastMediaAdded = DateTime.UtcNow;
await UpdatePlaylistInternal(playlist).ConfigureAwait(false); await UpdatePlaylistInternal(playlist).ConfigureAwait(false);

View File

@@ -191,17 +191,9 @@ public class DisplayPreferencesController : BaseJellyfinApiController
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase))) foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{ {
var viewType = displayPreferences.CustomPrefs[key]; if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out _))
if (string.IsNullOrEmpty(viewType))
{ {
displayPreferences.CustomPrefs.Remove(key); _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
continue;
}
if (!Enum.TryParse<ViewType>(viewType, true, out _))
{
_logger.LogError("Invalid ViewType: {LandingScreenOption}", viewType);
displayPreferences.CustomPrefs.Remove(key); displayPreferences.CustomPrefs.Remove(key);
} }
} }

View File

@@ -359,7 +359,6 @@ public class PlaylistsController : BaseJellyfinApiController
/// </summary> /// </summary>
/// <param name="playlistId">The playlist id.</param> /// <param name="playlistId">The playlist id.</param>
/// <param name="ids">Item id, comma delimited.</param> /// <param name="ids">Item id, comma delimited.</param>
/// <param name="position">Optional. 0-based index where to place the items or at the end if <c>null</c>.</param>
/// <param name="userId">The userId.</param> /// <param name="userId">The userId.</param>
/// <response code="204">Items added to playlist.</response> /// <response code="204">Items added to playlist.</response>
/// <response code="403">Access forbidden.</response> /// <response code="403">Access forbidden.</response>
@@ -372,7 +371,6 @@ public class PlaylistsController : BaseJellyfinApiController
public async Task<ActionResult> AddItemToPlaylist( public async Task<ActionResult> AddItemToPlaylist(
[FromRoute, Required] Guid playlistId, [FromRoute, Required] Guid playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
[FromQuery] int? position,
[FromQuery] Guid? userId) [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId); userId = RequestHelpers.GetUserId(User, userId);
@@ -390,7 +388,7 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid(); return Forbid();
} }
await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, position, userId.Value).ConfigureAwait(false); await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false);
return NoContent(); return NoContent();
} }

View File

@@ -74,10 +74,9 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
/// <inheritdoc /> /// <inheritdoc />
public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people) public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
{ {
foreach (var person in people) foreach (var item in people.Where(e => e.Role is null))
{ {
person.Name = person.Name.Trim(); item.Role = string.Empty;
person.Role = person.Role?.Trim() ?? string.Empty;
} }
// multiple metadata providers can provide the _same_ person // multiple metadata providers can provide the _same_ person

View File

@@ -1163,9 +1163,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
Item = null!, Item = null!,
ProviderId = e[0], ProviderId = e[0],
ProviderValue = string.Join('|', e.Skip(1)) ProviderValue = string.Join('|', e.Skip(1))
}) }).ToArray();
.DistinctBy(e => e.ProviderId)
.ToArray();
} }
if (reader.TryGetString(index++, out var imageInfos)) if (reader.TryGetString(index++, out var imageInfos))

View File

@@ -452,7 +452,6 @@ namespace MediaBrowser.Controller.Entities
// That's all the new and changed ones - now see if any have been removed and need cleanup // That's all the new and changed ones - now see if any have been removed and need cleanup
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList(); var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
var shouldRemove = !IsRoot || allowRemoveRoot; var shouldRemove = !IsRoot || allowRemoveRoot;
var actuallyRemoved = new List<BaseItem>();
// If it's an AggregateFolder, don't remove // If it's an AggregateFolder, don't remove
if (shouldRemove && itemsRemoved.Count > 0) if (shouldRemove && itemsRemoved.Count > 0)
{ {
@@ -468,7 +467,6 @@ namespace MediaBrowser.Controller.Entities
{ {
Logger.LogDebug("Removed item: {Path}", item.Path); Logger.LogDebug("Removed item: {Path}", item.Path);
actuallyRemoved.Add(item);
item.SetParent(null); item.SetParent(null);
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false); LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
} }
@@ -479,20 +477,6 @@ namespace MediaBrowser.Controller.Entities
{ {
LibraryManager.CreateItems(newItems, this, cancellationToken); LibraryManager.CreateItems(newItems, this, cancellationToken);
} }
// After removing items, reattach any detached user data to remaining children
// that share the same user data keys (eg. same episode replaced with a new file).
if (actuallyRemoved.Count > 0)
{
var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet();
foreach (var child in validChildren)
{
if (child.GetUserDataKeys().Any(removedKeys.Contains))
{
await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
}
}
}
} }
else else
{ {

View File

@@ -451,7 +451,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual) if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual)
{ {
return episodeItem.Season is null or { LocationType: LocationType.Virtual }; return true;
} }
var season = episodeItem.Season; var season = episodeItem.Season;

View File

@@ -61,10 +61,9 @@ namespace MediaBrowser.Controller.Playlists
/// </summary> /// </summary>
/// <param name="playlistId">The playlist identifier.</param> /// <param name="playlistId">The playlist identifier.</param>
/// <param name="itemIds">The item ids.</param> /// <param name="itemIds">The item ids.</param>
/// <param name="position">Optional. 0-based index where to place the items or at the end if null.</param>
/// <param name="userId">The user identifier.</param> /// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, int? position, Guid userId); Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
/// <summary> /// <summary>
/// Removes from playlist. /// Removes from playlist.

View File

@@ -83,7 +83,6 @@ namespace MediaBrowser.MediaEncoding.Probing
"Smith/Kotzen", "Smith/Kotzen",
"We;Na", "We;Na",
"LSR/CITY", "LSR/CITY",
"Kairon; IRSE!",
}; };
/// <summary> /// <summary>

View File

@@ -1,3 +1,4 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@@ -18,7 +19,7 @@ namespace MediaBrowser.Model.Providers
/// Gets or sets the name. /// Gets or sets the name.
/// </summary> /// </summary>
/// <value>The name.</value> /// <value>The name.</value>
public string? Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// Gets or sets the provider ids. /// Gets or sets the provider ids.
@@ -40,13 +41,13 @@ namespace MediaBrowser.Model.Providers
public DateTime? PremiereDate { get; set; } public DateTime? PremiereDate { get; set; }
public string? ImageUrl { get; set; } public string ImageUrl { get; set; }
public string? SearchProviderName { get; set; } public string SearchProviderName { get; set; }
public string? Overview { get; set; } public string Overview { get; set; }
public RemoteSearchResult? AlbumArtist { get; set; } public RemoteSearchResult AlbumArtist { get; set; }
public RemoteSearchResult[] Artists { get; set; } public RemoteSearchResult[] Artists { get; set; }
} }

View File

@@ -33,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Api
/// <returns>The image portion of the TMDb client configuration.</returns> /// <returns>The image portion of the TMDb client configuration.</returns>
[HttpGet("ClientConfiguration")] [HttpGet("ClientConfiguration")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ConfigImageTypes?> TmdbClientConfiguration() public async Task<ConfigImageTypes> TmdbClientConfiguration()
{ {
return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images; return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images;
} }

View File

@@ -75,17 +75,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
var posters = collection.Images.Posters; var posters = collection.Images.Posters;
var backdrops = collection.Images.Backdrops; var backdrops = collection.Images.Backdrops;
var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0); var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
if (posters is not null) remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
{ remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
}
if (backdrops is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
}
return remoteImages; return remoteImages;
} }

View File

@@ -67,14 +67,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture)); result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
return [result]; return new[] { result };
} }
var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false); var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (collectionSearchResults is null)
{
return [];
}
var collections = new RemoteSearchResult[collectionSearchResults.Count]; var collections = new RemoteSearchResult[collectionSearchResults.Count];
for (var i = 0; i < collectionSearchResults.Count; i++) for (var i = 0; i < collectionSearchResults.Count; i++)

View File

@@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movieTmdbId <= 0) if (movieTmdbId <= 0)
{ {
return []; return Enumerable.Empty<RemoteImageInfo>();
} }
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
@@ -89,28 +89,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movie?.Images is null) if (movie?.Images is null)
{ {
return []; return Enumerable.Empty<RemoteImageInfo>();
} }
var posters = movie.Images.Posters; var posters = movie.Images.Posters;
var backdrops = movie.Images.Backdrops; var backdrops = movie.Images.Backdrops;
var logos = movie.Images.Logos; var logos = movie.Images.Logos;
var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0 + logos?.Count ?? 0); var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count);
if (posters is not null) remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
{ remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
}
if (backdrops is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
}
if (logos is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
}
return remoteImages; return remoteImages;
} }

View File

@@ -15,7 +15,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
using TMDbLib.Objects.Find; using TMDbLib.Objects.Find;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search; using TMDbLib.Objects.Search;
namespace MediaBrowser.Providers.Plugins.Tmdb.Movies namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
@@ -85,7 +84,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture)); remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
remoteResult.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId); remoteResult.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId);
return [remoteResult]; return new[] { remoteResult };
} }
} }
@@ -119,11 +118,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
.ConfigureAwait(false); .ConfigureAwait(false);
} }
if (movieResults is null)
{
return [];
}
var len = movieResults.Count; var len = movieResults.Count;
var remoteSearchResults = new RemoteSearchResult[len]; var remoteSearchResults = new RemoteSearchResult[len];
for (var i = 0; i < len; i++) for (var i = 0; i < len; i++)
@@ -164,7 +158,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (searchResults?.Count > 0) if (searchResults.Count > 0)
{ {
tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
} }
@@ -173,7 +167,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId)) if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId))
{ {
var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (movieResultFromImdbId?.MovieResults?.Count > 0) if (movieResultFromImdbId?.MovieResults.Count > 0)
{ {
tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture); tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture);
} }
@@ -199,7 +193,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
OriginalTitle = movieResult.OriginalTitle, OriginalTitle = movieResult.OriginalTitle,
Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture), Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
Tagline = movieResult.Tagline, Tagline = movieResult.Tagline,
ProductionLocations = movieResult.ProductionCountries?.Select(pc => pc.Name).ToArray() ?? Array.Empty<string>() ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray()
}; };
var metadataResult = new MetadataResult<Movie> var metadataResult = new MetadataResult<Movie>
{ {
@@ -224,14 +218,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)); var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase));
if (ourRelease?.Certification is not null) if (ourRelease is not null)
{ {
movie.OfficialRating = TmdbUtils.BuildParentalRating(info.MetadataCountryCode, ourRelease.Certification); movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification);
} }
else else
{ {
var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
if (usRelease?.Certification is not null) if (usRelease is not null)
{ {
movie.OfficialRating = usRelease.Certification; movie.OfficialRating = usRelease.Certification;
} }
@@ -248,23 +242,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var genres = movieResult.Genres; var genres = movieResult.Genres;
if (genres is not null) foreach (var genre in genres.Select(g => g.Name).Trimmed())
{ {
foreach (var genre in genres.Select(g => g.Name).Trimmed()) movie.AddGenre(genre);
{
movie.AddGenre(genre);
}
} }
if (movieResult.Keywords?.Keywords is not null) if (movieResult.Keywords?.Keywords is not null)
{ {
foreach (var keyword in movieResult.Keywords.Keywords) for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++)
{ {
var name = keyword.Name; movie.AddTag(movieResult.Keywords.Keywords[i].Name);
if (!string.IsNullOrWhiteSpace(name))
{
movie.AddTag(name);
}
} }
} }

View File

@@ -56,17 +56,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
} }
result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture)); result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds?.ImdbId); result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
return [result]; return new[] { result };
} }
} }
var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false); var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false);
if (personSearchResult is null)
{
return [];
}
var remoteSearchResults = new RemoteSearchResult[personSearchResult.Count]; var remoteSearchResults = new RemoteSearchResult[personSearchResult.Count];
for (var i = 0; i < personSearchResult.Count; i++) for (var i = 0; i < personSearchResult.Count; i++)
@@ -95,7 +91,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (personTmdbId <= 0) if (personTmdbId <= 0)
{ {
var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false); var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false);
if (personSearchResults?.Count > 0) if (personSearchResults.Count > 0)
{ {
personTmdbId = personSearchResults[0].Id; personTmdbId = personSearchResults[0].Id;
} }

View File

@@ -76,7 +76,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.Item.Name = seasonResult.Name; result.Item.Name = seasonResult.Name;
} }
result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds?.TvdbId); result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
// TODO why was this disabled? // TODO why was this disabled?
var credits = seasonResult.Credits; var credits = seasonResult.Credits;

View File

@@ -79,22 +79,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var posters = series.Images.Posters; var posters = series.Images.Posters;
var backdrops = series.Images.Backdrops; var backdrops = series.Images.Backdrops;
var logos = series.Images.Logos; var logos = series.Images.Logos;
var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0 + logos?.Count ?? 0); var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count);
if (posters is not null) remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
{ remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
}
if (backdrops is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
}
if (logos is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
}
return remoteImages; return remoteImages;
} }

View File

@@ -112,10 +112,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken: cancellationToken) var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (tvSearchResults is null)
{
return [];
}
var remoteResults = new RemoteSearchResult[tvSearchResults.Count]; var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
for (var i = 0; i < tvSearchResults.Count; i++) for (var i = 0; i < tvSearchResults.Count; i++)
@@ -145,7 +141,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
} }
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
remoteResult.ProductionYear = series.FirstAirDate?.Year;
return remoteResult; return remoteResult;
} }
@@ -162,7 +157,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture)); remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
remoteResult.ProductionYear = series.FirstAirDate?.Year;
return remoteResult; return remoteResult;
} }
@@ -180,7 +174,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId)) if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
{ {
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (searchResult?.TvResults?.Count > 0) if (searchResult?.TvResults.Count > 0)
{ {
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture); tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
} }
@@ -189,7 +183,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)) if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
{ {
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
if (searchResult?.TvResults?.Count > 0) if (searchResult?.TvResults.Count > 0)
{ {
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture); tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
} }
@@ -204,7 +198,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var cleanedName = TmdbUtils.CleanName(parsedName.Name); var cleanedName = TmdbUtils.CleanName(parsedName.Name);
var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.MetadataCountryCode, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false); var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.MetadataCountryCode, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false);
if (searchResults?.Count > 0) if (searchResults.Count > 0)
{ {
tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
} }
@@ -268,19 +262,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (seriesResult.Keywords?.Results is not null) if (seriesResult.Keywords?.Results is not null)
{ {
foreach (var result in seriesResult.Keywords.Results) for (var i = 0; i < seriesResult.Keywords.Results.Count; i++)
{ {
var name = result.Name; series.AddTag(seriesResult.Keywords.Results[i].Name);
if (!string.IsNullOrWhiteSpace(name))
{
series.AddTag(name);
}
} }
} }
series.HomePageUrl = seriesResult.Homepage; series.HomePageUrl = seriesResult.Homepage;
series.RunTimeTicks = seriesResult.EpisodeRunTime?.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
if (Emby.Naming.TV.TvParserHelpers.TryParseSeriesStatus(seriesResult.Status, out var seriesStatus)) if (Emby.Naming.TV.TvParserHelpers.TryParseSeriesStatus(seriesResult.Status, out var seriesStatus))
{ {
@@ -289,7 +279,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.EndDate = seriesResult.LastAirDate; series.EndDate = seriesResult.LastAirDate;
series.PremiereDate = seriesResult.FirstAirDate; series.PremiereDate = seriesResult.FirstAirDate;
series.ProductionYear = seriesResult.FirstAirDate?.Year;
var ids = seriesResult.ExternalIds; var ids = seriesResult.ExternalIds;
if (ids is not null) if (ids is not null)
@@ -299,21 +288,21 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId); series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
} }
var contentRatings = seriesResult.ContentRatings?.Results ?? new List<ContentRating>(); var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>();
var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase)); var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
var minimumRelease = contentRatings.FirstOrDefault(); var minimumRelease = contentRatings.FirstOrDefault();
if (ourRelease?.Rating is not null) if (ourRelease is not null)
{ {
series.OfficialRating = TmdbUtils.BuildParentalRating(preferredCountryCode, ourRelease.Rating); series.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Rating);
} }
else if (usRelease?.Rating is not null) else if (usRelease is not null)
{ {
series.OfficialRating = usRelease.Rating; series.OfficialRating = usRelease.Rating;
} }
else if (minimumRelease?.Rating is not null) else if (minimumRelease is not null)
{ {
series.OfficialRating = minimumRelease.Rating; series.OfficialRating = minimumRelease.Rating;
} }
@@ -358,8 +347,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Role = actor.Character?.Trim() ?? string.Empty, Role = actor.Character?.Trim() ?? string.Empty,
Type = PersonKind.Actor, Type = PersonKind.Actor,
SortOrder = actor.Order, SortOrder = actor.Order,
// NOTE: Null values are filtered out above ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath!)
}; };
if (actor.Id > 0) if (actor.Id > 0)
@@ -400,8 +388,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Name = crewMember.Name.Trim(), Name = crewMember.Name.Trim(),
Role = crewMember.Job?.Trim() ?? string.Empty, Role = crewMember.Job?.Trim() ?? string.Empty,
Type = entry.PersonType, Type = entry.PersonType,
// NOTE: Null values are filtered out above ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath!)
}; };
if (crewMember.Id > 0) if (crewMember.Id > 0)

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
@@ -194,7 +195,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false); await EnsureClientConfigAsync().ConfigureAwait(false);
var series = await GetSeriesAsync(tvShowId, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false); var series = await GetSeriesAsync(tvShowId, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
var episodeGroupId = series?.EpisodeGroups?.Results?.Find(g => g.Type == groupType)?.Id; var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id;
if (episodeGroupId is null) if (episodeGroupId is null)
{ {
@@ -262,7 +263,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param> /// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv episode information or null if not found.</returns> /// <returns>The TMDb tv episode information or null if not found.</returns>
public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, long episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken) public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
{ {
var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
if (_memoryCache.TryGetValue(key, out TvEpisode? episode)) if (_memoryCache.TryGetValue(key, out TvEpisode? episode))
@@ -275,9 +276,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false); var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
if (group is not null) if (group is not null)
{ {
var season = group.Groups?.Find(s => s.Order == seasonNumber); var season = group.Groups.Find(s => s.Order == seasonNumber);
// Episode order starts at 0 // Episode order starts at 0
var ep = season?.Episodes?.Find(e => e.Order == episodeNumber - 1); var ep = season?.Episodes.Find(e => e.Order == episodeNumber - 1);
if (ep is not null) if (ep is not null)
{ {
seasonNumber = ep.SeasonNumber; seasonNumber = ep.SeasonNumber;
@@ -381,7 +382,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="year">The year the tv show first aired.</param> /// <param name="year">The year the tv show first aired.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv show information.</returns> /// <returns>The TMDb tv show information.</returns>
public async Task<IReadOnlyList<SearchTv>?> SearchSeriesAsync(string name, string language, string? countryCode, int year = 0, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, string? countryCode, int year = 0, CancellationToken cancellationToken = default)
{ {
var key = $"searchseries-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}"; var key = $"searchseries-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null) if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null)
@@ -395,12 +396,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken) .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (searchResults?.Results?.Count > 0) if (searchResults.Results.Count > 0)
{ {
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
} }
return searchResults?.Results; return searchResults.Results;
} }
/// <summary> /// <summary>
@@ -409,7 +410,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="name">The name of the person.</param> /// <param name="name">The name of the person.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb person information.</returns> /// <returns>The TMDb person information.</returns>
public async Task<IReadOnlyList<SearchPerson>?> SearchPersonAsync(string name, CancellationToken cancellationToken) public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken)
{ {
var key = $"searchperson-{name}"; var key = $"searchperson-{name}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson>? person) && person is not null) if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson>? person) && person is not null)
@@ -423,12 +424,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: cancellationToken) .SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (searchResults?.Results?.Count > 0) if (searchResults.Results.Count > 0)
{ {
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
} }
return searchResults?.Results; return searchResults.Results;
} }
/// <summary> /// <summary>
@@ -438,7 +439,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="language">The movie's language.</param> /// <param name="language">The movie's language.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb movie information.</returns> /// <returns>The TMDb movie information.</returns>
public Task<IReadOnlyList<SearchMovie>?> SearchMovieAsync(string name, string language, CancellationToken cancellationToken) public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
{ {
return SearchMovieAsync(name, 0, language, null, cancellationToken); return SearchMovieAsync(name, 0, language, null, cancellationToken);
} }
@@ -452,7 +453,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param> /// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb movie information.</returns> /// <returns>The TMDb movie information.</returns>
public async Task<IReadOnlyList<SearchMovie>?> SearchMovieAsync(string name, int year, string language, string? countryCode, CancellationToken cancellationToken) public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, string? countryCode, CancellationToken cancellationToken)
{ {
var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}"; var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null) if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null)
@@ -466,12 +467,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken) .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (searchResults?.Results?.Count > 0) if (searchResults.Results.Count > 0)
{ {
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
} }
return searchResults?.Results; return searchResults.Results;
} }
/// <summary> /// <summary>
@@ -482,7 +483,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param> /// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb collection information.</returns> /// <returns>The TMDb collection information.</returns>
public async Task<IReadOnlyList<SearchCollection>?> SearchCollectionAsync(string name, string language, string? countryCode, CancellationToken cancellationToken) public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, string? countryCode, CancellationToken cancellationToken)
{ {
var key = $"collectionsearch-{name}-{language}"; var key = $"collectionsearch-{name}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not null) if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not null)
@@ -496,12 +497,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), cancellationToken: cancellationToken) .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (searchResults?.Results?.Count > 0) if (searchResults.Results.Count > 0)
{ {
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
} }
return searchResults?.Results; return searchResults.Results;
} }
/// <summary> /// <summary>
@@ -510,17 +511,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="size">The image size to fetch.</param> /// <param name="size">The image size to fetch.</param>
/// <param name="path">The relative URL of the image.</param> /// <param name="path">The relative URL of the image.</param>
/// <returns>The absolute URL.</returns> /// <returns>The absolute URL.</returns>
private string? GetUrl(string? size, string? path) private string? GetUrl(string? size, string path)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
return null; return null;
} }
// Use "original" as default size if size is null or empty to prevent malformed URLs return _tmDbClient.GetImageUrl(size, path, true).ToString();
var imageSize = string.IsNullOrEmpty(size) ? "original" : size;
return _tmDbClient.GetImageUrl(imageSize, path, true).ToString();
} }
/// <summary> /// <summary>
@@ -528,7 +526,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary> /// </summary>
/// <param name="posterPath">The relative URL of the poster.</param> /// <param name="posterPath">The relative URL of the poster.</param>
/// <returns>The absolute URL.</returns> /// <returns>The absolute URL.</returns>
public string? GetPosterUrl(string? posterPath) public string? GetPosterUrl(string posterPath)
{ {
return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath); return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
} }
@@ -538,7 +536,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary> /// </summary>
/// <param name="actorProfilePath">The relative URL of the profile image.</param> /// <param name="actorProfilePath">The relative URL of the profile image.</param>
/// <returns>The absolute URL.</returns> /// <returns>The absolute URL.</returns>
public string? GetProfileUrl(string? actorProfilePath) public string? GetProfileUrl(string actorProfilePath)
{ {
return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath); return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
} }
@@ -641,44 +639,30 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
private static void ValidatePreferences(TMDbConfig config) private static void ValidatePreferences(TMDbConfig config)
{ {
var imageConfig = config.Images; var imageConfig = config.Images;
if (imageConfig is null)
{
return;
}
var pluginConfig = Plugin.Instance.Configuration; var pluginConfig = Plugin.Instance.Configuration;
if (imageConfig.PosterSizes is not null if (!imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
&& pluginConfig.PosterSize is not null
&& !imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
{ {
pluginConfig.PosterSize = imageConfig.PosterSizes[^1]; pluginConfig.PosterSize = imageConfig.PosterSizes[^1];
} }
if (imageConfig.BackdropSizes is not null if (!imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
&& pluginConfig.BackdropSize is not null
&& !imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
{ {
pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1]; pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1];
} }
if (imageConfig.LogoSizes is not null if (!imageConfig.LogoSizes.Contains(pluginConfig.LogoSize))
&& pluginConfig.LogoSize is not null
&& !imageConfig.LogoSizes.Contains(pluginConfig.LogoSize))
{ {
pluginConfig.LogoSize = imageConfig.LogoSizes[^1]; pluginConfig.LogoSize = imageConfig.LogoSizes[^1];
} }
if (imageConfig.ProfileSizes is not null if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
&& pluginConfig.ProfileSize is not null
&& !imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
{ {
pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1]; pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1];
} }
if (imageConfig.StillSizes is not null if (!imageConfig.StillSizes.Contains(pluginConfig.StillSize))
&& pluginConfig.StillSize is not null
&& !imageConfig.StillSizes.Contains(pluginConfig.StillSize))
{ {
pluginConfig.StillSize = imageConfig.StillSizes[^1]; pluginConfig.StillSize = imageConfig.StillSizes[^1];
} }

View File

@@ -69,20 +69,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>The Jellyfin person type.</returns> /// <returns>The Jellyfin person type.</returns>
public static PersonKind MapCrewToPersonType(Crew crew) public static PersonKind MapCrewToPersonType(Crew crew)
{ {
if (string.Equals(crew.Department, "directing", StringComparison.OrdinalIgnoreCase) if (crew.Department.Equals("directing", StringComparison.OrdinalIgnoreCase)
&& string.Equals(crew.Job, "director", StringComparison.OrdinalIgnoreCase)) && crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase))
{ {
return PersonKind.Director; return PersonKind.Director;
} }
if (string.Equals(crew.Department, "production", StringComparison.OrdinalIgnoreCase) if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
&& string.Equals(crew.Job, "producer", StringComparison.OrdinalIgnoreCase)) && crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase))
{ {
return PersonKind.Producer; return PersonKind.Producer;
} }
if (string.Equals(crew.Department, "writing", StringComparison.OrdinalIgnoreCase) if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(crew.Job, "writer", StringComparison.OrdinalIgnoreCase) || string.Equals(crew.Job, "screenplay", StringComparison.OrdinalIgnoreCase))) && (crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase) || crew.Job.Equals("screenplay", StringComparison.OrdinalIgnoreCase)))
{ {
return PersonKind.Writer; return PersonKind.Writer;
} }
@@ -97,9 +97,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>A boolean indicating whether the video is a trailer.</returns> /// <returns>A boolean indicating whether the video is a trailer.</returns>
public static bool IsTrailerType(Video video) public static bool IsTrailerType(Video video)
{ {
return string.Equals(video.Site, "youtube", StringComparison.OrdinalIgnoreCase) return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(video.Type, "trailer", StringComparison.OrdinalIgnoreCase) && (video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
|| string.Equals(video.Type, "teaser", StringComparison.OrdinalIgnoreCase)); || video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase));
} }
/// <summary> /// <summary>
@@ -117,6 +117,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode); preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode);
languages.Add(preferredLanguage); languages.Add(preferredLanguage);
if (preferredLanguage.Length == 5) // Like en-US
{
// Currently, TMDb supports 2-letter language codes only.
// They are planning to change this in the future, thus we're
// supplying both codes if we're having a 5-letter code.
languages.Add(preferredLanguage.Substring(0, 2));
}
} }
languages.Add("null"); languages.Add("null");
@@ -177,14 +185,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguage">The image's actual language code.</param> /// <param name="imageLanguage">The image's actual language code.</param>
/// <param name="requestLanguage">The requested language code.</param> /// <param name="requestLanguage">The requested language code.</param>
/// <returns>The language code.</returns> /// <returns>The language code.</returns>
public static string AdjustImageLanguage(string? imageLanguage, string requestLanguage) public static string AdjustImageLanguage(string imageLanguage, string requestLanguage)
{ {
if (string.IsNullOrEmpty(imageLanguage)) if (!string.IsNullOrEmpty(imageLanguage)
{ && !string.IsNullOrEmpty(requestLanguage)
return string.Empty;
}
if (!string.IsNullOrEmpty(requestLanguage)
&& requestLanguage.Length > 2 && requestLanguage.Length > 2
&& imageLanguage.Length == 2 && imageLanguage.Length == 2
&& requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase)) && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))

View File

@@ -4,7 +4,6 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@@ -202,26 +201,6 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
false); false);
} }
private static bool NeedsVirtualSeason(Episode episode, HashSet<Guid> physicalSeasonIds, HashSet<string> physicalSeasonPaths)
{
// Episode has a known season number, needs a season
if (episode.ParentIndexNumber.HasValue)
{
return true;
}
// Not yet processed
if (episode.SeasonId.IsEmpty())
{
return false;
}
// Episode has been processed, only needs a virtual season if it isn't
// already linked to a known physical season by ID or path
return !physicalSeasonIds.Contains(episode.SeasonId)
&& !physicalSeasonPaths.Contains(System.IO.Path.GetDirectoryName(episode.Path) ?? string.Empty);
}
/// <summary> /// <summary>
/// Creates seasons for all episodes if they don't exist. /// Creates seasons for all episodes if they don't exist.
/// If no season number can be determined, a dummy season will be created. /// If no season number can be determined, a dummy season will be created.
@@ -233,20 +212,8 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
{ {
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
var seasons = seriesChildren.OfType<Season>().ToList(); var seasons = seriesChildren.OfType<Season>().ToList();
var physicalSeasonIds = seasons
.Where(e => e.LocationType != LocationType.Virtual)
.Select(e => e.Id)
.ToHashSet();
var physicalSeasonPathSet = seasons
.Where(e => e.LocationType != LocationType.Virtual && !string.IsNullOrEmpty(e.Path))
.Select(e => e.Path)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var uniqueSeasonNumbers = seriesChildren var uniqueSeasonNumbers = seriesChildren
.OfType<Episode>() .OfType<Episode>()
.Where(e => NeedsVirtualSeason(e, physicalSeasonIds, physicalSeasonPathSet))
.Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null) .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
.Distinct(); .Distinct();

View File

@@ -94,12 +94,13 @@ git clone https://github.com/jellyfin/jellyfin.git
The server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend by default. Before you can run the server, you will need to get a copy of the web client since they are not included in this repository directly. The server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend by default. Before you can run the server, you will need to get a copy of the web client since they are not included in this repository directly.
Note that it is recommended for development to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step. Note that it is also possible to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step.
There are two options to get the files for the web client. There are three options to get the files for the web client.
1. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web) 1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27&_a=summary&repositoryFilter=6&view=branches) of the pipelines page.
2. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web` 2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
### Running The Server ### Running The Server

View File

@@ -131,7 +131,7 @@ namespace Jellyfin.Extensions
/// </summary> /// </summary>
/// <param name="values">The enumerable of strings to trim.</param> /// <param name="values">The enumerable of strings to trim.</param>
/// <returns>The enumeration of trimmed strings.</returns> /// <returns>The enumeration of trimmed strings.</returns>
public static IEnumerable<string> Trimmed(this IEnumerable<string?> values) public static IEnumerable<string> Trimmed(this IEnumerable<string> values)
{ {
return values.Select(i => (i ?? string.Empty).Trim()); return values.Select(i => (i ?? string.Empty).Trim());
} }

View File

@@ -11,29 +11,21 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")] [InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid=tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid-tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)] [InlineData("Superman: Red Son", "imdbid", null)]
[InlineData("Superman: Red Son", "something", null)]
[InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid1=tt11111111}(imdbid=tt10985510)", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [imdbid1-tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid1-tt11111111)[imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355]{imdbid-tt10985510}", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (tmdbid-618355)[imdbid-tt10985510]", "tmdbid", "618355")] [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")] [InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
[InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")] [InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
[InlineData("Superman: Red Son [providera id=4]", "providera id", "4")] [InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
[InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")] [InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
[InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")] [InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
[InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")] [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
[InlineData("Superman: Red Son {tmdbid=3}", "tmdbid", "3")]
[InlineData("Superman: Red Son (tvdbid-6)", "tvdbid", "6")]
[InlineData("[tmdbid=618355]", "tmdbid", "618355")] [InlineData("[tmdbid=618355]", "tmdbid", "618355")]
[InlineData("{tmdbid=618355}", "tmdbid", "618355")]
[InlineData("(tmdbid=618355)", "tmdbid", "618355")]
[InlineData("[tmdbid-618355]", "tmdbid", "618355")] [InlineData("[tmdbid-618355]", "tmdbid", "618355")]
[InlineData("{tmdbid-618355)", "tmdbid", null)]
[InlineData("[tmdbid-618355}", "tmdbid", null)]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")] [InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")] [InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
[InlineData("tmdbid=618355]", "tmdbid", null)] [InlineData("tmdbid=618355]", "tmdbid", null)]
@@ -44,9 +36,6 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)] [InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)]
[InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)] [InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)]
[InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")] [InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")]
[InlineData("{tmdbid=}{imdbid=tt10985510}", "tmdbid", null)]
[InlineData("(tmdbid-)(imdbid-tt10985510)", "tmdbid", null)]
[InlineData("Superman: Red Son {tmdbid-618355}{tmdbid=1234567}", "tmdbid", "618355")]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult) public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{ {
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute)); Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));