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,
"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,27 +26,27 @@
<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.631" />
@@ -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

@@ -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.
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)
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`
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. 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

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));