Compare commits

..

1 Commits

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

View File

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

View File

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

View File

@@ -87,9 +87,13 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- 10.11.8
- 10.11.7
- 10.11.6
- 10.11.5
- 10.11.4
- 10.11.3
- 10.11.2
- 10.11.1
- 10.11.0
- Master
- Unstable
- Older*

View File

@@ -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
View File

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

View File

@@ -1,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
View File

@@ -0,0 +1,55 @@
name: OpenAPI Build
on:
pull_request:
permissions: {}
jobs:
openapi-head:
name: OpenAPI - HEAD
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-head
retention-days: 1
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
openapi-base:
name: OpenAPI - BASE
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-base
retention-days: 1
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json

View File

@@ -1,35 +1,91 @@
name: OpenAPI Publish
name: OpenAPI
on:
push:
branches:
- master
tags:
- 'v*'
workflow_run:
workflows: ["OpenAPI Build"]
types: [completed]
permissions: {}
jobs:
publish-openapi:
name: OpenAPI - Publish Artifact
uses: ./.github/workflows/openapi-generate.yml
openapi-head:
name: OpenAPI - HEAD
if: ${{ github.event_name == 'push' }}
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: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
openapi-diff:
permissions:
contents: read
with:
ref: ${{ github.sha }}
repository: ${{ github.repository }}
artifact: openapi-head
pull-requests: write
name: OpenAPI - Difference
if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
runs-on: ubuntu-latest
steps:
- name: Download openapi-head
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: openapi-head
path: openapi-head
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download openapi-base
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: openapi-base
path: openapi-base
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Detect OpenAPI changes
id: openapi-diff
uses: jellyfin/openapi-diff-action@9274f6bda9d01ab091942a4a8334baa53692e8a4 # v1.0.0
with:
old-spec: openapi-base/openapi.json
new-spec: openapi-head/openapi.json
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:
- publish-openapi
- openapi-head
steps:
- name: Set unstable dated version
id: version
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
@@ -80,17 +136,17 @@ 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:
- publish-openapi
- openapi-head
steps:
- name: Set version number
id: version
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

View File

@@ -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/"

View File

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

View File

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

View File

@@ -1,44 +0,0 @@
name: OpenAPI Generate
on:
workflow_call:
inputs:
ref:
required: true
type: string
repository:
required: true
type: string
artifact:
required: true
type: string
permissions:
contents: read
jobs:
main:
name: Main
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref }}
repository: ${{ inputs.repository }}
- name: Configure .NET
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.0.x'
- name: Create File
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter Jellyfin.Server.Integration.Tests.OpenApiSpecTests
- name: Upload Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ inputs.artifact }}
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
retention-days: 14
if-no-files-found: error

View File

@@ -1,80 +0,0 @@
name: OpenAPI Check
on:
pull_request:
jobs:
ancestor:
name: Common Ancestor
runs-on: ubuntu-latest
outputs:
base_ref: ${{ steps.ancestor.outputs.base_ref }}
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: Search History
id: ancestor
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git 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 }} HEAD)
echo "ref: ${ANCESTOR_REF}"
echo "base_ref=${ANCESTOR_REF}" >> "$GITHUB_OUTPUT"
head:
name: Head Artifact
uses: ./.github/workflows/openapi-generate.yml
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
artifact: openapi-head
base:
name: Base Artifact
uses: ./.github/workflows/openapi-generate.yml
needs:
- ancestor
with:
ref: ${{ needs.ancestor.outputs.base_ref }}
repository: ${{ github.event.pull_request.base.repo.full_name }}
artifact: openapi-base
diff:
name: Generate Report
runs-on: ubuntu-latest
needs:
- head
- base
steps:
- name: Download Head
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-head
path: openapi-head
- name: Download Base
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-base
path: openapi-base
- name: Detect Changes
id: openapi-diff
run: |
sed -i 's:allOf:oneOf:g' openapi-head/openapi.json
sed -i 's:allOf:oneOf:g' openapi-base/openapi.json
mkdir -p /tmp/openapi-report
mv openapi-head/openapi.json /tmp/openapi-report/head.json
mv openapi-base/openapi.json /tmp/openapi-report/base.json
docker run -v /tmp/openapi-report:/data openapitools/openapi-diff:2.1.6 /data/base.json /data/head.json --state -l ERROR --markdown /data/openapi-report.md
- name: Upload Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: openapi-report
path: /tmp/openapi-report/openapi-report.md

View File

@@ -1,59 +0,0 @@
name: OpenAPI Report
on:
workflow_run:
workflows:
- OpenAPI Check
types:
- completed
jobs:
metadata:
name: Generate Metadata
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
pr_number: ${{ steps.pr_number.outputs.pr_number }}
steps:
- name: Get Pull Request Number
id: pr_number
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
API_RESPONSE=$(gh pr list --repo "${GITHUB_REPOSITORY}" --search "${HEAD_SHA}" --state open --json number)
PR_NUMBER=$(echo "${API_RESPONSE}" | jq '.[0].number')
echo "repository: ${GITHUB_REPOSITORY}"
echo "sha: ${HEAD_SHA}"
echo "response: ${API_RESPONSE}"
echo "pr: ${PR_NUMBER}"
echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
comment:
name: Pull Request Comment
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
needs:
- metadata
permissions:
pull-requests: write
actions: read
contents: read
steps:
- name: Download OpenAPI Report
id: download_report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-report
path: openapi-report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Push Comment
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
with:
github-token: ${{ secrets.JF_BOT_TOKEN }}
file-path: ${{ steps.download_report.outputs.download-path }}/openapi-report.md
pr-number: ${{ needs.metadata.outputs.pr_number }}
comment-tag: openapi-report

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -164,7 +164,6 @@
- [XVicarious](https://github.com/XVicarious)
- [YouKnowBlom](https://github.com/YouKnowBlom)
- [ZachPhelan](https://github.com/ZachPhelan)
- [ZeusCraft10](https://github.com/ZeusCraft10)
- [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
@@ -288,4 +287,3 @@
- [Martin Reuter](https://github.com/reuterma24)
- [Michael McElroy](https://github.com/mcmcelro)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
- [DerMaddis](https://github.com/dermaddis)

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="8.0.2" />
<PackageVersion Include="AsyncKeyedLock" Version="8.0.0" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -13,7 +13,7 @@
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="8.0.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.4.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.12.0" />
<PackageVersion Include="TMDbLib" Version="3.0.0" />
<PackageVersion Include="z440.atl.core" Version="7.10.0" />
<PackageVersion Include="TMDbLib" Version="2.3.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />

View File

@@ -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",

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -1019,15 +1019,6 @@ namespace Emby.Server.Implementations.Dto
{
dto.AlbumId = albumParent.Id;
dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
if (albumParent.LUFS.HasValue)
{
// -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
dto.AlbumNormalizationGain = -18f - albumParent.LUFS;
}
else if (albumParent.NormalizationGain.HasValue)
{
dto.AlbumNormalizationGain = albumParent.NormalizationGain;
}
}
// if (options.ContainsField(ItemFields.MediaSourceCount))

View File

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

View File

@@ -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 };

View File

@@ -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",

View File

@@ -2289,7 +2289,7 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
return [];
return new List<Folder>();
}
return GetCollectionFoldersInternal(item, allUserRootChildren);

View File

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

View File

@@ -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)
{

View File

@@ -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."
}

View File

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

View File

@@ -15,7 +15,7 @@
"Favorites": "Любими",
"Folders": "Папки",
"Genres": "Жанрове",
"HeaderAlbumArtists": "Изпълнители на албума",
"HeaderAlbumArtists": "Изпълнители на албуми",
"HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители",

View File

@@ -63,8 +63,8 @@
"Photos": "Fotos",
"Playlists": "Llistes de reproducció",
"Plugin": "Complement",
"PluginInstalledWithName": "{0} s'ha instal·lat",
"PluginUninstalledWithName": "{0} s'ha desinstal·lat",
"PluginInstalledWithName": "{0} ha estat instal·lat",
"PluginUninstalledWithName": "S'ha instal·lat {0}",
"PluginUpdatedWithName": "S'ha actualitzat {0}",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
@@ -104,7 +104,7 @@
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja dels registres",
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneja la mediateca",
"TaskRefreshLibrary": "Escaneig de les mediateques",
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",

View File

@@ -19,7 +19,7 @@
"HeaderContinueWatching": "Weiterschauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblingsinterpreten",
"HeaderFavoriteEpisodes": "Lieblingsfolgen",
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
"HeaderFavoriteShows": "Lieblingsserien",
"HeaderFavoriteSongs": "Lieblingssongs",
"HeaderLiveTV": "Live TV",

View File

@@ -133,8 +133,8 @@
"TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
"TaskExtractMediaSegments": "Skaneeri meedialõike",
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.",
"TaskExtractMediaSegments": "Skaneeri meediasegmente",
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
"CleanupUserDataTask": "Puhasta kasutajaandmed",
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."

View File

@@ -14,9 +14,5 @@
"DeviceOnlineWithName": "{0} er sambundið",
"Favorites": "Yndis",
"Folders": "Mappur",
"Forced": "Kravt",
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
"HeaderFavoriteEpisodes": "Yndispartar",
"HeaderFavoriteSongs": "Yndissangir",
"LabelIpAddressValue": "IP atsetur: {0}"
"Forced": "Kravt"
}

View File

@@ -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": "סרטונים ביתיים"
}
{}

View File

@@ -127,7 +127,7 @@
"TaskRefreshTrickplayImages": "ट्रिकप्लै चित्रों को सृजन करे",
"TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे.",
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
@@ -136,5 +136,5 @@
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य"
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
}

View File

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

View File

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

View File

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

View File

@@ -9,46 +9,46 @@
"Artists": "არტისტი",
"AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია",
"Books": "წიგნები",
"Forced": "იძულებით",
"Forced": "ძალით",
"Inherit": "მემკვიდრეობით",
"Latest": "უახლესი",
"Movies": "ფილმები",
"Music": "მუსიკა",
"Photos": "ფოტოები",
"Playlists": "დასაკრავი სიები",
"Plugin": "მოდული",
"Plugin": "დამატება",
"Shows": "სერიალები",
"Songs": "სიმღერები",
"Sync": "სინქრონიზაცია",
"System": "სისტემა",
"Undefined": "განუსაზღვრელი",
"Undefined": "აღუწერელი",
"User": "მომხმარებელი",
"TasksMaintenanceCategory": "რემონტი",
"TasksLibraryCategory": "ბიბლიოთეკა",
"ChapterNameValue": "თავი {0}",
"HeaderContinueWatching": "ყურების გაგრძელება",
"HeaderFavoriteArtists": "რჩეული შემსრულებლები",
"DeviceOfflineWithName": "{0} გამოეთიშა",
"DeviceOfflineWithName": "{0} გათიშა",
"External": "გარე",
"HeaderFavoriteEpisodes": "რჩეული ეპიზოდები",
"HeaderFavoriteSongs": "რჩეული სიმღერები",
"HeaderRecordingGroups": "ჩამწერი ჯგუფები",
"HearingImpaired": "სმენადაქვეითებული",
"LabelRunningTimeValue": "ხანგრძლივობა: {0}",
"LabelRunningTimeValue": "გაშვებულობის დრო: {0}",
"MessageApplicationUpdatedTo": "Jellyfin-ის სერვერი განახლდა {0}-ზე",
"MessageNamedServerConfigurationUpdatedWithValue": "სერვერის კონფიგურაციის სექცია {0} განახლდა",
"MixedContent": "შერეული შემცველობა",
"MusicVideos": "მუსიკალური ვიდეოები",
"MusicVideos": "მუსიკი ვიდეოები",
"NotificationOptionInstallationFailed": "დაყენების შეცდომა",
"NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია",
"NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია",
"NotificationOptionCameraImageUploaded": "კამერის გამოსახულება ატვირთულია",
"NotificationOptionVideoPlaybackStopped": "ვიდეოს დაკვრა გაჩერებულია",
"PluginUninstalledWithName": "{0} წაიშალა",
"ScheduledTaskStartedWithName": "{0} დაიწყო",
"ScheduledTaskStartedWithName": "{0} გაეშვა",
"VersionNumber": "ვერსია {0}",
"TasksChannelsCategory": "ინტერნეტ-არხები",
"ValueSpecialEpisodeName": "დამატებითი - {0}",
"ValueSpecialEpisodeName": "სპეციალური - {0}",
"TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.",
"Channels": "არხები",
"Collections": "კოლექციები",
@@ -56,31 +56,31 @@
"Favorites": "რჩეულები",
"Folders": "საქაღალდეები",
"HeaderFavoriteShows": "რჩეული სერიალები",
"HeaderLiveTV": "ლაივ ტელევიზია",
"HeaderNextUp": "შემდეგი",
"HeaderLiveTV": "ცოცხალი TV",
"HeaderNextUp": "შემდეგი ზემოთ",
"HomeVideos": "სახლის ვიდეოები",
"NameSeasonNumber": "სეზონი {0}",
"NameSeasonUnknown": "სეზონი უცნობია",
"NotificationOptionPluginError": "მოდულის შეცდომა",
"NotificationOptionPluginInstalled": "მოდული დაყენებულია",
"NotificationOptionPluginUninstalled": "მოდული წაიშალა",
"NotificationOptionPluginError": "დამატების შეცდომა",
"NotificationOptionPluginInstalled": "დამატება დაყენებულია",
"NotificationOptionPluginUninstalled": "დამატება წაიშალა",
"ProviderValue": "მომწოდებელი: {0}",
"ScheduledTaskFailedWithName": "{0} ვერ შესრულა",
"TvShows": "სატელევიზიო სერიალები",
"ScheduledTaskFailedWithName": "{0} ავარიულა",
"TvShows": "TV სერიალები",
"TaskRefreshPeople": "ხალხის განახლება",
"TaskUpdatePlugins": "მოდულების განახლება",
"TaskUpdatePlugins": "დამატებების განახლება",
"TaskRefreshChannels": "არხების განახლება",
"TaskOptimizeDatabase": "მონაცემთა ბაზის ოპტიმიზაცია",
"TaskOptimizeDatabase": "ბაზების ოპტიმიზაცია",
"TaskKeyframeExtractor": "საკვანძო კადრის გამომღები",
"DeviceOnlineWithName": "{0} დაკავშირდა",
"DeviceOnlineWithName": "{0} შეერთებულია",
"LabelIpAddressValue": "IP მისამართი: {0}",
"NameInstallFailed": "{0}-ის დაყენების შეცდომა",
"NotificationOptionApplicationUpdateAvailable": "ხელმისაწვდომია აპლიკაციის განახლება",
"NotificationOptionAudioPlaybackStopped": "აუდიოს დაკვრა გაჩერებულია",
"NotificationOptionNewLibraryContent": "ახალი შემცველობა დამატებულია",
"NotificationOptionPluginUpdateInstalled": "მოდულიs განახლება დაყენებულია",
"NotificationOptionServerRestartRequired": "საჭიროა სერვერის გადატვირთვა",
"NotificationOptionTaskFailed": "გეგმიური დავალების შეცდომა",
"NotificationOptionPluginUpdateInstalled": "დამატების განახლება დაყენებულია",
"NotificationOptionServerRestartRequired": "სერვერის გადატვირთვა აუცილებელია",
"NotificationOptionTaskFailed": "დაგეგმილი ამოცანის შეცდომა",
"NotificationOptionUserLockedOut": "მომხმარებელი დაიბლოკა",
"NotificationOptionVideoPlayback": "ვიდეოს დაკვრა დაწყებულია",
"PluginInstalledWithName": "{0} დაყენებულია",
@@ -91,51 +91,39 @@
"TaskRefreshLibrary": "მედიის ბიბლიოთეკის სკანირება",
"TaskCleanLogs": "ჟურნალის საქაღალდის გასუფთავება",
"TaskCleanTranscode": "ტრანსკოდირების საქაღალდის გასუფთავება",
"TaskDownloadMissingSubtitles": "მიუწვდომელი სუბტიტრების გადმოწერა",
"UserDownloadingItemWithValues": "{0} -ი {1}-ს იწერს",
"FailedLoginAttemptWithUserName": "შესვლის წარუმატებელი მცდელობა {0}-დან",
"TaskDownloadMissingSubtitles": "ნაკლული სუბტიტრების გადმოწერა",
"UserDownloadingItemWithValues": "{0} -ი {0}-ს იწერს",
"FailedLoginAttemptWithUserName": "{0}-დან შემოსვლის შეცდომა",
"MessageApplicationUpdated": "Jellyfin-ის სერვერი განახლდა",
"MessageServerConfigurationUpdated": "სერვერის კონფიგურაცია განახლდა",
"ServerNameNeedsToBeRestarted": "საჭიროა {0}-ის გადატვირთვა",
"UserCreatedWithName": "მომხმარებელი {0} შეიქმნა",
"UserDeletedWithName": "მომხმარებელი {0} წაშლილია",
"UserOnlineFromDevice": "{0}-ი დაკავშირდა {1}-დან",
"UserOfflineFromDevice": "{0}-ი {1}-დან გათიშა",
"UserOnlineFromDevice": "{0}-ი ხაზზეა {1}-დან",
"UserOfflineFromDevice": "{0}-ი {1}-დან გათიშა",
"ItemAddedWithName": "{0} ჩამატებულია ბიბლიოთეკაში",
"ItemRemovedWithName": "{0} წაშლილია ბიბლიოთეკიდან",
"UserLockedOutWithName": "მომხმარებელი {0} დაბლოკილია",
"UserStartedPlayingItemWithValues": "{0} უყურებს {1}-ს {2}-ზე",
"UserPasswordChangedWithName": "მომხმარებლი {0}-სთვის პაროლი შეცვლა",
"UserStartedPlayingItemWithValues": "{0} თამაშობს {1}-ს {2}-ზე",
"UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია",
"UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა",
"UserStoppedPlayingItemWithValues": "{0}-მა დაასრულა {1}-ის ყურება {2}-ზე",
"UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე",
"TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.",
"TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.",
"NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.",
"CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან",
"StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.",
"SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერა ვერ შესრულდა",
"SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა",
"ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას",
"TaskCleanActivityLogDescription": "შლის მითითებულ ასაკზე ძველ ჟურნალის ჩანაწერებ.",
"TaskCleanCacheDescription": "შლის სისტემისთვის არასაჭირო ქეშის ფაილებ.",
"TaskRefreshLibraryDescription": "ეძებს ახალ ფაილებს თქვენ მედიის ბიბლიოთეკაში და ანახლებს მეტამონაცემებ.",
"TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველ ჟურნალის ჩანაწერების წაშლა.",
"TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.",
"TaskRefreshLibraryDescription": "თქვენ მედი ბიბლიოთეკაში ახალი ფაილებ ძებნა და მეტამონაცემების განახლება.",
"TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.",
"TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.",
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული მოდულების განახლებების გადმოწერა და დაყენება.",
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
"TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
"TaskDownloadMissingSubtitlesDescription": "ეძებს ბიბლიოთეკაში მიუწვდომელ სუბტიტრებს ინტერნეტში მეტამონაცემებზე დაყრდნობით.",
"TaskOptimizeDatabaseDescription": "კუმშავს მონაცემთა ბაზას ადგილის გათავისუფლებლად. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის დაშვებულ ბიბლიოთეკებში.",
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება",
"TaskAudioNormalization": "აუდიოს ნორმალიზება",
"TaskAudioNormalizationDescription": "აანალიზებს ფაილებს აუდიოს ნორმალიზაციისთვის.",
"TaskDownloadMissingLyrics": "მიუწვდომელი ლირიკების ჩამოტვირთვა",
"TaskDownloadMissingLyricsDescription": "ჩამოტვირთავს ამჟამად ბიბლიოთეკაში არარსებულ ლირიკებს სიმღერებისთვის",
"TaskCleanCollectionsAndPlaylists": "კოლექციების და დასაკრავი სიების გასუფთავება",
"TaskCleanCollectionsAndPlaylistsDescription": "შლის არარსებულ ერთეულებს კოლექციებიდან და დასაკრავი სიებიდან.",
"TaskExtractMediaSegments": "მედია სეგმენტების სკანირება",
"TaskExtractMediaSegmentsDescription": "მედია სეგმენტების სკანირება მხარდაჭერილი მოდულებისთვის.",
"TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია",
"TaskMoveTrickplayImagesDescription": "გადააქვს trickplay ფაილები ბიბლიოთეკის პარამეტრებზე დაყრდნობით.",
"CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავება",
"CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ."
"TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრებძებნა.",
"TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლებ. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.",
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება"
}

View File

@@ -1,4 +1,5 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}",
"Application": "Applicatie",
"Artists": "Artiesten",
@@ -13,18 +14,19 @@
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"Favorites": "Favorieten",
"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": "Overnemen",
"Inherit": "Erven",
"ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
"ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
"LabelIpAddressValue": "IP-adres: {0}",
@@ -114,7 +116,7 @@
"TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd",
"Forced": "Geforceerd",
"Forced": "Gedwongen",
"Default": "Standaard",
"TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.",
"TaskOptimizeDatabase": "Database optimaliseren",
@@ -135,7 +137,5 @@
"TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.",
"TaskExtractMediaSegments": "Scannen op mediasegmenten",
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.",
"CleanupUserDataTask": "Opruimtaak gebruikersdata",
"Albums": "Albums",
"Genres": "Genres"
"CleanupUserDataTask": "Opruimtaak gebruikersdata"
}

View File

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

View File

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

View File

@@ -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",

View File

@@ -124,17 +124,17 @@
"TaskKeyframeExtractor": "Екстрактор ключових кадрів",
"External": "Зовнішній",
"HearingImpaired": "З порушеннями слуху",
"TaskRefreshTrickplayImagesDescription": "Створює прев'ю-зображення для відео у ввімкнених медіатеках.",
"TaskRefreshTrickplayImages": "Створити Прев'ю-зображення",
"TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
"TaskRefreshTrickplayImages": "Створити Trickplay-зображення",
"TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення",
"TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.",
"TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.",
"TaskAudioNormalization": "Нормалізація аудіо",
"TaskDownloadMissingLyrics": "Завантажити відсутні тексти пісень",
"TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень",
"TaskMoveTrickplayImagesDescription": "Переміщує наявні прев'ю-зображення відповідно до налаштувань медіатеки.",
"TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.",
"TaskExtractMediaSegments": "Сканування медіа-сегментів",
"TaskMoveTrickplayImages": "Змінити місце розташування прев'ю-зображень",
"TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень",
"TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.",
"CleanupUserDataTask": "Завдання очищення даних користувача",
"CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому."

View File

@@ -1,141 +1,141 @@
{
"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} 登入嘗試失敗",
"Favorites": "心水",
"DeviceOfflineWithName": "{0} 已中斷連接",
"DeviceOnlineWithName": "{0} 已連接",
"FailedLoginAttemptWithUserName": "{0} 登入失敗",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯歌手",
"HeaderContinueWatching": "繼續睇返",
"HeaderFavoriteAlbums": "心水嘅專輯",
"HeaderFavoriteArtists": "心水嘅藝人",
"HeaderFavoriteEpisodes": "心水嘅劇集",
"HeaderFavoriteShows": "心水嘅節目",
"HeaderFavoriteSongs": "心水嘅歌曲",
"HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛的專輯",
"HeaderFavoriteArtists": "最愛的藝人",
"HeaderFavoriteEpisodes": "最愛的劇集",
"HeaderFavoriteShows": "最愛的節目",
"HeaderFavoriteSongs": "最愛的歌曲",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "跟住落嚟",
"HeaderNextUp": "繼續觀看",
"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": "音樂",
"MusicVideos": "MV",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季度",
"NewVersionIsAvailable": "有新版本 Jellyfin 可下載。",
"NotificationOptionApplicationUpdateAvailable": "有得更新應用程式",
"NotificationOptionApplicationUpdateInstalled": "應用程式更新好咗",
"NotificationOptionAudioPlayback": "開始播放音訊",
"NotificationOptionAudioPlaybackStopped": "停播放音訊",
"NotificationOptionCameraImageUploaded": "相機相片上載咗",
"NameSeasonUnknown": "未知季度",
"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": "壓縮數據櫃並釋放剩餘空間。喺掃描媒體櫃或者做咗一啲會修改數據櫃嘅操作之後行呢個任務,或者可以提升效能。",
"TaskOptimizeDatabase": "最佳化數據",
"TaskCleanActivityLogDescription": "刪走超過設定日期嘅活動記錄。",
"TaskKeyframeExtractorDescription": "提取關鍵影格Keyframe建立更準確 HLS 播放列表。呢個任務可能要行好耐。",
"Default": "預設",
"TaskOptimizeDatabaseDescription": "壓縮數據庫及釋放可用空間。完成任何會修改數據庫的工作(例如掃描媒體庫)後,執行此工作或可提升伺服器速度。",
"TaskOptimizeDatabase": "最佳化數據",
"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 日媒體相關資料。"
}

View File

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

View File

@@ -960,7 +960,7 @@ namespace Emby.Server.Implementations.Session
}
var tracksChanged = UpdatePlaybackSettings(user, info, data);
if (tracksChanged)
if (!tracksChanged)
{
changed = true;
}

View File

@@ -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)
{

View File

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

View File

@@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStream(
[FromRoute, Required] Guid itemId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -112,7 +112,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -131,8 +131,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -255,18 +255,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStreamByContainer(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -276,7 +276,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -295,8 +295,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,

View File

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

View File

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

View File

@@ -166,18 +166,18 @@ public class DynamicHlsController : BaseJellyfinApiController
[ProducesPlaylistFile]
public async Task<ActionResult> GetLiveHlsStream(
[FromRoute, Required] Guid itemId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -187,7 +187,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -206,8 +206,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -412,12 +412,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -427,7 +427,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -448,8 +448,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -585,12 +585,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -601,7 +601,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -620,8 +620,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -752,12 +752,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -767,7 +767,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -788,8 +788,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -921,12 +921,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -937,7 +937,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -956,8 +956,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1091,7 +1091,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery, Required] long runtimeTicks,
[FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
@@ -1099,12 +1099,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1114,7 +1114,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -1135,8 +1135,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1273,7 +1273,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery, Required] long runtimeTicks,
[FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
@@ -1281,12 +1281,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1297,7 +1297,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -1316,8 +1316,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -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));

View File

@@ -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());
}
}

View File

@@ -11,7 +11,6 @@ using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
@@ -271,17 +270,15 @@ public class ItemsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var item = _libraryManager.GetParentItem(parentId, userId);
QueryResult<BaseItem> result;
if (includeItemTypes.Length == 1
&& includeItemTypes[0] == BaseItemKind.BoxSet
&& item is not BoxSet)
&& includeItemTypes[0] == BaseItemKind.BoxSet)
{
parentId = null;
item = _libraryManager.GetUserRootFolder();
}
var item = _libraryManager.GetParentItem(parentId, userId);
QueryResult<BaseItem> result;
if (item is not Folder folder)
{
folder = _libraryManager.GetUserRootFolder();
@@ -389,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)

View File

@@ -454,7 +454,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Tuners/{tunerId}/Reset")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.RequiresElevation)]
[Authorize(Policy = Policies.LiveTvManagement)]
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
{
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
@@ -976,7 +976,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created tuner host returned.</response>
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
[HttpPost("TunerHosts")]
[Authorize(Policy = Policies.RequiresElevation)]
[Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
=> await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
@@ -988,7 +988,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Tuner host deleted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("TunerHosts")]
[Authorize(Policy = Policies.RequiresElevation)]
[Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteTunerHost([FromQuery] string? id)
{
@@ -1021,7 +1021,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created listings provider returned.</response>
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
[HttpPost("ListingProviders")]
[Authorize(Policy = Policies.RequiresElevation)]
[Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
@@ -1047,7 +1047,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Listing provider deleted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("ListingProviders")]
[Authorize(Policy = Policies.RequiresElevation)]
[Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteListingProvider([FromQuery] string? id)
{
@@ -1080,7 +1080,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Available countries returned.</response>
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
[Authorize(Policy = Policies.RequiresElevation)]
[Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile(MediaTypeNames.Application.Json)]
public async Task<ActionResult> GetSchedulesDirectCountries()
@@ -1101,7 +1101,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Channel mapping options returned.</response>
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
[HttpGet("ChannelMappingOptions")]
[Authorize(Policy = Policies.RequiresElevation)]
[Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId)
=> _listingsManager.GetChannelMappingOptions(providerId);
@@ -1113,7 +1113,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created channel mapping returned.</response>
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
[HttpPost("ChannelMappings")]
[Authorize(Policy = Policies.RequiresElevation)]
[Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto)
=> _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId);
@@ -1137,7 +1137,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
[HttpGet("Tuners/Discover")]
[Authorize(Policy = Policies.RequiresElevation)]
[Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public IAsyncEnumerable<TunerHostInfo> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
=> _tunerHostManager.DiscoverTuners(newDevicesOnly);
@@ -1185,7 +1185,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesVideoFile]
public ActionResult GetLiveStreamFile(
[FromRoute, Required] string streamId,
[FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container)
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container)
{
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
if (liveStreamInfo is null)

View File

@@ -47,7 +47,6 @@ public class PersonsController : BaseJellyfinApiController
/// <summary>
/// Gets all persons.
/// </summary>
/// <param name="startIndex">Optional. All items with a lower index will be dropped from the response.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
@@ -58,7 +57,6 @@ public class PersonsController : BaseJellyfinApiController
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
/// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
/// <param name="parentId">Optional. Specify this to localize the search to a specific library. Omit to use the root.</param>
/// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
/// <param name="userId">User id.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
@@ -67,7 +65,6 @@ public class PersonsController : BaseJellyfinApiController
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetPersons(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
@@ -78,7 +75,6 @@ public class PersonsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] excludePersonTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
[FromQuery] Guid? parentId,
[FromQuery] Guid? appearsInItemId,
[FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true)
@@ -100,8 +96,6 @@ public class PersonsController : BaseJellyfinApiController
User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
AppearsInItemId = appearsInItemId ?? Guid.Empty,
ParentId = parentId,
StartIndex = startIndex,
Limit = limit ?? 0
});

View File

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

View File

@@ -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

View File

@@ -58,7 +58,7 @@ public class SyncPlayController : BaseJellyfinApiController
[FromBody, Required] NewGroupRequestDto requestData)
{
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var syncPlayRequest = new NewGroupRequest(requestData.GroupName.Trim());
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None));
}

View File

@@ -101,13 +101,13 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer,
[FromQuery] MediaStreamProtocol? transcodingProtocol,
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,

View File

@@ -313,18 +313,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public async Task<ActionResult> GetVideoStream(
[FromRoute, Required] Guid itemId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -334,7 +334,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -355,8 +355,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -551,18 +551,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public Task<ActionResult> GetVideoStreamByContainer(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -572,7 +572,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -593,8 +593,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -17,7 +17,9 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Api.Helpers;
@@ -266,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
@@ -420,18 +422,14 @@ public static class StreamingHelpers
request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
break;
case 4:
if (videoRequest is not null && IsValidCodecName(val))
if (videoRequest is not null)
{
videoRequest.VideoCodec = val;
}
break;
case 5:
if (IsValidCodecName(val))
{
request.AudioCodec = val;
}
request.AudioCodec = val;
break;
case 6:
if (videoRequest is not null)
@@ -485,7 +483,7 @@ public static class StreamingHelpers
request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
break;
case 15:
if (videoRequest is not null && EncodingHelper.LevelValidationRegex().IsMatch(val))
if (videoRequest is not null)
{
videoRequest.Level = val;
}
@@ -506,7 +504,7 @@ public static class StreamingHelpers
break;
case 18:
if (videoRequest is not null && IsValidCodecName(val))
if (videoRequest is not null)
{
videoRequest.Profile = val;
}
@@ -565,11 +563,7 @@ public static class StreamingHelpers
break;
case 30:
if (IsValidCodecName(val))
{
request.SubtitleCodec = val;
}
request.SubtitleCodec = val;
break;
case 31:
if (videoRequest is not null)
@@ -592,11 +586,6 @@ public static class StreamingHelpers
}
}
private static bool IsValidCodecName(string val)
{
return EncodingHelper.ContainerValidationRegex().IsMatch(val);
}
/// <summary>
/// Parses the container into its file extension.
/// </summary>

View File

@@ -1,5 +1,3 @@
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Api.Models.SyncPlayDtos;
/// <summary>
@@ -19,6 +17,5 @@ public class NewGroupRequestDto
/// Gets or sets the group name.
/// </summary>
/// <value>The name of the new group.</value>
[StringLength(200, ErrorMessage = "Group name must not exceed 200 characters.")]
public string GroupName { get; set; }
}

View File

@@ -185,7 +185,7 @@ public static class UserEntityExtensions
entity.Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true));
entity.Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true));
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true));
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, false));
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true));
entity.Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true));
entity.Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
entity.Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));

View File

@@ -118,21 +118,15 @@ public class BackupService : IBackupService
throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
}
void CopyDirectory(string source, string target, string[]? exclude = null)
void CopyDirectory(string source, string target)
{
var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
var excludePaths = exclude?.Select(e => $"{source}/{e}/").ToArray();
foreach (var item in zipArchive.Entries)
{
var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
if (excludePaths is not null && excludePaths.Any(e => item.FullName.StartsWith(e, StringComparison.Ordinal)))
{
continue;
}
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|| Path.EndsInDirectorySeparator(item.FullName))
@@ -148,10 +142,8 @@ public class BackupService : IBackupService
}
CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
CopyDirectory("Data", _applicationPaths.DataPath, exclude: ["metadata", "metadata-default"]);
CopyDirectory("Data", _applicationPaths.DataPath);
CopyDirectory("Root", _applicationPaths.RootFolderPath);
CopyDirectory("Data/metadata", _applicationPaths.InternalMetadataPath);
CopyDirectory("Data/metadata-default", _applicationPaths.DefaultInternalMetadataPath);
if (manifest.Options.Database)
{
@@ -412,15 +404,6 @@ public class BackupService : IBackupService
if (backupOptions.Metadata)
{
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
// If a custom metadata path is configured, the default location may still contain data.
if (!string.Equals(
Path.GetFullPath(_applicationPaths.DefaultInternalMetadataPath),
Path.GetFullPath(_applicationPaths.InternalMetadataPath),
StringComparison.OrdinalIgnoreCase))
{
CopyDirectory(Path.Combine(_applicationPaths.DefaultInternalMetadataPath), Path.Combine("Data", "metadata-default"));
}
}
var manifestStream = await zipArchive.CreateEntry(ManifestEntryName).OpenAsync().ConfigureAwait(false);

View File

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

View File

@@ -62,11 +62,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct();
if (filter.StartIndex.HasValue && filter.StartIndex > 0)
{
dbQuery = dbQuery.Skip(filter.StartIndex.Value);
}
// dbQuery = dbQuery.OrderBy(e => e.ListOrder);
if (filter.Limit > 0)
{
dbQuery = dbQuery.Take(filter.Limit);
@@ -78,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
@@ -201,11 +196,6 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
}
if (filter.ParentId != null)
{
query = query.Where(e => e.BaseItems!.Any(w => context.AncestorIds.Any(i => i.ParentItemId == filter.ParentId && i.ItemId == w.ItemId)));
}
if (!filter.AppearsInItemId.IsEmpty())
{
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId)));

View File

@@ -182,18 +182,6 @@ public class MediaSegmentManager : IMediaSegmentManager
/// <inheritdoc />
public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken)
{
foreach (var provider in _segmentProviders)
{
try
{
await provider.CleanupExtractedData(itemId, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Provider {ProviderName} failed to clean up extracted data for item {ItemId}", provider.Name, itemId);
}
}
var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{

View File

@@ -399,72 +399,64 @@ public class TrickplayManager : ITrickplayManager
var workDir = Path.Combine(_appPaths.TempDirectory, "trickplay_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
try
var trickplayInfo = new TrickplayInfo
{
var trickplayInfo = new TrickplayInfo
Width = width,
Interval = options.Interval,
TileWidth = options.TileWidth,
TileHeight = options.TileHeight,
ThumbnailCount = images.Count,
// Set during image generation
Height = 0,
Bandwidth = 0
};
/*
* Generate trickplay tiles from sets of thumbnails
*/
var imageOptions = new ImageCollageOptions
{
Width = trickplayInfo.TileWidth,
Height = trickplayInfo.TileHeight
};
var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
for (int i = 0; i < requiredTiles; i++)
{
// Set output/input paths
var tilePath = Path.Combine(workDir, $"{i}.jpg");
imageOptions.OutputPath = tilePath;
imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList();
// Generate image and use returned height for tiles info
var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
if (trickplayInfo.Height == 0)
{
Width = width,
Interval = options.Interval,
TileWidth = options.TileWidth,
TileHeight = options.TileHeight,
ThumbnailCount = images.Count,
// Set during image generation
Height = 0,
Bandwidth = 0
};
/*
* Generate trickplay tiles from sets of thumbnails
*/
var imageOptions = new ImageCollageOptions
{
Width = trickplayInfo.TileWidth,
Height = trickplayInfo.TileHeight
};
var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
for (int i = 0; i < requiredTiles; i++)
{
// Set output/input paths
var tilePath = Path.Combine(workDir, $"{i}.jpg");
imageOptions.OutputPath = tilePath;
imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList();
// Generate image and use returned height for tiles info
var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
if (trickplayInfo.Height == 0)
{
trickplayInfo.Height = height;
}
// Update bitrate
var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
trickplayInfo.Height = height;
}
/*
* Move trickplay tiles to output directory
*/
Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
// Replace existing tiles if they already exist
if (Directory.Exists(outputDir))
{
Directory.Delete(outputDir, true);
}
_fileSystem.MoveDirectory(workDir, outputDir);
return trickplayInfo;
// Update bitrate
var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
}
catch
/*
* Move trickplay tiles to output directory
*/
Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
// Replace existing tiles if they already exist
if (Directory.Exists(outputDir))
{
Directory.Delete(workDir, true);
throw;
Directory.Delete(outputDir, true);
}
_fileSystem.MoveDirectory(workDir, outputDir);
return trickplayInfo;
}
private bool CanGenerateTrickplay(Video video, int interval)

View File

@@ -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
{

View File

@@ -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"
});
}
}

View File

@@ -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
{

View File

@@ -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)
{

View File

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

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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"
}
}

View File

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

View File

@@ -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;
}
}
}
}
}

View File

@@ -1,105 +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.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;
}
}

View File

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

View File

@@ -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,

View File

@@ -154,6 +154,11 @@ namespace MediaBrowser.Controller.Entities.Audio
return "Artist-" + (Name ?? string.Empty).RemoveDiacritics();
}
protected override bool GetBlockUnratedValue(User user)
{
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()
{
return UnratedItem.Music;

View File

@@ -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;
@@ -1171,18 +1172,11 @@ namespace MediaBrowser.Controller.Entities
info.Video3DFormat = video.Video3DFormat;
info.Timestamp = video.Timestamp;
if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
if (video.IsShortcut)
{
var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
// Only allow remote shortcut paths — local file paths in .strm files
// could be used to read arbitrary files from the server.
if (shortcutProtocol != MediaProtocol.File)
{
info.IsRemote = true;
info.Path = video.ShortcutPath;
info.Protocol = shortcutProtocol;
}
info.IsRemote = true;
info.Path = video.ShortcutPath;
info.Protocol = MediaSourceManager.GetPathProtocol(info.Path);
}
if (string.IsNullOrEmpty(info.Container))
@@ -1607,10 +1601,11 @@ namespace MediaBrowser.Controller.Entities
if (string.IsNullOrEmpty(rating))
{
Logger.LogDebug("{0} has no parental rating set.", Name);
return !GetBlockUnratedValue(user);
}
var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
var ratingScore = LocalizationManager.GetRatingScore(rating);
// Could not determine rating level
if (ratingScore is null)
@@ -1652,7 +1647,7 @@ namespace MediaBrowser.Controller.Entities
return null;
}
return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
return LocalizationManager.GetRatingScore(rating);
}
public List<string> GetInheritedTags()
@@ -2134,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);
}
@@ -2615,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);

View File

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

View File

@@ -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;
}
}
}
}
}

View File

@@ -21,8 +21,6 @@ namespace MediaBrowser.Controller.Entities
ExcludePersonTypes = excludePersonTypes;
}
public int? StartIndex { get; set; }
/// <summary>
/// Gets or sets the maximum number of items the query should return.
/// </summary>
@@ -30,8 +28,6 @@ namespace MediaBrowser.Controller.Entities
public Guid ItemId { get; set; }
public Guid? ParentId { get; set; }
public IReadOnlyList<string> PersonTypes { get; }
public IReadOnlyList<string> ExcludePersonTypes { get; }

View File

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

View File

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

View File

@@ -33,18 +33,18 @@ namespace MediaBrowser.Controller.MediaEncoding
public partial class EncodingHelper
{
/// <summary>
/// The codec validation regex string.
/// The codec validation regex.
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
/// This should matches all common valid codecs.
/// </summary>
public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
/// <summary>
/// The level validation regex string.
/// The level validation regex.
/// This regular expression matches strings representing a double.
/// </summary>
public const string LevelValidationRegexStr = @"-?[0-9]+(?:\.[0-9]+)?";
public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
private const string _defaultMjpegEncoder = "mjpeg";
@@ -85,7 +85,8 @@ 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);
private static readonly string[] _videoProfilesH264 =
[
@@ -179,22 +180,6 @@ namespace MediaBrowser.Controller.MediaEncoding
RemoveHdr10Plus,
}
/// <summary>
/// The codec validation regex.
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
/// This should matches all common valid codecs.
/// </summary>
[GeneratedRegex(ContainerValidationRegexStr)]
public static partial Regex ContainerValidationRegex();
/// <summary>
/// The level validation regex string.
/// This regular expression matches strings representing a double.
/// </summary>
[GeneratedRegex(LevelValidationRegexStr)]
public static partial Regex LevelValidationRegex();
[GeneratedRegex(@"\s+")]
private static partial Regex WhiteSpaceRegex();
@@ -491,7 +476,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetMjpegEncoder(state, encodingOptions);
}
if (ContainerValidationRegex().IsMatch(codec))
if (_containerValidationRegex.IsMatch(codec))
{
return codec.ToLowerInvariant();
}
@@ -532,7 +517,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string GetInputFormat(string container)
{
if (string.IsNullOrEmpty(container) || !ContainerValidationRegex().IsMatch(container))
if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
{
return null;
}
@@ -750,7 +735,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = state.OutputAudioCodec;
if (!ContainerValidationRegex().IsMatch(codec))
if (!_containerValidationRegex.IsMatch(codec))
{
codec = "aac";
}
@@ -1282,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))
@@ -1581,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))
{
@@ -1619,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)
@@ -1804,40 +1768,38 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
{
if (!double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
{
return null;
}
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.3 (15) and lower for maximum compatibility.
// https://en.wikipedia.org/wiki/AV1#Levels
if (requestLevel < 0 || requestLevel >= 15)
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
{
return "15";
// Transcode to level 5.3 (15) and lower for maximum compatibility.
// https://en.wikipedia.org/wiki/AV1#Levels
if (requestLevel < 0 || requestLevel >= 15)
{
return "15";
}
}
}
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.0 and lower for maximum compatibility.
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
if (requestLevel < 0 || requestLevel >= 150)
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
{
return "150";
// Transcode to level 5.0 and lower for maximum compatibility.
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
if (requestLevel < 0 || requestLevel >= 150)
{
return "150";
}
}
}
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.1 and lower for maximum compatibility.
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
if (requestLevel < 0 || requestLevel >= 51)
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
return "51";
// Transcode to level 5.1 and lower for maximum compatibility.
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
if (requestLevel < 0 || requestLevel >= 51)
{
return "51";
}
}
}
@@ -2227,10 +2189,12 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
var level = NormalizeTranscodingLevel(state, state.GetRequestedLevel(targetVideoCodec));
var level = state.GetRequestedLevel(targetVideoCodec);
if (!string.IsNullOrEmpty(level))
{
level = NormalizeTranscodingLevel(state, level);
// libx264, QSV, AMF can adjust the given level to match the output.
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
@@ -2628,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)
@@ -6403,15 +6359,17 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
&& ((videoStream.Profile?.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) ?? false)
|| (videoStream.Profile?.Contains("4:4:4", StringComparison.OrdinalIgnoreCase) ?? false)))
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
{
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase)
|| videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase))
{
return null;
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
{
return null;
}
}
}
@@ -7165,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,
@@ -7182,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;
@@ -7210,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);
@@ -7254,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
@@ -7268,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>();

Some files were not shown because too many files have changed in this diff Show More