mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-03-29 14:31:53 +01:00
Compare commits
1 Commits
master
...
fix/ci-wor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d0b84ce48 |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.5",
|
||||
"version": "10.0.2",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
8
.github/workflows/ci-codeql-analysis.yml
vendored
8
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -23,18 +23,18 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
|
||||
55
.github/workflows/ci-compat-build.yml
vendored
Normal file
55
.github/workflows/ci-compat-build.yml
vendored
Normal 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/
|
||||
95
.github/workflows/ci-compat.yml
vendored
95
.github/workflows/ci-compat.yml
vendored
@@ -1,100 +1,37 @@
|
||||
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@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
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
55
.github/workflows/ci-openapi-build.yml
vendored
Normal 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
|
||||
74
.github/workflows/ci-openapi.yml
vendored
74
.github/workflows/ci-openapi.yml
vendored
@@ -1,100 +1,65 @@
|
||||
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@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
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@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
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
|
||||
@@ -119,7 +85,7 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: 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
|
||||
@@ -180,7 +146,7 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
|
||||
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
with:
|
||||
dotnet-version: ${{ env.SDK_VERSION }}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@cf6fe1b38ed5becc89ffe056c1f240825993be5b # v5.5.4
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # v5.5.1
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
2
.github/workflows/commands.yml
vendored
2
.github/workflows/commands.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
types:
|
||||
- created
|
||||
- edited
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
- synchronize
|
||||
|
||||
2
.github/workflows/issue-stale.yml
vendored
2
.github/workflows/issue-stale.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/project-automation.yml
vendored
2
.github/workflows/project-automation.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
issue_comment:
|
||||
|
||||
permissions: {}
|
||||
|
||||
4
.github/workflows/pull-request-conflict.yml
vendored
4
.github/workflows/pull-request-conflict.yml
vendored
@@ -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.'
|
||||
|
||||
2
.github/workflows/pull-request-stale.yaml
vendored
2
.github/workflows/pull-request-stale.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release-bump-version.yaml
vendored
2
.github/workflows/release-bump-version.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
timeoutSeconds: 3600
|
||||
|
||||
- name: Setup YQ
|
||||
uses: chrisdickinson/setup-yq@fa3192edd79d6eb0e4e12de8dde3a0c26f2b853b # latest
|
||||
uses: chrisdickinson/setup-yq@latest
|
||||
with:
|
||||
yq-version: v4.9.8
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.1" />
|
||||
<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,28 +26,28 @@
|
||||
<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.5" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.5" />
|
||||
<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.3.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.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.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" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
@@ -57,7 +57,7 @@
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="Polly" Version="8.6.6" />
|
||||
<PackageVersion Include="Polly" Version="8.6.5" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||
@@ -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="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.5" />
|
||||
<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.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" />
|
||||
|
||||
@@ -152,8 +152,8 @@ namespace Emby.Naming.Common
|
||||
|
||||
CleanStrings =
|
||||
[
|
||||
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS)(?=[ _\,\.\(\)\[\]\-]|$)",
|
||||
@"^\s*(?<cleaned>.+?)((\s*\[[^\]]+\]\s*)+)(\.[^\s]+)?$",
|
||||
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
||||
@"^(?<cleaned>.+?)(\[.*\])",
|
||||
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
||||
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
||||
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
|
||||
@@ -225,7 +225,6 @@ namespace Emby.Naming.Common
|
||||
".afc",
|
||||
".amf",
|
||||
".aif",
|
||||
".aifc",
|
||||
".aiff",
|
||||
".alac",
|
||||
".amr",
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Emby.Naming.Video
|
||||
var match = expression.Match(name);
|
||||
if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
|
||||
{
|
||||
newName = cleaned.Value.Trim();
|
||||
newName = cleaned.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -217,8 +217,6 @@ namespace Emby.Naming.Video
|
||||
// The CleanStringParser should have removed common keywords etc.
|
||||
return testFilename.IsEmpty
|
||||
|| testFilename[0] == '-'
|
||||
|| testFilename[0] == '_'
|
||||
|| testFilename[0] == '.'
|
||||
|| CheckMultiVersionRegex().IsMatch(testFilename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Images
|
||||
includeItemTypes = new[] { BaseItemKind.Series };
|
||||
break;
|
||||
case CollectionType.music:
|
||||
includeItemTypes = new[] { BaseItemKind.MusicArtist }; // Music albums usually don't have dedicated backdrops, so use artist instead
|
||||
includeItemTypes = new[] { BaseItemKind.MusicAlbum };
|
||||
break;
|
||||
case CollectionType.musicvideos:
|
||||
includeItemTypes = new[] { BaseItemKind.MusicVideo };
|
||||
|
||||
@@ -31,20 +31,6 @@ namespace Emby.Server.Implementations.Library
|
||||
"**/*.sample.?????",
|
||||
"**/sample/*",
|
||||
|
||||
// Avoid adding Hungarian sample files
|
||||
// https://github.com/jellyfin/jellyfin/issues/16237
|
||||
"**/minta.?",
|
||||
"**/minta.??",
|
||||
"**/minta.???", // Matches minta.mkv
|
||||
"**/minta.????", // Matches minta.webm
|
||||
"**/minta.?????",
|
||||
"**/*.minta.?",
|
||||
"**/*.minta.??",
|
||||
"**/*.minta.???",
|
||||
"**/*.minta.????",
|
||||
"**/*.minta.?????",
|
||||
"**/minta/*",
|
||||
|
||||
// Directories
|
||||
"**/metadata/**",
|
||||
"**/metadata",
|
||||
|
||||
@@ -2289,7 +2289,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
return [];
|
||||
return new List<Folder>();
|
||||
}
|
||||
|
||||
return GetCollectionFoldersInternal(item, allUserRootChildren);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
{
|
||||
public class BookResolver : ItemResolver<Book>
|
||||
{
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf", ".m4b", ".m4a", ".aac", ".flac", ".mp3", ".opus" };
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
|
||||
|
||||
protected override Book Resolve(ItemResolveArgs args)
|
||||
{
|
||||
|
||||
@@ -135,7 +135,5 @@
|
||||
"TaskExtractMediaSegments": "Media Segment Skandeer",
|
||||
"TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
|
||||
"TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
|
||||
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings.",
|
||||
"CleanupUserDataTask": "Gebruikers data skoon maak taak",
|
||||
"CleanupUserDataTaskDescription": "Maak alle gebruikers data (kykstatus, gunstelingstatus, ens.) skoon van media wat nie meer vir ten minste 90 dae teenwoordig is nie."
|
||||
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
|
||||
}
|
||||
|
||||
@@ -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": "Плагін выдалены",
|
||||
@@ -123,10 +123,10 @@
|
||||
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
|
||||
"TaskRefreshChannels": "Абнавіць каналы",
|
||||
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры",
|
||||
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа выконвацца доўга.",
|
||||
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.",
|
||||
"TaskRefreshTrickplayImages": "Стварыць выявы Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Ачысціць калекцыі і плэй-лісты",
|
||||
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.",
|
||||
"TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.",
|
||||
"TaskAudioNormalization": "Нармалізацыя гуку",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Любими",
|
||||
"Folders": "Папки",
|
||||
"Genres": "Жанрове",
|
||||
"HeaderAlbumArtists": "Изпълнители на албума",
|
||||
"HeaderAlbumArtists": "Изпълнители на албуми",
|
||||
"HeaderContinueWatching": "Продължаване на гледането",
|
||||
"HeaderFavoriteAlbums": "Любими албуми",
|
||||
"HeaderFavoriteArtists": "Любими изпълнители",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -1,27 +1 @@
|
||||
{
|
||||
"Books": "ספרים",
|
||||
"NameSeasonNumber": "עונה {0}",
|
||||
"Channels": "ערוצים",
|
||||
"Movies": "סרטים",
|
||||
"Music": "מוזיקה",
|
||||
"Collections": "אוספים",
|
||||
"Albums": "אלבומים",
|
||||
"Application": "אפליקציה",
|
||||
"Artists": "אמנים",
|
||||
"ChapterNameValue": "פרק {0}",
|
||||
"External": "חיצונית",
|
||||
"Favorites": "מועדפים",
|
||||
"Folders": "תיקיות",
|
||||
"Genres": "ז'אנרים",
|
||||
"HeaderAlbumArtists": "אמני אלבומים",
|
||||
"HeaderContinueWatching": "להמשיך לצפות",
|
||||
"HeaderFavoriteAlbums": "אלבומים אהובים",
|
||||
"HeaderFavoriteArtists": "אמנים אהובים",
|
||||
"HeaderFavoriteEpisodes": "פרקים אהובים",
|
||||
"HeaderFavoriteShows": "תוכניות אהובות",
|
||||
"HeaderFavoriteSongs": "שירים אהובים",
|
||||
"HeaderLiveTV": "טלוויזיה בשידור חי",
|
||||
"HeaderNextUp": "הבא",
|
||||
"HearingImpaired": "ללקויי שמיעה",
|
||||
"HomeVideos": "סרטונים ביתיים"
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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 dell’età 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",
|
||||
|
||||
@@ -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": "曲",
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
"Folders": "Mappen",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Albumartiesten",
|
||||
"HeaderContinueWatching": "Verder kijken",
|
||||
"HeaderContinueWatching": "Verderkijken",
|
||||
"HeaderFavoriteAlbums": "Favoriete albums",
|
||||
"HeaderFavoriteArtists": "Favoriete artiesten",
|
||||
"HeaderFavoriteEpisodes": "Favoriete afleveringen",
|
||||
"HeaderFavoriteShows": "Favoriete series",
|
||||
"HeaderFavoriteSongs": "Favoriete nummers",
|
||||
"HeaderLiveTV": "Live-tv",
|
||||
"HeaderNextUp": "Volgende",
|
||||
"HeaderNextUp": "Als volgende",
|
||||
"HeaderRecordingGroups": "Opnamegroepen",
|
||||
"HomeVideos": "Homevideo's",
|
||||
"Inherit": "Erven",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} till {1}",
|
||||
"Sync": "Synk",
|
||||
"System": "System",
|
||||
"TvShows": "Tv-serier",
|
||||
"TvShows": "TV-serier",
|
||||
"User": "Användare",
|
||||
"UserCreatedWithName": "Användaren {0} har skapats",
|
||||
"UserDeletedWithName": "Användaren {0} har tagits bort",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"Albums": "專輯",
|
||||
"AppDeviceValues": "程式:{0},裝置:{1}",
|
||||
"AppDeviceValues": "程式:{0},設備:{1}",
|
||||
"Application": "應用程式",
|
||||
"Artists": "藝人",
|
||||
"AuthenticationSucceededWithUserName": "{0} 成功通過驗證",
|
||||
"AuthenticationSucceededWithUserName": "成功授權 {0}",
|
||||
"Books": "書籍",
|
||||
"CameraImageUploadedFrom": "{0} 已經成功上傳咗一張新相",
|
||||
"CameraImageUploadedFrom": "{0} 成功上傳一張新照片",
|
||||
"Channels": "頻道",
|
||||
"ChapterNameValue": "第 {0} 章",
|
||||
"Collections": "系列",
|
||||
"DeviceOfflineWithName": "{0} 斷開咗連接",
|
||||
"DeviceOnlineWithName": "{0} 連接咗",
|
||||
"FailedLoginAttemptWithUserName": "來自 {0} 嘅登入嘗試失敗咗",
|
||||
"DeviceOfflineWithName": "{0} 已中斷連接",
|
||||
"DeviceOnlineWithName": "{0} 已連接",
|
||||
"FailedLoginAttemptWithUserName": "{0} 登入失敗",
|
||||
"Favorites": "我的最愛",
|
||||
"Folders": "資料夾",
|
||||
"Genres": "風格",
|
||||
@@ -27,15 +27,15 @@
|
||||
"HeaderRecordingGroups": "錄製組",
|
||||
"HomeVideos": "家庭影片",
|
||||
"Inherit": "繼承",
|
||||
"ItemAddedWithName": "{0} 經已加咗入媒體櫃",
|
||||
"ItemRemovedWithName": "{0} 經已由媒體櫃移除咗",
|
||||
"ItemAddedWithName": "{0} 已被加入至媒體庫",
|
||||
"ItemRemovedWithName": "{0} 已從媒體庫移除",
|
||||
"LabelIpAddressValue": "IP 地址:{0}",
|
||||
"LabelRunningTimeValue": "運行時間:{0}",
|
||||
"LabelRunningTimeValue": "運作時間:{0}",
|
||||
"Latest": "最新",
|
||||
"MessageApplicationUpdated": "Jellyfin 經已更新咗",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin 已經更新到 {0} 版本",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」經已更新咗",
|
||||
"MessageServerConfigurationUpdated": "伺服器設定經已更新咗",
|
||||
"MessageApplicationUpdated": "Jellyfin 已被更新",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin 已被更新至 {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已被更新",
|
||||
"MessageServerConfigurationUpdated": "已更新伺服器設定",
|
||||
"MixedContent": "混合內容",
|
||||
"Movies": "電影",
|
||||
"Music": "音樂",
|
||||
@@ -43,99 +43,99 @@
|
||||
"NameInstallFailed": "{0} 安裝失敗",
|
||||
"NameSeasonNumber": "第 {0} 季",
|
||||
"NameSeasonUnknown": "未知的季度",
|
||||
"NewVersionIsAvailable": "有新版本嘅 Jellyfin 可以下載。",
|
||||
"NotificationOptionApplicationUpdateAvailable": "有得更新應用程式",
|
||||
"NotificationOptionApplicationUpdateInstalled": "應用程式更新好咗",
|
||||
"NotificationOptionAudioPlayback": "開始播放音訊",
|
||||
"NotificationOptionAudioPlaybackStopped": "停咗播放音訊",
|
||||
"NotificationOptionCameraImageUploaded": "相機相片上傳咗",
|
||||
"NewVersionIsAvailable": "有新版本的 Jellyfin 可供下載。",
|
||||
"NotificationOptionApplicationUpdateAvailable": "有可用的更新",
|
||||
"NotificationOptionApplicationUpdateInstalled": "完成更新應用程式",
|
||||
"NotificationOptionAudioPlayback": "播放音訊",
|
||||
"NotificationOptionAudioPlaybackStopped": "停止播放音訊",
|
||||
"NotificationOptionCameraImageUploaded": "相片上傳",
|
||||
"NotificationOptionInstallationFailed": "安裝失敗",
|
||||
"NotificationOptionNewLibraryContent": "加咗新內容",
|
||||
"NotificationOptionPluginError": "外掛程式錯誤",
|
||||
"NotificationOptionPluginInstalled": "安裝外掛程式",
|
||||
"NotificationOptionPluginUninstalled": "解除安裝外掛程式",
|
||||
"NotificationOptionPluginUpdateInstalled": "外掛程式更新好咗",
|
||||
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
|
||||
"NotificationOptionTaskFailed": "排程工作失敗",
|
||||
"NotificationOptionUserLockedOut": "用家被鎖定咗",
|
||||
"NotificationOptionVideoPlayback": "開始播放影片",
|
||||
"NotificationOptionVideoPlaybackStopped": "停咗播放影片",
|
||||
"NotificationOptionNewLibraryContent": "新增媒體",
|
||||
"NotificationOptionPluginError": "插件錯誤",
|
||||
"NotificationOptionPluginInstalled": "安裝插件",
|
||||
"NotificationOptionPluginUninstalled": "解除安裝插件",
|
||||
"NotificationOptionPluginUpdateInstalled": "完成更新插件",
|
||||
"NotificationOptionServerRestartRequired": "伺服器需要重啟",
|
||||
"NotificationOptionTaskFailed": "排程工作執行失敗",
|
||||
"NotificationOptionUserLockedOut": "封鎖用戶",
|
||||
"NotificationOptionVideoPlayback": "播放影片",
|
||||
"NotificationOptionVideoPlaybackStopped": "停止播放影片",
|
||||
"Photos": "相片",
|
||||
"Playlists": "播放清單",
|
||||
"Plugin": "外掛程式",
|
||||
"PluginInstalledWithName": "裝好咗 {0}",
|
||||
"PluginUninstalledWithName": "剷走咗 {0}",
|
||||
"PluginUpdatedWithName": "更新好咗 {0}",
|
||||
"Plugin": "插件",
|
||||
"PluginInstalledWithName": "已安裝 {0}",
|
||||
"PluginUninstalledWithName": "已移除 {0}",
|
||||
"PluginUpdatedWithName": "已更新 {0}",
|
||||
"ProviderValue": "提供者:{0}",
|
||||
"ScheduledTaskFailedWithName": "{0} 執行失敗",
|
||||
"ScheduledTaskStartedWithName": "開始執行 {0}",
|
||||
"ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
|
||||
"ServerNameNeedsToBeRestarted": "{0} 需要重啟",
|
||||
"Shows": "節目",
|
||||
"Songs": "歌曲",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,請稍後再試。",
|
||||
"SubtitleDownloadFailureFromForItem": "經 {0} 下載 {1} 嘅字幕失敗咗",
|
||||
"StartupEmbyServerIsLoading": "正在載入 Jellyfin,請稍後再試。",
|
||||
"SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕",
|
||||
"Sync": "同步",
|
||||
"System": "系統",
|
||||
"TvShows": "電視節目",
|
||||
"User": "使用者",
|
||||
"UserCreatedWithName": "經已建立咗新使用者 {0}",
|
||||
"UserDeletedWithName": "使用者 {0} 經已被刪除",
|
||||
"UserDownloadingItemWithValues": "{0} 下載緊 {1}",
|
||||
"UserLockedOutWithName": "使用者 {0} 經已被鎖定",
|
||||
"UserOfflineFromDevice": "{0} 經已由 {1} 斷開咗連線",
|
||||
"UserOnlineFromDevice": "{0} 正喺 {1} 連線",
|
||||
"UserPasswordChangedWithName": "使用者 {0} 嘅密碼經已更改咗",
|
||||
"UserPolicyUpdatedWithName": "使用者 {0} 嘅權限經已更新咗",
|
||||
"UserStartedPlayingItemWithValues": "{0} 正喺 {2} 播緊 {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體櫃",
|
||||
"ValueSpecialEpisodeName": "特別篇 - {0}",
|
||||
"User": "用戶",
|
||||
"UserCreatedWithName": "建立新用戶 {0}",
|
||||
"UserDeletedWithName": "用戶 {0} 已被移除",
|
||||
"UserDownloadingItemWithValues": "{0} 正在下載 {1}",
|
||||
"UserLockedOutWithName": "用戶 {0} 已被封鎖",
|
||||
"UserOfflineFromDevice": "{0} 終止了 {1} 的連接",
|
||||
"UserOnlineFromDevice": "{0} 從 {1} 連線",
|
||||
"UserPasswordChangedWithName": "{0} 的密碼已被更改",
|
||||
"UserPolicyUpdatedWithName": "使用條款已更新為 {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} 在 {2} 上播放 {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} 停止在 {2} 上播放 {1}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} 已被加入至你的媒體庫",
|
||||
"ValueSpecialEpisodeName": "特典 - {0}",
|
||||
"VersionNumber": "版本 {0}",
|
||||
"TaskDownloadMissingSubtitles": "下載漏咗嘅字幕",
|
||||
"TaskUpdatePlugins": "更新外掛程式",
|
||||
"TaskDownloadMissingSubtitles": "下載欠缺字幕",
|
||||
"TaskUpdatePlugins": "更新插件",
|
||||
"TasksApplicationCategory": "應用程式",
|
||||
"TaskRefreshLibraryDescription": "掃描媒體櫃嚟搵新檔案,同時重新載入媒體詳細資料。",
|
||||
"TaskRefreshLibraryDescription": "掃描媒體庫以加入新增的檔案及重新載入元數據。",
|
||||
"TasksMaintenanceCategory": "維護",
|
||||
"TaskDownloadMissingSubtitlesDescription": "根據媒體詳細資料設定,喺網上幫你搵返啲欠缺嘅字幕。",
|
||||
"TaskRefreshChannelsDescription": "重新整理網上頻道嘅資訊。",
|
||||
"TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在網上搜尋欠缺的字幕。",
|
||||
"TaskRefreshChannelsDescription": "重新載入網絡頻道的資訊。",
|
||||
"TaskRefreshChannels": "重新載入頻道",
|
||||
"TaskCleanTranscodeDescription": "自動刪除超過一日嘅轉碼檔案。",
|
||||
"TaskCleanTranscode": "清理轉碼資料夾",
|
||||
"TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅外掛程式進行下載同安裝。",
|
||||
"TaskRefreshPeopleDescription": "更新媒體櫃入面演員同導演嘅媒體詳細資料。",
|
||||
"TaskCleanLogsDescription": "自動刪除超過 {0} 日嘅紀錄檔。",
|
||||
"TaskCleanLogs": "清理日誌資料夾",
|
||||
"TaskRefreshLibrary": "掃描媒體櫃",
|
||||
"TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節縮圖。",
|
||||
"TaskRefreshChapterImages": "擷取章節圖片",
|
||||
"TaskCleanCacheDescription": "刪除系統已經唔再需要嘅快取檔案。",
|
||||
"TaskCleanCache": "清理快取(Cache)資料夾",
|
||||
"TasksChannelsCategory": "網路頻道",
|
||||
"TasksLibraryCategory": "媒體櫃",
|
||||
"TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
|
||||
"TaskCleanTranscode": "清理轉碼檔資料夾",
|
||||
"TaskUpdatePluginsDescription": "下載並更新能夠被自動更新的插件。",
|
||||
"TaskRefreshPeopleDescription": "更新你的媒體中有關的演員和導演的元數據。",
|
||||
"TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。",
|
||||
"TaskCleanLogs": "清理紀錄檔資料夾",
|
||||
"TaskRefreshLibrary": "掃描媒體庫",
|
||||
"TaskRefreshChapterImagesDescription": "為帶有章節的影片建立縮圖。",
|
||||
"TaskRefreshChapterImages": "提取章節圖像",
|
||||
"TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。",
|
||||
"TaskCleanCache": "清理緩存資料夾",
|
||||
"TasksChannelsCategory": "網絡頻道",
|
||||
"TasksLibraryCategory": "媒體庫",
|
||||
"TaskRefreshPeople": "重新載入人物",
|
||||
"TaskCleanActivityLog": "清理活動紀錄",
|
||||
"TaskCleanActivityLog": "清理活動記錄",
|
||||
"Undefined": "未定義",
|
||||
"Forced": "強制",
|
||||
"Default": "初始",
|
||||
"TaskOptimizeDatabaseDescription": "壓縮數據庫並釋放剩餘空間。喺掃描媒體櫃或者做咗一啲會修改數據庫嘅操作之後行呢個任務,或者可以提升效能。",
|
||||
"Default": "預設",
|
||||
"TaskOptimizeDatabaseDescription": "壓縮數據庫及釋放可用空間。完成任何會修改數據庫的工作(例如掃描媒體庫)後,執行此工作或可提升伺服器速度。",
|
||||
"TaskOptimizeDatabase": "最佳化數據庫",
|
||||
"TaskCleanActivityLogDescription": "刪除超過設定日期嘅活動記錄。",
|
||||
"TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)嚟建立更準確嘅 HLS 播放列表。呢個任務可能要行好耐。",
|
||||
"TaskCleanActivityLogDescription": "刪除早於設定時間的活動記錄。",
|
||||
"TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)以建立更準確的 HLS playlist。此工作可能需要使用較長時間來完成。",
|
||||
"TaskKeyframeExtractor": "關鍵影格提取器",
|
||||
"External": "外部",
|
||||
"HearingImpaired": "聽力障礙",
|
||||
"TaskRefreshTrickplayImages": "產生搜畫預覽圖",
|
||||
"TaskRefreshTrickplayImagesDescription": "爲已啟用功能嘅媒體櫃影片製作快轉預覽圖。",
|
||||
"TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
|
||||
"TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。",
|
||||
"TaskExtractMediaSegments": "掃描媒體分段資訊",
|
||||
"TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅外掛程式入面提取媒體片段。",
|
||||
"TaskDownloadMissingLyrics": "下載缺失嘅歌詞",
|
||||
"TaskDownloadMissingLyricsDescription": "幫啲歌下載歌詞",
|
||||
"TaskCleanCollectionsAndPlaylists": "清理媒體系列(Collections)同埋播放清單",
|
||||
"TaskExtractMediaSegmentsDescription": "從允許MediaSegment 功能的插件中獲取媒體片段。",
|
||||
"TaskDownloadMissingLyrics": "下載欠缺歌詞",
|
||||
"TaskDownloadMissingLyricsDescription": "下載歌詞",
|
||||
"TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單",
|
||||
"TaskAudioNormalization": "音訊同等化",
|
||||
"TaskAudioNormalizationDescription": "掃描檔案入面嘅音訊標准化(Audio Normalization)數據。",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "自動清理資料庫同播放清單入面已經唔存在嘅項目。",
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體櫃設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。",
|
||||
"TaskMoveTrickplayImages": "搬移快轉預覽圖嘅位置",
|
||||
"CleanupUserDataTask": "清理使用者資料嘅任務",
|
||||
"CleanupUserDataTaskDescription": "從使用者數據入面清除嗰啲經已被刪除咗超過 90 日嘅媒體相關資料。"
|
||||
"TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
|
||||
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
|
||||
"CleanupUserDataTask": "用戶資料清理工作",
|
||||
"CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -41,8 +41,8 @@ public class OfficialRatingComparer : IBaseItemComparer
|
||||
ArgumentNullException.ThrowIfNull(y);
|
||||
var zeroRating = new ParentalRatingScore(0, 0);
|
||||
|
||||
var ratingX = string.IsNullOrEmpty(x.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(x.OfficialRating, x.GetPreferredMetadataCountryCode()) ?? zeroRating;
|
||||
var ratingY = string.IsNullOrEmpty(y.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(y.OfficialRating, y.GetPreferredMetadataCountryCode()) ?? zeroRating;
|
||||
var ratingX = string.IsNullOrEmpty(x.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(x.OfficialRating) ?? zeroRating;
|
||||
var ratingY = string.IsNullOrEmpty(y.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(y.OfficialRating) ?? zeroRating;
|
||||
var scoreCompare = ratingX.Score.CompareTo(ratingY.Score);
|
||||
if (scoreCompare is 0)
|
||||
{
|
||||
|
||||
@@ -187,7 +187,39 @@ public class ArtistsController : BaseJellyfinApiController
|
||||
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
|
||||
}
|
||||
|
||||
query.ApplyFilters(filters);
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = _libraryManager.GetArtists(query);
|
||||
|
||||
@@ -358,7 +390,39 @@ public class ArtistsController : BaseJellyfinApiController
|
||||
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
|
||||
}
|
||||
|
||||
query.ApplyFilters(filters);
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = _libraryManager.GetAlbumArtists(query);
|
||||
|
||||
|
||||
@@ -136,13 +136,45 @@ public class ChannelsController : BaseJellyfinApiController
|
||||
{
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
ChannelIds = [channelId],
|
||||
ChannelIds = new[] { channelId },
|
||||
ParentId = folderId ?? Guid.Empty,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
|
||||
DtoOptions = new DtoOptions { Fields = fields }
|
||||
};
|
||||
|
||||
query.ApplyFilters(filters);
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
@@ -183,7 +215,39 @@ public class ChannelsController : BaseJellyfinApiController
|
||||
DtoOptions = new DtoOptions { Fields = fields }
|
||||
};
|
||||
|
||||
query.ApplyFilters(filters);
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1403,8 +1403,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
double fps = state.TargetFramerate ?? 0.0f;
|
||||
int segmentLength = state.SegmentLength * 1000;
|
||||
|
||||
// If video is transcoded and framerate is fractional (i.e. 23.976), we need to slightly adjust segment length
|
||||
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001)
|
||||
// If framerate is fractional (i.e. 23.976), we need to slightly adjust segment length
|
||||
if (Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001)
|
||||
{
|
||||
double nearestIntFramerate = Math.Ceiling(fps);
|
||||
segmentLength = (int)Math.Ceiling(segmentLength * (nearestIntFramerate / fps));
|
||||
|
||||
@@ -249,7 +249,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
item.IndexNumber = request.IndexNumber;
|
||||
item.ParentIndexNumber = request.ParentIndexNumber;
|
||||
item.Overview = request.Overview;
|
||||
item.Genres = request.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
item.Genres = request.Genres;
|
||||
|
||||
if (item is Episode episode)
|
||||
{
|
||||
@@ -270,7 +270,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
|
||||
if (request.Studios is not null)
|
||||
{
|
||||
item.Studios = Array.ConvertAll(request.Studios, x => x.Name).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
item.Studios = Array.ConvertAll(request.Studios, x => x.Name);
|
||||
}
|
||||
|
||||
if (request.DateCreated.HasValue)
|
||||
@@ -287,7 +287,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
item.CustomRating = request.CustomRating;
|
||||
|
||||
var currentTags = item.Tags;
|
||||
var newTags = request.Tags.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
var newTags = request.Tags;
|
||||
var removedTags = currentTags.Except(newTags).ToList();
|
||||
var addedTags = newTags.Except(currentTags).ToList();
|
||||
item.Tags = newTags;
|
||||
@@ -373,7 +373,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
|
||||
if (request.ProductionLocations is not null)
|
||||
{
|
||||
item.ProductionLocations = request.ProductionLocations.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
item.ProductionLocations = request.ProductionLocations;
|
||||
}
|
||||
|
||||
item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
|
||||
@@ -421,7 +421,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
{
|
||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||
{
|
||||
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
{
|
||||
if (item is IHasArtist hasArtists)
|
||||
{
|
||||
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -386,7 +386,39 @@ public class ItemsController : BaseJellyfinApiController
|
||||
query.CollapseBoxSetItems = false;
|
||||
}
|
||||
|
||||
query.ApplyFilters(filters);
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by Series Status
|
||||
if (seriesStatus.Length != 0)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ public class QuickConnectController : BaseJellyfinApiController
|
||||
/// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
|
||||
[HttpPost("Initiate")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -209,25 +209,6 @@ public class DynamicHlsHelper
|
||||
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
|
||||
}
|
||||
|
||||
// For DoVi profiles without a compatible base layer (P5 HEVC, P10/bl0 AV1),
|
||||
// add a spec-compliant dvh1/dav1 variant before the hvc1 hack variant.
|
||||
// SUPPLEMENTAL-CODECS cannot be used for these profiles (no compatible BL to supplement).
|
||||
// The DoVi variant is listed first so spec-compliant clients (Apple TV, webOS 24+)
|
||||
// select it over the fallback when both have identical BANDWIDTH.
|
||||
// Only emit for clients that explicitly declared DOVI support to avoid breaking
|
||||
// non-compliant players that don't recognize dvh1/dav1 CODECS strings.
|
||||
if (state.VideoStream is not null
|
||||
&& state.VideoRequest is not null
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& state.VideoStream.VideoRangeType == VideoRangeType.DOVI
|
||||
&& state.VideoStream.DvProfile.HasValue
|
||||
&& state.VideoStream.DvLevel.HasValue
|
||||
&& state.GetRequestedRangeTypes(state.VideoStream.Codec)
|
||||
.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AppendDoviPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
||||
}
|
||||
|
||||
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
||||
|
||||
if (state.VideoStream is not null && state.VideoRequest is not null)
|
||||
@@ -374,65 +355,6 @@ public class DynamicHlsHelper
|
||||
return playlistBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a Dolby Vision variant with dvh1/dav1 CODECS for profiles without a compatible
|
||||
/// base layer (P5 HEVC, P10/bl0 AV1). This enables spec-compliant HLS clients to detect
|
||||
/// DoVi from the manifest rather than relying on init segment inspection.
|
||||
/// </summary>
|
||||
/// <param name="builder">StringBuilder for the master playlist.</param>
|
||||
/// <param name="state">StreamState of the current stream.</param>
|
||||
/// <param name="url">Playlist URL for this variant.</param>
|
||||
/// <param name="bitrate">Bitrate for the BANDWIDTH field.</param>
|
||||
/// <param name="subtitleGroup">Subtitle group identifier, or null.</param>
|
||||
private void AppendDoviPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
|
||||
{
|
||||
var dvProfile = state.VideoStream.DvProfile;
|
||||
var dvLevel = state.VideoStream.DvLevel;
|
||||
if (dvProfile is null || dvLevel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playlistBuilder = new StringBuilder();
|
||||
playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
|
||||
.Append(bitrate.ToString(CultureInfo.InvariantCulture))
|
||||
.Append(",AVERAGE-BANDWIDTH=")
|
||||
.Append(bitrate.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
playlistBuilder.Append(",VIDEO-RANGE=PQ");
|
||||
|
||||
var dvCodec = HlsCodecStringHelpers.GetDoviString(dvProfile.Value, dvLevel.Value, state.ActualOutputVideoCodec);
|
||||
|
||||
string audioCodecs = string.Empty;
|
||||
if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
|
||||
{
|
||||
audioCodecs = GetPlaylistAudioCodecs(state);
|
||||
}
|
||||
|
||||
playlistBuilder.Append(",CODECS=\"")
|
||||
.Append(dvCodec);
|
||||
if (!string.IsNullOrEmpty(audioCodecs))
|
||||
{
|
||||
playlistBuilder.Append(',').Append(audioCodecs);
|
||||
}
|
||||
|
||||
playlistBuilder.Append('"');
|
||||
|
||||
AppendPlaylistResolutionField(playlistBuilder, state);
|
||||
AppendPlaylistFramerateField(playlistBuilder, state);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subtitleGroup))
|
||||
{
|
||||
playlistBuilder.Append(",SUBTITLES=\"")
|
||||
.Append(subtitleGroup)
|
||||
.Append('"');
|
||||
}
|
||||
|
||||
playlistBuilder.AppendLine();
|
||||
playlistBuilder.AppendLine(url);
|
||||
builder.Append(playlistBuilder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a VIDEO-RANGE field containing the range of the output video stream.
|
||||
/// </summary>
|
||||
|
||||
@@ -346,25 +346,4 @@ public static class HlsCodecStringHelpers
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Dolby Vision codec string for profiles without a compatible base layer.
|
||||
/// </summary>
|
||||
/// <param name="dvProfile">Dolby Vision profile number.</param>
|
||||
/// <param name="dvLevel">Dolby Vision level number.</param>
|
||||
/// <param name="codec">Video codec name (e.g. "hevc", "av1") to determine the DoVi FourCC.</param>
|
||||
/// <returns>Dolby Vision codec string.</returns>
|
||||
public static string GetDoviString(int dvProfile, int dvLevel, string codec)
|
||||
{
|
||||
// HEVC DoVi uses dvh1, AV1 DoVi uses dav1 (out-of-band parameter sets, recommended by Apple HLS spec Rule 1.10)
|
||||
var fourCc = string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1";
|
||||
StringBuilder result = new StringBuilder(fourCc, 12);
|
||||
|
||||
result.Append('.')
|
||||
.AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", dvProfile)
|
||||
.Append('.')
|
||||
.AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", dvLevel);
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ public static class StreamingHelpers
|
||||
Dictionary<string, string?> streamOptions = new Dictionary<string, string?>();
|
||||
foreach (var param in queryString)
|
||||
{
|
||||
if (param.Key.Length > 0 && char.IsLower(param.Key[0]))
|
||||
if (char.IsLower(param.Key[0]))
|
||||
{
|
||||
// This was probably not parsed initially and should be a StreamOptions
|
||||
// or the generated URL should correctly serialize it
|
||||
|
||||
@@ -683,15 +683,14 @@ public sealed class BaseItemRepository
|
||||
.SelectMany(f => f.Values)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
var types = allListedItemValues.Select(e => e.MagicNumber).Distinct().ToArray();
|
||||
var values = allListedItemValues.Select(e => e.Value).Distinct().ToArray();
|
||||
var allListedItemValuesSet = allListedItemValues.ToHashSet();
|
||||
|
||||
var existingValues = context.ItemValues
|
||||
.Where(e => types.Contains(e.Type) && values.Contains(e.Value))
|
||||
.AsEnumerable()
|
||||
.Where(e => allListedItemValuesSet.Contains((e.Type, e.Value)))
|
||||
.Select(e => new
|
||||
{
|
||||
item = e,
|
||||
Key = e.Type + "+" + e.Value
|
||||
})
|
||||
.Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
|
||||
.Select(e => e.item)
|
||||
.ToArray();
|
||||
var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).Select(f => new ItemValue()
|
||||
{
|
||||
@@ -1051,7 +1050,7 @@ public sealed class BaseItemRepository
|
||||
entity.TotalBitrate = dto.TotalBitrate;
|
||||
entity.ExternalId = dto.ExternalId;
|
||||
entity.Size = dto.Size;
|
||||
entity.Genres = string.Join('|', dto.Genres.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
entity.Genres = string.Join('|', dto.Genres);
|
||||
entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
|
||||
entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
|
||||
entity.ChannelId = dto.ChannelId;
|
||||
@@ -1078,9 +1077,9 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
|
||||
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p)).Distinct(StringComparer.OrdinalIgnoreCase)) : null;
|
||||
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
|
||||
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
|
||||
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null;
|
||||
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
|
||||
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
|
||||
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
|
||||
.Select(e => new BaseItemMetadataField()
|
||||
{
|
||||
@@ -1123,12 +1122,12 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (dto is IHasArtist hasArtists)
|
||||
{
|
||||
entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
|
||||
entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
|
||||
}
|
||||
|
||||
if (dto is IHasAlbumArtist hasAlbumArtists)
|
||||
{
|
||||
entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
|
||||
entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null;
|
||||
}
|
||||
|
||||
if (dto is LiveTvProgram program)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@ using Jellyfin.Api.Middleware;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Jellyfin.Server.Extensions
|
||||
{
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json.Nodes;
|
||||
using Emby.Server.Implementations;
|
||||
using Jellyfin.Api.Auth;
|
||||
using Jellyfin.Api.Auth.AnonymousLanAccessPolicy;
|
||||
@@ -26,6 +26,7 @@ using Jellyfin.Server.Filters;
|
||||
using MediaBrowser.Common.Api;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
@@ -33,7 +34,9 @@ using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Interfaces;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
|
||||
@@ -205,7 +208,7 @@ namespace Jellyfin.Server.Extensions
|
||||
{
|
||||
{
|
||||
"x-jellyfin-version",
|
||||
new JsonNodeExtension(JsonValue.Create(version))
|
||||
new OpenApiString(version)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -259,7 +262,6 @@ namespace Jellyfin.Server.Extensions
|
||||
c.OperationFilter<FileRequestFilter>();
|
||||
c.OperationFilter<ParameterObsoleteFilter>();
|
||||
c.DocumentFilter<AdditionalModelFilter>();
|
||||
c.DocumentFilter<SecuritySchemeReferenceFixupFilter>();
|
||||
})
|
||||
.Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>());
|
||||
}
|
||||
@@ -331,10 +333,10 @@ namespace Jellyfin.Server.Extensions
|
||||
options.MapType<Dictionary<ImageType, string>>(() =>
|
||||
new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Object,
|
||||
Type = "object",
|
||||
AdditionalProperties = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.String
|
||||
Type = "string"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -342,17 +344,18 @@ namespace Jellyfin.Server.Extensions
|
||||
options.MapType<Dictionary<string, string?>>(() =>
|
||||
new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Object,
|
||||
Type = "object",
|
||||
AdditionalProperties = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.String | JsonSchemaType.Null
|
||||
Type = "string",
|
||||
Nullable = true
|
||||
}
|
||||
});
|
||||
|
||||
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
|
||||
options.MapType<Version>(() => new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.String
|
||||
Type = "string"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Nodes;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Server.Migrations;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
|
||||
using MediaBrowser.Model.ApiClient;
|
||||
using MediaBrowser.Model.Session;
|
||||
using MediaBrowser.Model.SyncPlay;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters
|
||||
@@ -24,7 +25,7 @@ namespace Jellyfin.Server.Filters
|
||||
public class AdditionalModelFilter : IDocumentFilter
|
||||
{
|
||||
// Array of options that should not be visible in the api spec.
|
||||
private static readonly Type[] _ignoredConfigurations = [typeof(MigrationOptions), typeof(MediaBrowser.Model.Branding.BrandingOptions)];
|
||||
private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions), typeof(MediaBrowser.Model.Branding.BrandingOptions) };
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
/// <summary>
|
||||
@@ -47,8 +48,8 @@ namespace Jellyfin.Server.Filters
|
||||
&& t != typeof(WebSocketMessageInfo))
|
||||
.ToList();
|
||||
|
||||
var inboundWebSocketSchemas = new List<IOpenApiSchema>();
|
||||
var inboundWebSocketDiscriminators = new Dictionary<string, OpenApiSchemaReference>();
|
||||
var inboundWebSocketSchemas = new List<OpenApiSchema>();
|
||||
var inboundWebSocketDiscriminators = new Dictionary<string, string>();
|
||||
foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t)))
|
||||
{
|
||||
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
|
||||
@@ -59,16 +60,18 @@ namespace Jellyfin.Server.Filters
|
||||
|
||||
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
|
||||
inboundWebSocketSchemas.Add(schema);
|
||||
if (schema is OpenApiSchemaReference schemaRef)
|
||||
{
|
||||
inboundWebSocketDiscriminators[messageType.ToString()!] = schemaRef;
|
||||
}
|
||||
inboundWebSocketDiscriminators[messageType.ToString()!] = schema.Reference.ReferenceV3;
|
||||
}
|
||||
|
||||
var inboundWebSocketMessageSchema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Object,
|
||||
Type = "object",
|
||||
Description = "Represents the list of possible inbound websocket types",
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = nameof(InboundWebSocketMessage),
|
||||
Type = ReferenceType.Schema
|
||||
},
|
||||
OneOf = inboundWebSocketSchemas,
|
||||
Discriminator = new OpenApiDiscriminator
|
||||
{
|
||||
@@ -79,8 +82,8 @@ namespace Jellyfin.Server.Filters
|
||||
|
||||
context.SchemaRepository.AddDefinition(nameof(InboundWebSocketMessage), inboundWebSocketMessageSchema);
|
||||
|
||||
var outboundWebSocketSchemas = new List<IOpenApiSchema>();
|
||||
var outboundWebSocketDiscriminators = new Dictionary<string, OpenApiSchemaReference>();
|
||||
var outboundWebSocketSchemas = new List<OpenApiSchema>();
|
||||
var outboundWebSocketDiscriminators = new Dictionary<string, string>();
|
||||
foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t)))
|
||||
{
|
||||
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
|
||||
@@ -91,55 +94,58 @@ namespace Jellyfin.Server.Filters
|
||||
|
||||
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
|
||||
outboundWebSocketSchemas.Add(schema);
|
||||
if (schema is OpenApiSchemaReference schemaRef)
|
||||
{
|
||||
outboundWebSocketDiscriminators.Add(messageType.ToString()!, schemaRef);
|
||||
}
|
||||
outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3);
|
||||
}
|
||||
|
||||
// Add custom "SyncPlayGroupUpdateMessage" schema because Swashbuckle cannot generate it for us
|
||||
var syncPlayGroupUpdateMessageSchema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Object,
|
||||
Type = "object",
|
||||
Description = "Untyped sync play command.",
|
||||
Properties = new Dictionary<string, IOpenApiSchema>
|
||||
Properties = new Dictionary<string, OpenApiSchema>
|
||||
{
|
||||
{
|
||||
"Data", new OpenApiSchema
|
||||
{
|
||||
AllOf = new List<IOpenApiSchema>
|
||||
{
|
||||
new OpenApiSchemaReference(nameof(GroupUpdate<object>), null, null)
|
||||
},
|
||||
AllOf =
|
||||
[
|
||||
new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(GroupUpdate<object>) } }
|
||||
],
|
||||
Description = "Group update data",
|
||||
Nullable = false,
|
||||
}
|
||||
},
|
||||
{ "MessageId", new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid", Description = "Gets or sets the message id." } },
|
||||
{ "MessageId", new OpenApiSchema { Type = "string", Format = "uuid", Description = "Gets or sets the message id." } },
|
||||
{
|
||||
"MessageType", new OpenApiSchema
|
||||
{
|
||||
Enum = Enum.GetValues<SessionMessageType>().Select(type => (JsonNode)JsonValue.Create(type.ToString())!).ToList(),
|
||||
AllOf = new List<IOpenApiSchema>
|
||||
{
|
||||
new OpenApiSchemaReference(nameof(SessionMessageType), null, null)
|
||||
},
|
||||
Enum = Enum.GetValues<SessionMessageType>().Select(type => new OpenApiString(type.ToString())).ToList<IOpenApiAny>(),
|
||||
AllOf =
|
||||
[
|
||||
new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(SessionMessageType) } }
|
||||
],
|
||||
Description = "The different kinds of messages that are used in the WebSocket api.",
|
||||
Default = JsonValue.Create(nameof(SessionMessageType.SyncPlayGroupUpdate)),
|
||||
Default = new OpenApiString(nameof(SessionMessageType.SyncPlayGroupUpdate)),
|
||||
ReadOnly = true
|
||||
}
|
||||
},
|
||||
},
|
||||
AdditionalPropertiesAllowed = false,
|
||||
Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "SyncPlayGroupUpdateMessage" }
|
||||
};
|
||||
context.SchemaRepository.AddDefinition("SyncPlayGroupUpdateMessage", syncPlayGroupUpdateMessageSchema);
|
||||
var syncPlayRef = new OpenApiSchemaReference("SyncPlayGroupUpdateMessage", null, null);
|
||||
outboundWebSocketSchemas.Add(syncPlayRef);
|
||||
outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayRef;
|
||||
outboundWebSocketSchemas.Add(syncPlayGroupUpdateMessageSchema);
|
||||
outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayGroupUpdateMessageSchema.Reference.ReferenceV3;
|
||||
|
||||
var outboundWebSocketMessageSchema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Object,
|
||||
Type = "object",
|
||||
Description = "Represents the list of possible outbound websocket types",
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = nameof(OutboundWebSocketMessage),
|
||||
Type = ReferenceType.Schema
|
||||
},
|
||||
OneOf = outboundWebSocketSchemas,
|
||||
Discriminator = new OpenApiDiscriminator
|
||||
{
|
||||
@@ -153,12 +159,17 @@ namespace Jellyfin.Server.Filters
|
||||
nameof(WebSocketMessage),
|
||||
new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Object,
|
||||
Type = "object",
|
||||
Description = "Represents the possible websocket types",
|
||||
OneOf = new List<IOpenApiSchema>
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
new OpenApiSchemaReference(nameof(InboundWebSocketMessage), null, null),
|
||||
new OpenApiSchemaReference(nameof(OutboundWebSocketMessage), null, null)
|
||||
Id = nameof(WebSocketMessage),
|
||||
Type = ReferenceType.Schema
|
||||
},
|
||||
OneOf = new[]
|
||||
{
|
||||
inboundWebSocketMessageSchema,
|
||||
outboundWebSocketMessageSchema
|
||||
}
|
||||
});
|
||||
|
||||
@@ -169,8 +180,8 @@ namespace Jellyfin.Server.Filters
|
||||
&& t.BaseType.GetGenericTypeDefinition() == typeof(GroupUpdate<>))
|
||||
.ToList();
|
||||
|
||||
var groupUpdateSchemas = new List<IOpenApiSchema>();
|
||||
var groupUpdateDiscriminators = new Dictionary<string, OpenApiSchemaReference>();
|
||||
var groupUpdateSchemas = new List<OpenApiSchema>();
|
||||
var groupUpdateDiscriminators = new Dictionary<string, string>();
|
||||
foreach (var type in groupUpdateTypes)
|
||||
{
|
||||
var groupUpdateType = (GroupUpdateType?)type.GetProperty(nameof(GroupUpdate<object>.Type))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
|
||||
@@ -181,16 +192,18 @@ namespace Jellyfin.Server.Filters
|
||||
|
||||
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
|
||||
groupUpdateSchemas.Add(schema);
|
||||
if (schema is OpenApiSchemaReference schemaRef)
|
||||
{
|
||||
groupUpdateDiscriminators[groupUpdateType.ToString()!] = schemaRef;
|
||||
}
|
||||
groupUpdateDiscriminators[groupUpdateType.ToString()!] = schema.Reference.ReferenceV3;
|
||||
}
|
||||
|
||||
var groupUpdateSchema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Object,
|
||||
Type = "object",
|
||||
Description = "Represents the list of possible group update types",
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = nameof(GroupUpdate<object>),
|
||||
Type = ReferenceType.Schema
|
||||
},
|
||||
OneOf = groupUpdateSchemas,
|
||||
Discriminator = new OpenApiDiscriminator
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
@@ -48,7 +48,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public OpenApiDocument GetSwagger(string documentName, string host, string basePath)
|
||||
public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters
|
||||
@@ -28,11 +28,10 @@ namespace Jellyfin.Server.Filters
|
||||
{
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.String,
|
||||
Type = "string",
|
||||
Format = "binary"
|
||||
}
|
||||
};
|
||||
body.Content ??= new System.Collections.Generic.Dictionary<string, OpenApiMediaType>();
|
||||
foreach (var contentType in contentTypes)
|
||||
{
|
||||
body.Content.Add(contentType, mediaType);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters
|
||||
@@ -14,7 +14,7 @@ namespace Jellyfin.Server.Filters
|
||||
{
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.String,
|
||||
Type = "string",
|
||||
Format = "binary"
|
||||
}
|
||||
};
|
||||
@@ -22,11 +22,6 @@ namespace Jellyfin.Server.Filters
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (operation.Responses is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var attribute in context.ApiDescription.ActionDescriptor.EndpointMetadata)
|
||||
{
|
||||
if (attribute is ProducesFileAttribute producesFileAttribute)
|
||||
@@ -36,7 +31,7 @@ namespace Jellyfin.Server.Filters
|
||||
.FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal));
|
||||
|
||||
// Operation doesn't have a response.
|
||||
if (response.Value?.Content is null)
|
||||
if (response.Value is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters;
|
||||
public class FlagsEnumSchemaFilter : ISchemaFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Apply(IOpenApiSchema schema, SchemaFilterContext context)
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type);
|
||||
if (type is null || !type.IsEnum)
|
||||
@@ -29,16 +29,11 @@ public class FlagsEnumSchemaFilter : ISchemaFilter
|
||||
return;
|
||||
}
|
||||
|
||||
if (schema is not OpenApiSchema concreteSchema)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.MemberInfo is null)
|
||||
{
|
||||
// Processing the enum definition itself - ensure it's type "string" not "integer"
|
||||
concreteSchema.Type = JsonSchemaType.String;
|
||||
concreteSchema.Format = null;
|
||||
schema.Type = "string";
|
||||
schema.Format = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -48,11 +43,11 @@ public class FlagsEnumSchemaFilter : ISchemaFilter
|
||||
|
||||
// Flags enums should be represented as arrays referencing the enum schema
|
||||
// since multiple values can be combined
|
||||
concreteSchema.Type = JsonSchemaType.Array;
|
||||
concreteSchema.Format = null;
|
||||
concreteSchema.Enum = null;
|
||||
concreteSchema.AllOf = null;
|
||||
concreteSchema.Items = enumSchema;
|
||||
schema.Type = "array";
|
||||
schema.Format = null;
|
||||
schema.Enum = null;
|
||||
schema.AllOf = null;
|
||||
schema.Items = enumSchema;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Nodes;
|
||||
using Jellyfin.Data.Attributes;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters;
|
||||
public class IgnoreEnumSchemaFilter : ISchemaFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Apply(IOpenApiSchema schema, SchemaFilterContext context)
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
if (context.Type.IsEnum || (Nullable.GetUnderlyingType(context.Type)?.IsEnum ?? false))
|
||||
{
|
||||
@@ -25,23 +25,18 @@ public class IgnoreEnumSchemaFilter : ISchemaFilter
|
||||
return;
|
||||
}
|
||||
|
||||
if (schema is not OpenApiSchema concreteSchema)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enumOpenApiNodes = new List<JsonNode>();
|
||||
var enumOpenApiStrings = new List<IOpenApiAny>();
|
||||
|
||||
foreach (var enumName in Enum.GetNames(type))
|
||||
{
|
||||
var member = type.GetMember(enumName)[0];
|
||||
if (!member.GetCustomAttributes<OpenApiIgnoreEnumAttribute>().Any())
|
||||
{
|
||||
enumOpenApiNodes.Add(JsonValue.Create(enumName)!);
|
||||
enumOpenApiStrings.Add(new OpenApiString(enumName));
|
||||
}
|
||||
}
|
||||
|
||||
concreteSchema.Enum = enumOpenApiNodes;
|
||||
schema.Enum = enumOpenApiStrings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters
|
||||
@@ -21,17 +21,11 @@ namespace Jellyfin.Server.Filters
|
||||
.OfType<ParameterObsoleteAttribute>()
|
||||
.Any())
|
||||
{
|
||||
if (operation.Parameters is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var parameter in operation.Parameters)
|
||||
{
|
||||
if (parameter is OpenApiParameter concreteParam
|
||||
&& string.Equals(concreteParam.Name, parameterDescription.Name, StringComparison.Ordinal))
|
||||
if (parameter.Name.Equals(parameterDescription.Name, StringComparison.Ordinal))
|
||||
{
|
||||
concreteParam.Deprecated = true;
|
||||
parameter.Deprecated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
@@ -8,12 +8,12 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Responses?.TryAdd(
|
||||
operation.Responses.TryAdd(
|
||||
"503",
|
||||
new OpenApiResponse
|
||||
{
|
||||
Description = "The server is currently starting or is temporarily not available.",
|
||||
Headers = new Dictionary<string, IOpenApiHeader>
|
||||
Headers = new Dictionary<string, OpenApiHeader>
|
||||
{
|
||||
{
|
||||
"Retry-After", new OpenApiHeader
|
||||
@@ -23,7 +23,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
|
||||
Description = "A hint for when to retry the operation in full seconds.",
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Integer,
|
||||
Type = "integer",
|
||||
Format = "int32"
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
|
||||
Description = "A short plain-text reason why the server is not available.",
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.String,
|
||||
Type = "string",
|
||||
Format = "text"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
@@ -66,10 +66,17 @@ public class SecurityRequirementsOperationFilter : IOperationFilter
|
||||
return;
|
||||
}
|
||||
|
||||
operation.Responses?.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses?.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
|
||||
var scheme = new OpenApiSecuritySchemeReference(AuthenticationSchemes.CustomAuthentication, null, null);
|
||||
var scheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = AuthenticationSchemes.CustomAuthentication
|
||||
},
|
||||
};
|
||||
|
||||
// Add DefaultAuthorization scope to any endpoint that has a policy with a requirement that is a subset of DefaultAuthorization.
|
||||
if (!requiredScopes.Contains(DefaultAuthPolicy.AsSpan(), StringComparison.Ordinal))
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Document filter that fixes security scheme references after document generation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In Microsoft.OpenApi v2, <see cref="OpenApiSecuritySchemeReference"/> requires a resolved
|
||||
/// <c>Target</c> to serialize correctly. References created without a host document (as in
|
||||
/// operation filters) serialize as empty objects. This filter re-creates all security scheme
|
||||
/// references with the document context so they resolve properly during serialization.
|
||||
/// </remarks>
|
||||
internal class SecuritySchemeReferenceFixupFilter : IDocumentFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
swaggerDoc.RegisterComponents();
|
||||
|
||||
if (swaggerDoc.Paths is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var pathItem in swaggerDoc.Paths.Values)
|
||||
{
|
||||
if (pathItem.Operations is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var operation in pathItem.Operations.Values)
|
||||
{
|
||||
if (operation.Security is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < operation.Security.Count; i++)
|
||||
{
|
||||
var oldReq = operation.Security[i];
|
||||
var newReq = new OpenApiSecurityRequirement();
|
||||
foreach (var kvp in oldReq)
|
||||
{
|
||||
var fixedRef = new OpenApiSecuritySchemeReference(kvp.Key.Reference.Id!, swaggerDoc);
|
||||
newReq[fixedRef] = kvp.Value;
|
||||
}
|
||||
|
||||
operation.Security[i] = newReq;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Migration to fix broken library subtitle download languages.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2026-02-06T20:00:00", nameof(FixLibrarySubtitleDownloadLanguages))]
|
||||
internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine
|
||||
{
|
||||
private readonly ILocalizationManager _localizationManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FixLibrarySubtitleDownloadLanguages"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localizationManager">The Localization manager.</param>
|
||||
/// <param name="startupLogger">The startup logger for Startup UI integration.</param>
|
||||
/// <param name="libraryManager">The Library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public FixLibrarySubtitleDownloadLanguages(
|
||||
ILocalizationManager localizationManager,
|
||||
IStartupLogger<FixLibrarySubtitleDownloadLanguages> startupLogger,
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<FixLibrarySubtitleDownloadLanguages> logger)
|
||||
{
|
||||
_localizationManager = localizationManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = startupLogger.With(logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting to fix library subtitle download languages.");
|
||||
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders(false);
|
||||
|
||||
foreach (var virtualFolder in virtualFolders)
|
||||
{
|
||||
var options = virtualFolder.LibraryOptions;
|
||||
if (options.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some virtual folders don't have a proper item id.
|
||||
if (!Guid.TryParse(virtualFolder.ItemId, out var folderId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId);
|
||||
if (collectionFolder is null)
|
||||
{
|
||||
_logger.LogWarning("Could not find collection folder for virtual folder '{LibraryName}' with id '{FolderId}'. Skipping.", virtualFolder.Name, folderId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var fixedLanguages = new List<string>();
|
||||
|
||||
foreach (var language in options.SubtitleDownloadLanguages)
|
||||
{
|
||||
var foundLanguage = _localizationManager.FindLanguageInfo(language)?.ThreeLetterISOLanguageName;
|
||||
if (foundLanguage is not null)
|
||||
{
|
||||
// Converted ISO 639-2/B to T (ger to deu)
|
||||
if (!string.Equals(foundLanguage, language, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("Converted '{Language}' to '{ResolvedLanguage}' in library '{LibraryName}'.", language, foundLanguage, virtualFolder.Name);
|
||||
}
|
||||
|
||||
if (fixedLanguages.Contains(foundLanguage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("Language '{Language}' already exists for library '{LibraryName}'. Skipping duplicate.", foundLanguage, virtualFolder.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
fixedLanguages.Add(foundLanguage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Could not resolve language '{Language}' in library '{LibraryName}'. Skipping.", language, virtualFolder.Name);
|
||||
}
|
||||
}
|
||||
|
||||
options.SubtitleDownloadLanguages = [.. fixedLanguages];
|
||||
collectionFolder.UpdateLibraryOptions(options);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Library subtitle download languages fixed.");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -464,16 +464,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
using (var checkpointConnection = new SqliteConnection($"Filename={libraryDbPath}"))
|
||||
{
|
||||
checkpointConnection.Open();
|
||||
using var cmd = checkpointConnection.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
_logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
|
||||
File.Move(libraryDbPath, libraryDbPath + ".old", true);
|
||||
}
|
||||
@@ -515,7 +505,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
PlayCount = dto.GetInt32(4),
|
||||
IsFavorite = dto.GetBoolean(5),
|
||||
PlaybackPositionTicks = dto.GetInt64(6),
|
||||
LastPlayedDate = dto.IsDBNull(7) ? null : ReadDateTimeFromColumn(dto, 7),
|
||||
LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
|
||||
AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
|
||||
SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
|
||||
Likes = null,
|
||||
@@ -524,28 +514,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
};
|
||||
}
|
||||
|
||||
private static DateTime? ReadDateTimeFromColumn(SqliteDataReader reader, int index)
|
||||
{
|
||||
// Try reading as a formatted date string first (handles ISO-8601 dates).
|
||||
if (reader.TryReadDateTime(index, out var dateTimeResult))
|
||||
{
|
||||
return dateTimeResult;
|
||||
}
|
||||
|
||||
// Some databases have Unix epoch timestamps stored as integers.
|
||||
// SqliteDataReader.GetDateTime interprets integers as Julian dates, which crashes
|
||||
// for Unix epoch values. Handle them explicitly.
|
||||
var rawValue = reader.GetValue(index);
|
||||
if (rawValue is long unixTimestamp
|
||||
&& unixTimestamp > 0
|
||||
&& unixTimestamp <= DateTimeOffset.MaxValue.ToUnixTimeSeconds())
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).UtcDateTime;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private AncestorId GetAncestorId(SqliteDataReader reader)
|
||||
{
|
||||
return new AncestorId()
|
||||
@@ -1195,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))
|
||||
|
||||
@@ -162,7 +162,7 @@ public sealed class SetupServer : IDisposable
|
||||
{
|
||||
var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger<SetupServer>(), config.EnableIPv4, config.EnableIPv6);
|
||||
knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6);
|
||||
var bindInterfaces = NetworkManager.GetAllBindInterfaces(_loggerFactory.CreateLogger<NetworkManager>(), false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6);
|
||||
var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6);
|
||||
Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer(
|
||||
bindInterfaces,
|
||||
config.InternalHttpPort,
|
||||
|
||||
@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -1604,7 +1605,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
return !GetBlockUnratedValue(user);
|
||||
}
|
||||
|
||||
var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
|
||||
var ratingScore = LocalizationManager.GetRatingScore(rating);
|
||||
|
||||
// Could not determine rating level
|
||||
if (ratingScore is null)
|
||||
@@ -1646,7 +1647,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
return null;
|
||||
}
|
||||
|
||||
return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
|
||||
return LocalizationManager.GetRatingScore(rating);
|
||||
}
|
||||
|
||||
public List<string> GetInheritedTags()
|
||||
@@ -2128,6 +2129,17 @@ namespace MediaBrowser.Controller.Entities
|
||||
};
|
||||
}
|
||||
|
||||
// Music albums usually don't have dedicated backdrops, so return one from the artist instead
|
||||
if (GetType() == typeof(MusicAlbum) && imageType == ImageType.Backdrop)
|
||||
{
|
||||
var artist = FindParent<MusicArtist>();
|
||||
|
||||
if (artist is not null)
|
||||
{
|
||||
return artist.GetImages(imageType).ElementAtOrDefault(imageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return GetImages(imageType)
|
||||
.ElementAtOrDefault(imageIndex);
|
||||
}
|
||||
@@ -2609,7 +2621,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
.Select(i => i.OfficialRating)
|
||||
.Where(i => !string.IsNullOrEmpty(i))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(rating => (rating, LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode())))
|
||||
.Select(rating => (rating, LocalizationManager.GetRatingScore(rating)))
|
||||
.OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
|
||||
.ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
|
||||
.Select(i => i.rating);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -10,7 +10,6 @@ using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
@@ -389,75 +388,5 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
User = user;
|
||||
}
|
||||
|
||||
public void ApplyFilters(ItemFilter[] filters)
|
||||
{
|
||||
static void ThrowConflictingFilters()
|
||||
=> throw new ArgumentException("Conflicting filters", nameof(filters));
|
||||
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.IsFolder:
|
||||
if (filters.Contains(ItemFilter.IsNotFolder))
|
||||
{
|
||||
ThrowConflictingFilters();
|
||||
}
|
||||
|
||||
IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
if (filters.Contains(ItemFilter.IsFolder))
|
||||
{
|
||||
ThrowConflictingFilters();
|
||||
}
|
||||
|
||||
IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
if (filters.Contains(ItemFilter.IsPlayed))
|
||||
{
|
||||
ThrowConflictingFilters();
|
||||
}
|
||||
|
||||
IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
if (filters.Contains(ItemFilter.IsUnplayed))
|
||||
{
|
||||
ThrowConflictingFilters();
|
||||
}
|
||||
|
||||
IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
if (filters.Contains(ItemFilter.Dislikes))
|
||||
{
|
||||
ThrowConflictingFilters();
|
||||
}
|
||||
|
||||
IsLiked = true;
|
||||
break;
|
||||
case ItemFilter.Dislikes:
|
||||
if (filters.Contains(ItemFilter.Likes))
|
||||
{
|
||||
ThrowConflictingFilters();
|
||||
}
|
||||
|
||||
IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
IsFavoriteOrLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,17 +201,12 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
|
||||
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||
{
|
||||
if (series is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetEpisodes()
|
||||
{
|
||||
return GetEpisodes(Series, null, null, new DtoOptions(true), true);
|
||||
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
|
||||
}
|
||||
|
||||
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -85,7 +85,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
|
||||
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
|
||||
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
|
||||
private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0);
|
||||
|
||||
private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
|
||||
|
||||
@@ -1268,20 +1267,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
// Use analyzeduration also for subtitle streams to improve resolution detection with streams inside MKS files
|
||||
var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state);
|
||||
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
||||
{
|
||||
arg.Append(' ').Append(analyzeDurationArgument);
|
||||
}
|
||||
|
||||
// Apply probesize, too, if configured
|
||||
var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg();
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument))
|
||||
{
|
||||
arg.Append(' ').Append(ffmpegProbeSizeArgument);
|
||||
}
|
||||
|
||||
// Also seek the external subtitles stream.
|
||||
var seekSubParam = GetFastSeekCommandLineParameter(state, options, segmentContainer);
|
||||
if (!string.IsNullOrEmpty(seekSubParam))
|
||||
@@ -1567,15 +1552,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
int bitrate = state.OutputVideoBitrate.Value;
|
||||
|
||||
// Bit rate under 1000k is not allowed in h264_qsv.
|
||||
// Bit rate under 1000k is not allowed in h264_qsv
|
||||
if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
bitrate = Math.Max(bitrate, 1000);
|
||||
}
|
||||
|
||||
// Currently use the same buffer size for all non-QSV encoders.
|
||||
// Use long arithmetic to prevent int32 overflow for very high bitrate values.
|
||||
int bufsize = (int)Math.Min((long)bitrate * 2, int.MaxValue);
|
||||
// Currently use the same buffer size for all encoders
|
||||
int bufsize = bitrate * 2;
|
||||
|
||||
if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -1605,13 +1589,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
// Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation
|
||||
// Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes
|
||||
// Use long arithmetic and clamp to int.MaxValue to prevent int32 overflow
|
||||
// (e.g. bitrate * 4 wraps to a negative value for bitrates above ~537 million)
|
||||
int qsvMaxrate = (int)Math.Min((long)bitrate + 1, int.MaxValue);
|
||||
int qsvInitOcc = (int)Math.Min((long)bitrate * 2, int.MaxValue);
|
||||
int qsvBufsize = (int)Math.Min((long)bitrate * 4, int.MaxValue);
|
||||
|
||||
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {qsvMaxrate} -rc_init_occupancy {qsvInitOcc} -bufsize {qsvBufsize}");
|
||||
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {bitrate + 1} -rc_init_occupancy {bitrate * 2} -bufsize {bitrate * 4}");
|
||||
}
|
||||
|
||||
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -2614,16 +2592,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
// Cap the max target bitrate to 400 Mbps.
|
||||
// No consumer or professional hardware transcode target exceeds this value
|
||||
// (Intel QSV tops out at ~300 Mbps for H.264; HEVC High Tier Level 5.x is ~240 Mbps).
|
||||
// Without this cap, plugin-provided MPEG-TS streams with no usable bitrate metadata
|
||||
// can produce unreasonably large -bufsize/-maxrate values for the encoder.
|
||||
// Note: the existing FallbackMaxStreamingBitrate mechanism (default 30 Mbps) only
|
||||
// applies when a LiveStreamId is set (M3U/HDHR sources). Plugin streams and other
|
||||
// sources that bypass the LiveTV pipeline are not covered by it.
|
||||
const int MaxSaneBitrate = 400_000_000; // 400 Mbps
|
||||
return Math.Min(bitrate ?? 0, MaxSaneBitrate);
|
||||
// Cap the max target bitrate to intMax/2 to satisfy the bufsize=bitrate*2.
|
||||
return Math.Min(bitrate ?? 0, int.MaxValue / 2);
|
||||
}
|
||||
|
||||
private int GetMinBitrate(int sourceBitrate, int requestedBitrate)
|
||||
@@ -7153,8 +7123,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFfmpegAnalyzeDurationArg(EncodingJobInfo state)
|
||||
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
|
||||
{
|
||||
var inputModifier = string.Empty;
|
||||
var analyzeDurationArgument = string.Empty;
|
||||
|
||||
// Apply -analyzeduration as per the environment variable,
|
||||
@@ -7170,26 +7141,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||
}
|
||||
|
||||
return analyzeDurationArgument;
|
||||
}
|
||||
|
||||
private string GetFfmpegProbesizeArg()
|
||||
{
|
||||
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
|
||||
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSize))
|
||||
{
|
||||
return $"-probesize {ffmpegProbeSize}";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
|
||||
{
|
||||
var inputModifier = string.Empty;
|
||||
var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state);
|
||||
|
||||
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
||||
{
|
||||
inputModifier += " " + analyzeDurationArgument;
|
||||
@@ -7198,11 +7149,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
inputModifier = inputModifier.Trim();
|
||||
|
||||
// Apply -probesize if configured
|
||||
var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg();
|
||||
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
|
||||
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument))
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSize))
|
||||
{
|
||||
inputModifier += " " + ffmpegProbeSizeArgument;
|
||||
inputModifier += $" -probesize {ffmpegProbeSize}";
|
||||
}
|
||||
|
||||
var userAgentParam = GetUserAgentParam(state);
|
||||
@@ -7242,10 +7193,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
|
||||
}
|
||||
|
||||
int readrate = 0;
|
||||
if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp)
|
||||
{
|
||||
readrate = 1;
|
||||
inputModifier += " -re";
|
||||
}
|
||||
else if (encodingOptions.EnableSegmentDeletion
|
||||
@@ -7256,15 +7205,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
// Set an input read rate limit 10x for using SegmentDeletion with stream-copy
|
||||
// to prevent ffmpeg from exiting prematurely (due to fast drive)
|
||||
readrate = 10;
|
||||
inputModifier += $" -readrate {readrate}";
|
||||
}
|
||||
|
||||
// Set a larger catchup value to revert to the old behavior,
|
||||
// otherwise, remuxing might stall due to this new option
|
||||
if (readrate > 0 && _mediaEncoder.EncoderVersion >= _minFFmpegReadrateCatchupOption)
|
||||
{
|
||||
inputModifier += $" -readrate_catchup {readrate * 100}";
|
||||
inputModifier += " -readrate 10";
|
||||
}
|
||||
|
||||
var flags = new List<string>();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -115,6 +115,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
await ExtractAllAttachmentsInternal(
|
||||
inputFile,
|
||||
mediaSource,
|
||||
false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -122,6 +123,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
private async Task ExtractAllAttachmentsInternal(
|
||||
string inputFile,
|
||||
MediaSourceInfo mediaSource,
|
||||
bool isExternal,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource);
|
||||
@@ -140,19 +142,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
return;
|
||||
}
|
||||
|
||||
// Files without video/audio streams (e.g. MKS subtitle files) don't need a dummy
|
||||
// output since there are no streams to process. Omit "-t 0 -f null null" so ffmpeg
|
||||
// doesn't fail trying to open an output with no streams. It will exit with code 1
|
||||
// ("at least one output file must be specified") which is expected and harmless
|
||||
// since we only need the -dump_attachment side effect.
|
||||
var hasVideoOrAudioStream = mediaSource.MediaStreams
|
||||
.Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio);
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:t \"\" -y {0} -i {1} {2}",
|
||||
"-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
|
||||
inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
|
||||
inputPath,
|
||||
hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty);
|
||||
inputPath);
|
||||
|
||||
int exitCode;
|
||||
|
||||
@@ -191,7 +185,12 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
if (hasVideoOrAudioStream || exitCode != 1)
|
||||
if (isExternal && exitCode == 1)
|
||||
{
|
||||
// ffmpeg returns exitCode 1 because there is no video or audio stream
|
||||
// this can be ignored
|
||||
}
|
||||
else
|
||||
{
|
||||
failed = true;
|
||||
|
||||
@@ -206,8 +205,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!failed && !Directory.Exists(outputFolder))
|
||||
else if (!Directory.Exists(outputFolder))
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
@@ -248,7 +246,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
{
|
||||
await ExtractAttachmentInternal(
|
||||
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
|
||||
mediaSource,
|
||||
mediaAttachment.Index,
|
||||
attachmentPath,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
@@ -260,7 +257,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
private async Task ExtractAttachmentInternal(
|
||||
string inputPath,
|
||||
MediaSourceInfo mediaSource,
|
||||
int attachmentStreamIndex,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -271,15 +267,12 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
|
||||
|
||||
var hasVideoOrAudioStream = mediaSource.MediaStreams
|
||||
.Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio);
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:{1} \"{2}\" -i {0} {3}",
|
||||
"-dump_attachment:{1} \"{2}\" -i {0} -t 0 -f null null",
|
||||
inputPath,
|
||||
attachmentStreamIndex,
|
||||
EncodingUtils.NormalizePath(outputPath),
|
||||
hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty);
|
||||
EncodingUtils.NormalizePath(outputPath));
|
||||
|
||||
int exitCode;
|
||||
|
||||
@@ -317,26 +310,22 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
if (hasVideoOrAudioStream || exitCode != 1)
|
||||
{
|
||||
failed = true;
|
||||
failed = true;
|
||||
|
||||
_logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode);
|
||||
try
|
||||
_logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode);
|
||||
try
|
||||
{
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath);
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!failed && !File.Exists(outputPath))
|
||||
else if (!File.Exists(outputPath))
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
|
||||
@@ -693,7 +693,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
[GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||
private static partial Regex CodecRegex();
|
||||
|
||||
[GeneratedRegex("^\\s\\S{2,3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||
[GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||
private static partial Regex FilterRegex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
"Smith/Kotzen",
|
||||
"We;Na",
|
||||
"LSR/CITY",
|
||||
"Kairon; IRSE!",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -863,7 +862,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
stream.IsAnamorphic = false;
|
||||
}
|
||||
else if (IsNearSquarePixelSar(streamInfo.SampleAspectRatio))
|
||||
else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
|
||||
{
|
||||
stream.IsAnamorphic = false;
|
||||
}
|
||||
@@ -1154,34 +1153,6 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
return Math.Abs(d1 - d2) <= variance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a sample aspect ratio represents square (or near-square) pixels.
|
||||
/// Some encoders produce SARs like 3201:3200 for content that is effectively 1:1,
|
||||
/// which would be falsely classified as anamorphic by an exact string comparison.
|
||||
/// A 1% tolerance safely covers encoder rounding artifacts while preserving detection
|
||||
/// of genuine anamorphic content (closest standard is PAL 4:3 at 16:15 = 6.67% off).
|
||||
/// </summary>
|
||||
/// <param name="sar">The sample aspect ratio string in "N:D" format.</param>
|
||||
/// <returns><c>true</c> if the SAR is within 1% of 1:1; otherwise <c>false</c>.</returns>
|
||||
internal static bool IsNearSquarePixelSar(string sar)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sar))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var parts = sar.Split(':');
|
||||
if (parts.Length == 2
|
||||
&& double.TryParse(parts[0], CultureInfo.InvariantCulture, out var num)
|
||||
&& double.TryParse(parts[1], CultureInfo.InvariantCulture, out var den)
|
||||
&& den > 0)
|
||||
{
|
||||
return IsClose(num / den, 1.0, 0.01);
|
||||
}
|
||||
|
||||
return string.Equals(sar, "1:1", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a frame rate from a string value in ffprobe output
|
||||
/// This could be a number or in the format of 2997/125.
|
||||
|
||||
@@ -328,7 +328,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -431,22 +431,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
||||
else if (!File.Exists(outputPath))
|
||||
{
|
||||
failed = true;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Deleting converted subtitle due to failure: {Path}", outputPath);
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed)
|
||||
@@ -520,7 +507,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (File.Exists(outputPath) && _fileSystem.GetFileInfo(outputPath).Length > 0)
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
releaser.Dispose();
|
||||
continue;
|
||||
@@ -735,24 +722,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
foreach (var outputPath in outputPaths)
|
||||
{
|
||||
if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
_logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
|
||||
failed = true;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -791,7 +764,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
|
||||
|
||||
@@ -894,22 +867,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
||||
else if (!File.Exists(outputPath))
|
||||
{
|
||||
failed = true;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed)
|
||||
|
||||
@@ -324,7 +324,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
return !condition.IsRequired;
|
||||
}
|
||||
|
||||
var expected = Enum.Parse<TransportStreamTimestamp>(condition.Value, true);
|
||||
var expected = (TransportStreamTimestamp)Enum.Parse(typeof(TransportStreamTimestamp), condition.Value, true);
|
||||
|
||||
switch (condition.Condition)
|
||||
{
|
||||
|
||||
@@ -2009,7 +2009,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
}
|
||||
else if (condition.Condition == ProfileConditionType.NotEquals)
|
||||
{
|
||||
item.SetOption(qualifier, "rangetype", string.Join(',', Enum.GetNames<VideoRangeType>().Except(values)));
|
||||
item.SetOption(qualifier, "rangetype", string.Join(',', Enum.GetNames(typeof(VideoRangeType)).Except(values)));
|
||||
}
|
||||
else if (condition.Condition == ProfileConditionType.EqualsAny)
|
||||
{
|
||||
|
||||
@@ -895,7 +895,7 @@ public class StreamInfo
|
||||
|
||||
if (SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
sb.Append("/master.m3u8");
|
||||
sb.Append("/master.m3u8?");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -906,9 +906,9 @@ public class StreamInfo
|
||||
sb.Append('.');
|
||||
sb.Append(Container);
|
||||
}
|
||||
}
|
||||
|
||||
var queryStart = sb.Length;
|
||||
sb.Append('?');
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(DeviceProfileId))
|
||||
{
|
||||
@@ -1133,12 +1133,6 @@ public class StreamInfo
|
||||
sb.Append(query);
|
||||
}
|
||||
|
||||
// Replace the first '&' with '?' to form a valid query string.
|
||||
if (sb.Length > queryStart)
|
||||
{
|
||||
sb[queryStart] = '?';
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Extensions
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Orders <see cref="RemoteImageInfo"/> by requested language in descending order, then "en", then no language, over other non-matches.
|
||||
/// Orders <see cref="RemoteImageInfo"/> by requested language in descending order, prioritizing "en" over other non-matches.
|
||||
/// </summary>
|
||||
/// <param name="remoteImageInfos">The remote image infos.</param>
|
||||
/// <param name="requestedLanguage">The requested language for the images.</param>
|
||||
@@ -28,9 +28,9 @@ namespace MediaBrowser.Model.Extensions
|
||||
{
|
||||
// Image priority ordering:
|
||||
// - Images that match the requested language
|
||||
// - Images with no language
|
||||
// - TODO: Images that match the original language
|
||||
// - Images in English
|
||||
// - Images with no language
|
||||
// - Images that don't match the requested language
|
||||
|
||||
if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -38,12 +38,12 @@ namespace MediaBrowser.Model.Extensions
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.IsNullOrEmpty(i.Language))
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(i.Language))
|
||||
if (string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Providers.Books.Isbn
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class IsbnExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => "ISBN";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "ISBN";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Book;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Books.Isbn;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public class IsbnExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "ISBN";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item.TryGetProviderId("ISBN", out var externalId))
|
||||
{
|
||||
if (item is Book)
|
||||
{
|
||||
yield return $"https://search.worldcat.org/search?q=bn:{externalId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.ComicVine
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ComicVineExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => "Comic Vine";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "ComicVine";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Book;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.ComicVine;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public class ComicVineExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Comic Vine";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item.TryGetProviderId("ComicVine", out var externalId))
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case Person:
|
||||
case Book:
|
||||
yield return $"https://comicvine.gamespot.com/{externalId}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.ComicVine
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ComicVinePersonExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => "Comic Vine";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "ComicVine";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Person;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.GoogleBooks
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class GoogleBooksExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => "Google Books";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "GoogleBooks";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Book;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.GoogleBooks;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public class GoogleBooksExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name => "Google Books";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item.TryGetProviderId("GoogleBooks", out var externalId))
|
||||
{
|
||||
if (item is Book)
|
||||
{
|
||||
yield return $"https://books.google.com/books?id={externalId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user