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
48 changed files with 360 additions and 551 deletions

View File

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

View File

@@ -1,31 +1,17 @@
{
"name": "Development Jellyfin Server",
"image": "mcr.microsoft.com/devcontainers/dotnet:10.0-noble",
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// 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\"",
// The previous way of installing extensions via the vs command dont work on selfhosted devcontainers
"customizations": {
"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"
]
}
},
// reads the extensions list and installs them
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
"dotnetRuntimeVersions": "10.0",
"aspNetCoreRuntimeVersions": "10.0"
"dotnetRuntimeVersions": "9.0",
"aspNetCoreRuntimeVersions": "9.0"
},
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"preserve_apt_list": false,

View File

@@ -28,13 +28,13 @@ jobs:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
- 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:
pull_request:
workflow_run:
workflows: ["ABI Compatibility Build"]
types: [completed]
permissions: {}
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:
permissions:
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
pull-requests: write
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
needs:
- abi-head
- abi-base
steps:
- name: Download abi-head
@@ -89,12 +22,16 @@ jobs:
with:
name: abi-head
path: abi-head
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download abi-base
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: abi-base
path: abi-base
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup ApiCompat
run: |
@@ -118,7 +55,7 @@ jobs:
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
issue-number: ${{ github.event.workflow_run.pull_requests[0].number }}
direction: last
body-includes: abi-diff-workflow-comment
@@ -126,7 +63,7 @@ jobs:
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.diff.outputs.body != '' }}
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 }}
edit-mode: replace
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -145,7 +82,7 @@ jobs:
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
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 }}
edit-mode: replace
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
on:
push:
branches:
- master
tags:
- 'v*'
pull_request:
workflow_run:
workflows: ["OpenAPI Build"]
types: [completed]
permissions: {}
jobs:
openapi-head:
name: OpenAPI - HEAD
if: ${{ github.event_name == 'push' }}
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: Generate openapi.json
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
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:
permissions:
pull-requests: write
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
needs:
- openapi-head
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: openapi-head
path: openapi-head
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download openapi-base
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: openapi-base
path: openapi-base
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Detect OpenAPI changes
id: openapi-diff
@@ -105,11 +70,12 @@ jobs:
markdown: openapi-changelog.md
add-pr-comment: true
github-token: ${{ secrets.GITHUB_TOKEN }}
pr-number: ${{ github.event.workflow_run.pull_requests[0].number }}
publish-unstable:
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
needs:
- openapi-head
@@ -170,7 +136,7 @@ jobs:
publish-stable:
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
needs:
- openapi-head

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ on:
push:
branches:
- master
pull_request:
pull_request_target:
issue_comment:
permissions: {}
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Apply label
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:
dirtyLabel: 'merge conflict'
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
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true

View File

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

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="8.0.2" />
<PackageVersion Include="AsyncKeyedLock" Version="8.0.0" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" 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" Version="1.4.0-pre.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="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
@@ -26,30 +26,30 @@
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" 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.Data.Sqlite" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="1.1.0.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
@@ -74,13 +74,13 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<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" 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="z440.atl.core" Version="7.11.0" />
<PackageVersion Include="TMDbLib" Version="3.0.0" />
<PackageVersion Include="z440.atl.core" Version="7.10.0" />
<PackageVersion Include="TMDbLib" Version="2.3.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<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="enumValue">The <see cref="SeriesStatus"/>.</param>
/// <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))
{

View File

@@ -267,24 +267,22 @@ namespace Emby.Server.Implementations.Images
{
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)
{
return false;
}
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
{
return false;
}
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
{
return false;
}
if (!HasChangedByDate(item, image))
{
return false;
if (!HasChangedByDate(item, image))
{
return false;
}
}
return true;

View File

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

View File

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

View File

@@ -104,7 +104,7 @@
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja dels registres",
"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.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
"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}",
"Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}",
"Collections": "Zbirke",
"Collections": "Kolekcije",
"DeviceOfflineWithName": "{0} je prekinuo vezu",
"DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto",
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
"Shows": "Emisije",
"Shows": "Serije",
"Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",

View File

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

View File

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

View File

@@ -124,8 +124,8 @@
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
"External": "Externo",
"HearingImpaired": "Surdo",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskRefreshTrickplayImages": "Gerar Imagens de Trickplay",
"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.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",

View File

@@ -124,8 +124,8 @@
"HearingImpaired": "Problemas auditivos",
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
"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.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"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);
}
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);
return AddToPlaylistInternal(
playlistId,
itemIds,
user,
new DtoOptions(false)
{
EnableImages = true
},
position);
return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
{
EnableImages = true
});
}
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
var playlist = _libraryManager.GetItemById(playlistId) as Playlist
@@ -248,30 +243,7 @@ namespace Emby.Server.Implementations.Playlists
}
// Update the playlist in the repository
if (position.HasValue)
{
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.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
playlist.DateLastMediaAdded = DateTime.UtcNow;
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)))
{
var viewType = displayPreferences.CustomPrefs[key];
if (string.IsNullOrEmpty(viewType))
if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out _))
{
displayPreferences.CustomPrefs.Remove(key);
continue;
}
if (!Enum.TryParse<ViewType>(viewType, true, out _))
{
_logger.LogError("Invalid ViewType: {LandingScreenOption}", viewType);
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
displayPreferences.CustomPrefs.Remove(key);
}
}

View File

@@ -359,7 +359,6 @@ public class PlaylistsController : BaseJellyfinApiController
/// </summary>
/// <param name="playlistId">The playlist id.</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>
/// <response code="204">Items added to playlist.</response>
/// <response code="403">Access forbidden.</response>
@@ -372,7 +371,6 @@ public class PlaylistsController : BaseJellyfinApiController
public async Task<ActionResult> AddItemToPlaylist(
[FromRoute, Required] Guid playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
[FromQuery] int? position,
[FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
@@ -390,7 +388,7 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, position, userId.Value).ConfigureAwait(false);
await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false);
return NoContent();
}

View File

@@ -74,10 +74,9 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
/// <inheritdoc />
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();
person.Role = person.Role?.Trim() ?? string.Empty;
item.Role = string.Empty;
}
// multiple metadata providers can provide the _same_ person

View File

@@ -1163,9 +1163,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
Item = null!,
ProviderId = e[0],
ProviderValue = string.Join('|', e.Skip(1))
})
.DistinctBy(e => e.ProviderId)
.ToArray();
}).ToArray();
}
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
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
var shouldRemove = !IsRoot || allowRemoveRoot;
var actuallyRemoved = new List<BaseItem>();
// If it's an AggregateFolder, don't remove
if (shouldRemove && itemsRemoved.Count > 0)
{
@@ -468,7 +467,6 @@ namespace MediaBrowser.Controller.Entities
{
Logger.LogDebug("Removed item: {Path}", item.Path);
actuallyRemoved.Add(item);
item.SetParent(null);
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
}
@@ -479,20 +477,6 @@ namespace MediaBrowser.Controller.Entities
{
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
{

View File

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

View File

@@ -61,10 +61,9 @@ namespace MediaBrowser.Controller.Playlists
/// </summary>
/// <param name="playlistId">The playlist identifier.</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>
/// <returns>Task.</returns>
Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, int? position, Guid userId);
Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
/// <summary>
/// Removes from playlist.

View File

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

View File

@@ -1,3 +1,4 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -18,7 +19,7 @@ namespace MediaBrowser.Model.Providers
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string? Name { get; set; }
public string Name { get; set; }
/// <summary>
/// Gets or sets the provider ids.
@@ -40,13 +41,13 @@ namespace MediaBrowser.Model.Providers
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; }
}

View File

@@ -33,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Api
/// <returns>The image portion of the TMDb client configuration.</returns>
[HttpGet("ClientConfiguration")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ConfigImageTypes?> TmdbClientConfiguration()
public async Task<ConfigImageTypes> TmdbClientConfiguration()
{
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 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));
}
if (backdrops is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
}
remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
return remoteImages;
}

View File

@@ -67,14 +67,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
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);
if (collectionSearchResults is null)
{
return [];
}
var collections = new RemoteSearchResult[collectionSearchResults.Count];
for (var i = 0; i < collectionSearchResults.Count; i++)

View File

@@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
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
@@ -89,28 +89,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movie?.Images is null)
{
return [];
return Enumerable.Empty<RemoteImageInfo>();
}
var posters = movie.Images.Posters;
var backdrops = movie.Images.Backdrops;
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));
}
if (backdrops is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
}
if (logos is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
}
remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
return remoteImages;
}

View File

@@ -15,7 +15,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using TMDbLib.Objects.Find;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
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.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId);
return [remoteResult];
return new[] { remoteResult };
}
}
@@ -119,11 +118,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
.ConfigureAwait(false);
}
if (movieResults is null)
{
return [];
}
var len = movieResults.Count;
var remoteSearchResults = new RemoteSearchResult[len];
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);
if (searchResults?.Count > 0)
if (searchResults.Count > 0)
{
tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
@@ -173,7 +167,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId))
{
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);
}
@@ -199,7 +193,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
OriginalTitle = movieResult.OriginalTitle,
Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
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>
{
@@ -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));
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
{
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;
}
@@ -248,23 +242,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
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)
{
foreach (var keyword in movieResult.Keywords.Keywords)
for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++)
{
var name = keyword.Name;
if (!string.IsNullOrWhiteSpace(name))
{
movie.AddTag(name);
}
movie.AddTag(movieResult.Keywords.Keywords[i].Name);
}
}

View File

@@ -56,17 +56,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
}
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);
if (personSearchResult is null)
{
return [];
}
var remoteSearchResults = new RemoteSearchResult[personSearchResult.Count];
for (var i = 0; i < personSearchResult.Count; i++)
@@ -95,7 +91,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (personTmdbId <= 0)
{
var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false);
if (personSearchResults?.Count > 0)
if (personSearchResults.Count > 0)
{
personTmdbId = personSearchResults[0].Id;
}

View File

@@ -76,7 +76,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
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?
var credits = seasonResult.Credits;

View File

@@ -79,22 +79,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var posters = series.Images.Posters;
var backdrops = series.Images.Backdrops;
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));
}
if (backdrops is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
}
if (logos is not null)
{
remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
}
remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
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)
.ConfigureAwait(false);
if (tvSearchResults is null)
{
return [];
}
var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
for (var i = 0; i < tvSearchResults.Count; i++)
@@ -145,7 +141,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
remoteResult.ProductionYear = series.FirstAirDate?.Year;
return remoteResult;
}
@@ -162,7 +157,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
remoteResult.ProductionYear = series.FirstAirDate?.Year;
return remoteResult;
}
@@ -180,7 +174,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
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);
if (searchResult?.TvResults?.Count > 0)
if (searchResult?.TvResults.Count > 0)
{
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))
{
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);
}
@@ -204,7 +198,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
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);
}
@@ -268,19 +262,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
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;
if (!string.IsNullOrWhiteSpace(name))
{
series.AddTag(name);
}
series.AddTag(seriesResult.Keywords.Results[i].Name);
}
}
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))
{
@@ -289,7 +279,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.EndDate = seriesResult.LastAirDate;
series.PremiereDate = seriesResult.FirstAirDate;
series.ProductionYear = seriesResult.FirstAirDate?.Year;
var ids = seriesResult.ExternalIds;
if (ids is not null)
@@ -299,21 +288,21 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
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 usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
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;
}
else if (minimumRelease?.Rating is not null)
else if (minimumRelease is not null)
{
series.OfficialRating = minimumRelease.Rating;
}
@@ -358,8 +347,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Role = actor.Character?.Trim() ?? string.Empty,
Type = PersonKind.Actor,
SortOrder = actor.Order,
// NOTE: Null values are filtered out above
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath!)
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
};
if (actor.Id > 0)
@@ -400,8 +388,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Name = crewMember.Name.Trim(),
Role = crewMember.Job?.Trim() ?? string.Empty,
Type = entry.PersonType,
// NOTE: Null values are filtered out above
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath!)
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
};
if (crewMember.Id > 0)

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -194,7 +195,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().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)
{
@@ -262,7 +263,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <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}";
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);
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
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)
{
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="cancellationToken">The cancellation token.</param>
/// <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}";
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)
.ConfigureAwait(false);
if (searchResults?.Results?.Count > 0)
if (searchResults.Results.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
return searchResults?.Results;
return searchResults.Results;
}
/// <summary>
@@ -409,7 +410,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="name">The name of the person.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <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}";
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)
.ConfigureAwait(false);
if (searchResults?.Results?.Count > 0)
if (searchResults.Results.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
return searchResults?.Results;
return searchResults.Results;
}
/// <summary>
@@ -438,7 +439,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="language">The movie's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <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);
}
@@ -452,7 +453,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <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}";
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)
.ConfigureAwait(false);
if (searchResults?.Results?.Count > 0)
if (searchResults.Results.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
return searchResults?.Results;
return searchResults.Results;
}
/// <summary>
@@ -482,7 +483,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <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}";
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)
.ConfigureAwait(false);
if (searchResults?.Results?.Count > 0)
if (searchResults.Results.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
return searchResults?.Results;
return searchResults.Results;
}
/// <summary>
@@ -510,17 +511,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="size">The image size to fetch.</param>
/// <param name="path">The relative URL of the image.</param>
/// <returns>The absolute URL.</returns>
private string? GetUrl(string? size, string? path)
private string? GetUrl(string? size, string path)
{
if (string.IsNullOrEmpty(path))
{
return null;
}
// Use "original" as default size if size is null or empty to prevent malformed URLs
var imageSize = string.IsNullOrEmpty(size) ? "original" : size;
return _tmDbClient.GetImageUrl(imageSize, path, true).ToString();
return _tmDbClient.GetImageUrl(size, path, true).ToString();
}
/// <summary>
@@ -528,7 +526,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="posterPath">The relative URL of the poster.</param>
/// <returns>The absolute URL.</returns>
public string? GetPosterUrl(string? posterPath)
public string? GetPosterUrl(string posterPath)
{
return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
}
@@ -538,7 +536,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="actorProfilePath">The relative URL of the profile image.</param>
/// <returns>The absolute URL.</returns>
public string? GetProfileUrl(string? actorProfilePath)
public string? GetProfileUrl(string actorProfilePath)
{
return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
}
@@ -641,44 +639,30 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
private static void ValidatePreferences(TMDbConfig config)
{
var imageConfig = config.Images;
if (imageConfig is null)
{
return;
}
var pluginConfig = Plugin.Instance.Configuration;
if (imageConfig.PosterSizes is not null
&& pluginConfig.PosterSize is not null
&& !imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
if (!imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
{
pluginConfig.PosterSize = imageConfig.PosterSizes[^1];
}
if (imageConfig.BackdropSizes is not null
&& pluginConfig.BackdropSize is not null
&& !imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
if (!imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
{
pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1];
}
if (imageConfig.LogoSizes is not null
&& pluginConfig.LogoSize is not null
&& !imageConfig.LogoSizes.Contains(pluginConfig.LogoSize))
if (!imageConfig.LogoSizes.Contains(pluginConfig.LogoSize))
{
pluginConfig.LogoSize = imageConfig.LogoSizes[^1];
}
if (imageConfig.ProfileSizes is not null
&& pluginConfig.ProfileSize is not null
&& !imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
{
pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1];
}
if (imageConfig.StillSizes is not null
&& pluginConfig.StillSize is not null
&& !imageConfig.StillSizes.Contains(pluginConfig.StillSize))
if (!imageConfig.StillSizes.Contains(pluginConfig.StillSize))
{
pluginConfig.StillSize = imageConfig.StillSizes[^1];
}

View File

@@ -69,20 +69,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>The Jellyfin person type.</returns>
public static PersonKind MapCrewToPersonType(Crew crew)
{
if (string.Equals(crew.Department, "directing", StringComparison.OrdinalIgnoreCase)
&& string.Equals(crew.Job, "director", StringComparison.OrdinalIgnoreCase))
if (crew.Department.Equals("directing", StringComparison.OrdinalIgnoreCase)
&& crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Director;
}
if (string.Equals(crew.Department, "production", StringComparison.OrdinalIgnoreCase)
&& string.Equals(crew.Job, "producer", StringComparison.OrdinalIgnoreCase))
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
&& crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Producer;
}
if (string.Equals(crew.Department, "writing", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(crew.Job, "writer", StringComparison.OrdinalIgnoreCase) || string.Equals(crew.Job, "screenplay", StringComparison.OrdinalIgnoreCase)))
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)
&& (crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase) || crew.Job.Equals("screenplay", StringComparison.OrdinalIgnoreCase)))
{
return PersonKind.Writer;
}
@@ -97,9 +97,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>A boolean indicating whether the video is a trailer.</returns>
public static bool IsTrailerType(Video video)
{
return string.Equals(video.Site, "youtube", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(video.Type, "trailer", StringComparison.OrdinalIgnoreCase)
|| string.Equals(video.Type, "teaser", StringComparison.OrdinalIgnoreCase));
return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase)
&& (video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
|| video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase));
}
/// <summary>
@@ -117,6 +117,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode);
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");
@@ -177,14 +185,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguage">The image's actual language code.</param>
/// <param name="requestLanguage">The requested language code.</param>
/// <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))
{
return string.Empty;
}
if (!string.IsNullOrEmpty(requestLanguage)
if (!string.IsNullOrEmpty(imageLanguage)
&& !string.IsNullOrEmpty(requestLanguage)
&& requestLanguage.Length > 2
&& imageLanguage.Length == 2
&& requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))

View File

@@ -4,7 +4,6 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -202,26 +201,6 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
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>
/// Creates seasons for all episodes if they don't exist.
/// 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 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
.OfType<Episode>()
.Where(e => NeedsVirtualSeason(e, physicalSeasonIds, physicalSeasonPathSet))
.Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
.Distinct();

View File

@@ -131,7 +131,7 @@ namespace Jellyfin.Extensions
/// </summary>
/// <param name="values">The enumerable of strings to trim.</param>
/// <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());
}

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 - 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", "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 [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]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
[InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
[InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
[InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
[InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
[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", null)]
[InlineData("[tmdbid-618355}", "tmdbid", null)]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
[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("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)
{
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));