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
600 changed files with 9894 additions and 44316 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.8",
"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

@@ -8,10 +8,6 @@ on:
schedule:
- cron: '24 2 * * 4'
permissions:
contents: read
security-events: write
jobs:
analyze:
name: Analyze
@@ -27,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@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10
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
@@ -36,7 +36,7 @@ jobs:
rename:
name: Rename
if: contains(github.event.comment.body, '@jellyfin-bot rename')
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'
runs-on: ubuntu-latest
steps:
- name: pull in script

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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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

@@ -1,6 +1,5 @@
# Jellyfin Contributors
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
- [1337joe](https://github.com/1337joe)
- [97carmine](https://github.com/97carmine)
- [Abbe98](https://github.com/Abbe98)
@@ -15,7 +14,7 @@
- [bilde2910](https://github.com/bilde2910)
- [bfayers](https://github.com/bfayers)
- [BnMcG](https://github.com/BnMcG)
- [Bond_009](https://github.com/Bond-009)
- [Bond-009](https://github.com/Bond-009)
- [brianjmurrell](https://github.com/brianjmurrell)
- [bugfixin](https://github.com/bugfixin)
- [chaosinnovator](https://github.com/chaosinnovator)
@@ -32,7 +31,6 @@
- [DaveChild](https://github.com/DaveChild)
- [DavidFair](https://github.com/DavidFair)
- [Delgan](https://github.com/Delgan)
- [DerMaddis](https://github.com/dermaddis)
- [Derpipose](https://github.com/Derpipose)
- [dcrdev](https://github.com/dcrdev)
- [dhartung](https://github.com/dhartung)
@@ -56,7 +54,6 @@
- [geilername](https://github.com/geilername)
- [GermanCoding](https://github.com/GermanCoding)
- [gnattu](https://github.com/gnattu)
- [gnuyent](https://github.com/gnuyent)
- [GodTamIt](https://github.com/GodTamIt)
- [grafixeyehero](https://github.com/grafixeyehero)
- [h1nk](https://github.com/h1nk)
@@ -64,7 +61,6 @@
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [iwalton3](https://github.com/iwalton3)
- [Jakob Kukla](https://github.com/jakobkukla)
- [jftuga](https://github.com/jftuga)
- [jkhsjdhjs](https://github.com/jkhsjdhjs)
- [jmshrv](https://github.com/jmshrv)
@@ -73,10 +69,8 @@
- [JustAMan](https://github.com/JustAMan)
- [justinfenn](https://github.com/justinfenn)
- [JPVenson](https://github.com/JPVenson)
- [JPUC1143](https://github.com/Jpuc1143/)
- [KerryRJ](https://github.com/KerryRJ)
- [Larvitar](https://github.com/Larvitar)
- [lbenini](https://github.com/lbenini)
- [LeoVerto](https://github.com/LeoVerto)
- [Liggy](https://github.com/Liggy)
- [lmaonator](https://github.com/lmaonator)
@@ -89,19 +83,15 @@
- [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro)
- [MarkCiliaVincenti](https://github.com/MarkCiliaVincenti)
- [Martin Reuter](https://github.com/reuterma24)
- [Matt07211](https://github.com/Matt07211)
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00)
- [Michael McElroy](https://github.com/mcmcelro)
- [mitchfizz05](https://github.com/mitchfizz05)
- [mohd-akram](https://github.com/mohd-akram)
- [MrTimscampi](https://github.com/MrTimscampi)
- [n8225](https://github.com/n8225)
- [Nalsai](https://github.com/Nalsai)
- [Narfinger](https://github.com/Narfinger)
- [Nathan McCrina](https://github.com/nfmccrina)
- [NathanPickard](https://github.com/NathanPickard)
- [neilsb](https://github.com/neilsb)
- [nevado](https://github.com/nevado)
@@ -112,19 +102,16 @@
- [OancaAndrei](https://github.com/OancaAndrei)
- [obradovichv](https://github.com/obradovichv)
- [oddstr13](https://github.com/oddstr13)
- [olsh](https://github.com/olsh)
- [orryverducci](https://github.com/orryverducci)
- [petermcneil](https://github.com/petermcneil)
- [Phlogi](https://github.com/Phlogi)
- [pjeanjean](https://github.com/pjeanjean)
- [ploughpuff](https://github.com/ploughpuff)
- [poytiis](https://github.com/poytiis)
- [pR0Ps](https://github.com/pR0Ps)
- [PrplHaz4](https://github.com/PrplHaz4)
- [RazeLighter777](https://github.com/RazeLighter777)
- [redSpoutnik](https://github.com/redSpoutnik)
- [ringmatter](https://github.com/ringmatter)
- [Robert Lützner](https://github.com/rluetzner)
- [ryan-hartzell](https://github.com/ryan-hartzell)
- [s0urcelab](https://github.com/s0urcelab)
- [sachk](https://github.com/sachk)
@@ -140,7 +127,6 @@
- [sl1288](https://github.com/sl1288)
- [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
- [ssenart](https://github.com/ssenart)
@@ -163,7 +149,6 @@
- [twinkybot](https://github.com/twinkybot)
- [Ullmie02](https://github.com/Ullmie02)
- [Unhelpful](https://github.com/Unhelpful)
- [Utku Özdemir](https://github.com/utkuozdemir)
- [viaregio](https://github.com/viaregio)
- [vitorsemeano](https://github.com/vitorsemeano)
- [voodoos](https://github.com/voodoos)
@@ -179,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)
@@ -227,10 +211,6 @@
- [martenumberto](https://github.com/martenumberto)
- [ZeusCraft10](https://github.com/ZeusCraft10)
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
- [LiHRaM](https://github.com/LiHRaM)
- [MSalman5230](https://github.com/MSalman5230)
- [dwandw](https://github.com/dwandw)
- [Lampan-git](https://github.com/Lampan-git)
# Emby Contributors
@@ -294,3 +274,16 @@
- [tikuf](https://github.com/tikuf/)
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [olsh](https://github.com/olsh)
- [lbenini](https://github.com/lbenini)
- [gnuyent](https://github.com/gnuyent)
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir)
- [JPUC1143](https://github.com/Jpuc1143/)
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
- [Robert Lützner](https://github.com/rluetzner)
- [Nathan McCrina](https://github.com/nfmccrina)
- [Martin Reuter](https://github.com/reuterma24)
- [Michael McElroy](https://github.com/mcmcelro)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)

View File

@@ -4,52 +4,52 @@
</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.Xunit3" Version="4.19.0" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
<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="10.0.0" />
<PackageVersion Include="Diacritics" Version="4.1.8" />
<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" />
<PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.3" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.2" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Ignore" Version="0.2.1" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.12.0-pre1" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<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.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.8" />
<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.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.8" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<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.670" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="1.1.0.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
@@ -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,16 +74,17 @@
<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.8" />
<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.13.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.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="Xunit.v3.Priority" Version="1.1.18" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.61" />
<PackageVersion Include="xunit" Version="2.9.3" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,3 @@
#pragma warning disable CA1815
namespace Emby.Naming.AudioBook
{
/// <summary>

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",
@@ -379,14 +378,6 @@ namespace Emby.Naming.Common
IsNamed = true
},
// "Name - 101.mkv", "Name - 101 [720p].mkv", "Name - 101 (2020).mkv"
// Handles absolute episode numbers with hyphen delimiter (common in anime)
// Without brackets (bracketed version handled above)
new EpisodeExpression(@".*[\\\/](?<seriesname>[^\\\/]+?)[\s_]+-[\s_]+(?<epnumber>[0-9]+)[\s_]*(?:\[.*?\]|\(.*?\))*[\s_]*(?:\.\w+)?$")
{
IsNamed = true
},
// /server/anything_102.mp4
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
// /server/anything_1996.11.14.mp4

View File

@@ -36,7 +36,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>12.0.0</VersionPrefix>
<VersionPrefix>10.12.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -12,10 +12,10 @@ namespace Emby.Naming.TV
{
private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled);
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul|érie|éria|erie|eria)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPre();
[GeneratedRegex(@"^\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul|érie|éria|erie|eria)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
[GeneratedRegex(@"^\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPost();
[GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]

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

@@ -1,5 +1,3 @@
#pragma warning disable CA1815
namespace Emby.Naming.Video
{
/// <summary>

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

@@ -17,8 +17,8 @@ namespace Emby.Naming.Video
{
Name = name;
Files = [];
AlternateVersions = [];
Files = Array.Empty<VideoFileInfo>();
AlternateVersions = Array.Empty<VideoFileInfo>();
}
/// <summary>
@@ -40,10 +40,10 @@ namespace Emby.Naming.Video
public IReadOnlyList<VideoFileInfo> Files { get; set; }
/// <summary>
/// Gets or sets the alternate versions. Each alternate may itself span multiple files.
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public IReadOnlyList<VideoInfo> AlternateVersions { get; set; }
public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
/// <summary>
/// Gets or sets the extra type.

View File

@@ -5,8 +5,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -14,23 +13,8 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
public partial class VideoListResolver
public static partial class VideoListResolver
{
private static readonly StringComparer _numericOrdinalComparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
private readonly NamingOptions _namingOptions;
private readonly EpisodePathParser _episodePathParser;
/// <summary>
/// Initializes a new instance of the <see cref="VideoListResolver"/> class.
/// </summary>
/// <param name="namingOptions">The naming options.</param>
public VideoListResolver(NamingOptions namingOptions)
{
_namingOptions = namingOptions;
_episodePathParser = new EpisodePathParser(namingOptions);
}
[GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
private static partial Regex ResolutionRegex();
@@ -41,12 +25,12 @@ namespace Emby.Naming.Video
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <param name="videoInfos">List of related video files.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <param name="collectionType">The type of the containing collection, if known.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
public IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "", CollectionType? collectionType = null)
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "")
{
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
@@ -54,7 +38,7 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType is null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
var stackResult = StackResolver.Resolve(nonExtras, _namingOptions).ToList();
var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
var remainingFiles = new List<VideoFileInfo>();
var standaloneMedia = new List<VideoFileInfo>();
@@ -83,7 +67,7 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, _namingOptions, parseName, libraryRoot))
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot))
.OfType<VideoFileInfo>()
.ToList()
};
@@ -102,9 +86,7 @@ namespace Emby.Naming.Video
if (supportMultiVersion)
{
list = collectionType is CollectionType.tvshows
? GetEpisodesGroupedByVersion(list)
: GetVideosGroupedByVersion(list);
list = GetVideosGroupedByVersion(list, namingOptions);
}
// Whatever files are left, just add them
@@ -118,7 +100,7 @@ namespace Emby.Naming.Video
return list;
}
private List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
{
if (videos.Count == 0)
{
@@ -142,7 +124,7 @@ namespace Emby.Naming.Video
continue;
}
if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension))
if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
{
return videos;
}
@@ -153,9 +135,45 @@ namespace Emby.Naming.Video
}
}
var organized = OrganizeAlternateVersions(videos, primary, folderName.ToString());
if (videos.Count > 1)
{
var groups = videos
.Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x))
.Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value))
.GroupBy(x => x.resolutionMatch.Success)
.ToList();
return [organized];
videos.Clear();
StringComparer comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
foreach (var group in groups)
{
if (group.Key)
{
videos.InsertRange(0, group
.OrderByDescending(x => x.resolutionMatch.Value, comparer)
.ThenBy(x => x.filename, comparer)
.Select(x => x.value));
}
else
{
videos.AddRange(group.OrderBy(x => x.filename, comparer).Select(x => x.value));
}
}
}
primary ??= videos[0];
videos.Remove(primary);
var list = new List<VideoInfo>
{
primary
};
list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
list[0].Name = folderName.ToString();
return list;
}
private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
@@ -177,7 +195,7 @@ namespace Emby.Naming.Video
return true;
}
private bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename)
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, NamingOptions namingOptions)
{
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
@@ -191,7 +209,7 @@ namespace Emby.Naming.Video
}
// There are no span overloads for regex unfortunately
if (CleanStringParser.TryClean(testFilename.ToString(), _namingOptions.CleanStringRegexes, out var cleanName))
if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.AsSpan().Trim();
}
@@ -199,117 +217,7 @@ namespace Emby.Naming.Video
// The CleanStringParser should have removed common keywords etc.
return testFilename.IsEmpty
|| testFilename[0] == '-'
|| testFilename[0] == '_'
|| testFilename[0] == '.'
|| CheckMultiVersionRegex().IsMatch(testFilename);
}
private List<VideoInfo> GetEpisodesGroupedByVersion(List<VideoInfo> videos)
{
if (videos.Count < 2)
{
return videos;
}
var result = new List<VideoInfo>();
var groups = new Dictionary<string, List<VideoInfo>>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
var episodeResult = _episodePathParser.Parse(video.Files[0].Path, false);
string? key = null;
if (episodeResult.Success)
{
if (episodeResult.IsByDate
&& episodeResult.Year.HasValue
&& episodeResult.Month.HasValue
&& episodeResult.Day.HasValue)
{
key = FormattableString.Invariant(
$"D{episodeResult.Year.Value}{episodeResult.Month.Value:D2}{episodeResult.Day.Value:D2}");
}
else if (episodeResult.EpisodeNumber.HasValue)
{
key = FormattableString.Invariant(
$"S{episodeResult.SeasonNumber ?? 0}E{episodeResult.EpisodeNumber.Value}");
}
}
if (key is null)
{
result.Add(video);
continue;
}
if (!groups.TryGetValue(key, out var group))
{
group = [];
groups[key] = group;
}
group.Add(video);
}
foreach (var group in groups.Values)
{
if (group.Count == 1)
{
result.Add(group[0]);
continue;
}
result.Add(OrganizeAlternateVersions(group));
}
return result;
}
private static VideoInfo OrganizeAlternateVersions(
List<VideoInfo> videos,
VideoInfo? primaryOverride = null,
string? nameOverride = null)
{
if (videos.Count > 1)
{
var groups = videos
.Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x))
.Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value))
.GroupBy(x => x.resolutionMatch.Success)
.ToList();
videos = [];
foreach (var group in groups)
{
if (group.Key)
{
videos.InsertRange(0, group
.OrderByDescending(x => x.resolutionMatch.Value, _numericOrdinalComparer)
.ThenBy(x => x.filename, _numericOrdinalComparer)
.Select(x => x.value));
}
else
{
videos.AddRange(group.OrderBy(x => x.filename, _numericOrdinalComparer).Select(x => x.value));
}
}
}
// Prefer a stacked entry (more than one part) as primary
var primary = primaryOverride
?? videos.FirstOrDefault(v => v.Files.Count > 1)
?? videos[0];
videos.Remove(primary);
primary.AlternateVersions = videos;
if (nameOverride is not null)
{
primary.Name = nameOverride;
}
return primary;
}
}
}

View File

@@ -90,7 +90,6 @@ namespace Emby.Server.Implementations.AppBase
CreateAndCheckMarker(ProgramDataPath, "data");
CreateAndCheckMarker(CachePath, "cache");
CreateAndCheckMarker(DataPath, "data");
CreateCacheDirTag(CachePath);
}
/// <inheritdoc />
@@ -101,26 +100,6 @@ namespace Emby.Server.Implementations.AppBase
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
}
/// <summary>
/// Creates a CACHEDIR.TAG file in the specified directory per the Cache Directory Tagging specification.
/// This signals to backup tools (e.g. Restic, Borg) that the directory contains cached data
/// and can be excluded from backups.
/// </summary>
/// <param name="path">The cache directory path.</param>
internal static void CreateCacheDirTag(string path)
{
var tagPath = Path.Combine(path, "CACHEDIR.TAG");
if (!File.Exists(tagPath))
{
File.WriteAllText(
tagPath,
"Signature: 8a477f597d28d172789f06886806bc55\n"
+ "# This file is a cache directory tag created by Jellyfin.\n"
+ "# For information about cache directory tags, see:\n"
+ "#\thttps://bford.info/cachedir/\n");
}
}
private IEnumerable<string> GetMarkers(string path, bool recursive = false)
{
return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);

View File

@@ -228,7 +228,6 @@ namespace Emby.Server.Implementations.AppBase
Logger.LogInformation("Setting cache path: {Path}", cachePath);
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
BaseApplicationPaths.CreateCacheDirTag(cachePath);
}
/// <summary>

View File

@@ -14,7 +14,6 @@ using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Emby.Photos;
using Emby.Server.Implementations.Chapters;
using Emby.Server.Implementations.Collections;
@@ -26,7 +25,6 @@ using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
using Emby.Server.Implementations.Library.SimilarItems;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
@@ -94,11 +92,7 @@ using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.ListenBrainz;
using MediaBrowser.Providers.Plugins.ListenBrainz.Api;
using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Plugins.Tmdb.Movies;
using MediaBrowser.Providers.Plugins.Tmdb.TV;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
@@ -172,6 +166,8 @@ namespace Emby.Server.Implementations
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
_disposableParts.Add(_pluginManager);
}
/// <summary>
@@ -489,11 +485,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddScoped<ISystemManager, SystemManager>();
serviceCollection.AddSingleton<TmdbClientManager>();
serviceCollection.AddSingleton<TmdbMovieSimilarProvider>();
serviceCollection.AddSingleton<TmdbSeriesSimilarProvider>();
serviceCollection.AddSingleton<ListenBrainzLabsClient>();
serviceCollection.AddSingleton<ListenBrainzSimilarArtistProvider>();
serviceCollection.AddSingleton(NetManager);
@@ -516,13 +507,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<BaseItemRepository>();
serviceCollection.AddSingleton<IItemRepository>(sp => sp.GetRequiredService<BaseItemRepository>());
serviceCollection.AddSingleton<IItemQueryHelpers>(sp => sp.GetRequiredService<BaseItemRepository>());
serviceCollection.AddSingleton<IItemPersistenceService, ItemPersistenceService>();
serviceCollection.AddSingleton<INextUpService, NextUpService>();
serviceCollection.AddSingleton<IItemCountService, ItemCountService>();
serviceCollection.AddSingleton<ILinkedChildrenService, LinkedChildrenService>();
serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
@@ -541,14 +526,10 @@ namespace Emby.Server.Implementations
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
serviceCollection.AddSingleton<NamingOptions>();
serviceCollection.AddSingleton<VideoListResolver>();
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
serviceCollection.AddSingleton<DotIgnoreIgnoreRule>();
serviceCollection.AddSingleton<ISimilarItemsManager, SimilarItemsManager>();
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
@@ -660,7 +641,6 @@ namespace Emby.Server.Implementations
BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.ItemCountService = Resolve<IItemCountService>();
BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
@@ -707,8 +687,6 @@ namespace Emby.Server.Implementations
GetExports<IExternalUrlProvider>());
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
Resolve<ISimilarItemsManager>().AddParts(GetExports<ISimilarItemsProvider>());
}
/// <summary>
@@ -1028,8 +1006,6 @@ namespace Emby.Server.Implementations
}
_disposableParts.Clear();
_pluginManager?.Dispose();
}
_disposed = true;

View File

@@ -7,7 +7,6 @@ using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -129,7 +128,7 @@ public class ChapterManager : IChapterManager
var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
var threshold = TimeSpan.FromSeconds(1).Ticks;
if (chapters.Count >= 2 && averageChapterDuration < threshold)
if (averageChapterDuration < threshold)
{
_logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold);
extractImages = false;
@@ -233,22 +232,12 @@ public class ChapterManager : IChapterManager
}
/// <inheritdoc />
public bool Supports(BaseItem item)
=> item is Video or Audio;
/// <inheritdoc />
public void SaveChapters(BaseItem item, IReadOnlyList<ChapterInfo> chapters)
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
{
if (!Supports(item))
{
_logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id);
return;
}
// Remove any chapters that are outside of the runtime of the item
var validChapters = chapters.Where(c => c.StartPositionTicks < item.RunTimeTicks).ToList();
_chapterRepository.SaveChapters(item.Id, validChapters);
}
// Remove any chapters that are outside of the runtime of the video
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
_chapterRepository.SaveChapters(video.Id, validChapters);
}
/// <inheritdoc />
public ChapterInfo? GetChapter(Guid baseItemId, int index)

View File

@@ -272,7 +272,7 @@ namespace Emby.Server.Implementations.Collections
{
var childItem = _libraryManager.GetItemById(guidId);
var child = collection.LinkedChildren.FirstOrDefault(i => i.ItemId.HasValue && i.ItemId.Value.Equals(guidId));
var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem is not null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase)));
if (child is null)
{
@@ -342,7 +342,7 @@ namespace Emby.Server.Implementations.Collections
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
if (item is Video video)
{
foreach (var childId in _libraryManager.GetLocalAlternateVersionIds(video))
foreach (var childId in video.GetLocalAlternateVersionIds())
{
if (!results.ContainsKey(childId))
{

View File

@@ -5,12 +5,10 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -37,11 +35,7 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var deadItemsProgress = new Progress<double>(val => progress.Report(val * 0.8));
await CleanDeadItems(cancellationToken, deadItemsProgress).ConfigureAwait(false);
var playlistProgress = new Progress<double>(val => progress.Report(80 + (val * 0.2)));
await CleanOrphanedFilePlaylistsAsync(cancellationToken, playlistProgress).ConfigureAwait(false);
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
}
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
@@ -122,32 +116,4 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
progress.Report(100);
}
private async Task CleanOrphanedFilePlaylistsAsync(CancellationToken cancellationToken, IProgress<double> progress)
{
var playlists = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Playlist],
Recursive = true
}).OfType<Playlist>().ToList();
var numComplete = 0;
var numItems = Math.Max(playlists.Count, 1);
foreach (var playlist in playlists)
{
cancellationToken.ThrowIfCancellationRequested();
if (playlist.IsFile && !File.Exists(playlist.Path))
{
_logger.LogInformation("Removing file-based playlist {Name} because source file {Path} no longer exists", playlist.Name, playlist.Path);
_libraryManager.DeleteItem(playlist, new DeleteOptions { DeleteFileLocation = false });
}
numComplete++;
progress.Report((double)numComplete / numItems * 100);
}
progress.Report(100);
}
}

View File

@@ -153,102 +153,17 @@ namespace Emby.Server.Implementations.Dto
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(
IReadOnlyList<BaseItem> items,
DtoOptions options,
User? user = null,
BaseItem? owner = null,
bool skipVisibilityCheck = false)
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null)
{
var accessibleItems = skipVisibilityCheck || user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
var returnItems = new BaseItemDto[accessibleItems.Count];
List<(BaseItem, BaseItemDto)>? programTuples = null;
List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
// Batch-fetch user data for all items
Dictionary<Guid, UserItemData>? userDataBatch = null;
if (user is not null && options.EnableUserData)
{
userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user);
}
// Pre-compute collection folders once to avoid N+1 queries in CanDelete
List<Folder>? allCollectionFolders = null;
if (user is not null && options.ContainsField(ItemFields.CanDelete))
{
allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
}
// Batch-fetch child counts for all folders to avoid N+1 queries
Dictionary<Guid, int>? childCountBatch = null;
if (options.ContainsField(ItemFields.ChildCount))
{
var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
if (folderIds.Count > 0)
{
childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
}
}
// Batch-fetch played/total counts for all folders to avoid N+1 queries
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null;
if (user is not null && options.EnableUserData)
{
var folderIds = accessibleItems.OfType<Folder>()
.Where(f => f.SupportsUserDataFromChildren && (f.SupportsPlayedStatus || options.ContainsField(ItemFields.RecursiveItemCount)))
.Select(f => f.Id).ToList();
if (folderIds.Count > 0)
{
playedCountBatch = _libraryManager.GetPlayedAndTotalCountBatch(folderIds, user);
}
}
// Batch-fetch MusicArtist lookups across all items to avoid N+1 queries.
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null;
var artistNames = new HashSet<string>(StringComparer.Ordinal);
foreach (var item in accessibleItems)
{
if (item is IHasArtist hasArtist)
{
foreach (var name in hasArtist.Artists)
{
if (!string.IsNullOrWhiteSpace(name))
{
artistNames.Add(name);
}
}
}
if (item is IHasAlbumArtist hasAlbumArtist)
{
foreach (var name in hasAlbumArtist.AlbumArtists)
{
if (!string.IsNullOrWhiteSpace(name))
{
artistNames.Add(name);
}
}
}
}
if (artistNames.Count > 0)
{
artistsBatch = _libraryManager.GetArtists(artistNames.ToArray());
}
for (int index = 0; index < accessibleItems.Count; index++)
{
var item = accessibleItems[index];
var dto = GetBaseItemDtoInternal(
item,
options,
user,
owner,
userDataBatch?.GetValueOrDefault(item.Id),
allCollectionFolders,
childCountBatch,
playedCountBatch,
artistsBatch);
var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel)
{
@@ -282,7 +197,7 @@ namespace Emby.Server.Implementations.Dto
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
{
var dto = GetBaseItemDtoInternal(item, options, user, owner, null);
var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel)
{
LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
@@ -300,16 +215,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
private BaseItemDto GetBaseItemDtoInternal(
BaseItem item,
DtoOptions options,
User? user = null,
BaseItem? owner = null,
UserItemData? userData = null,
List<Folder>? allCollectionFolders = null,
Dictionary<Guid, int>? childCountBatch = null,
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
{
var dto = new BaseItemDto
{
@@ -346,14 +252,7 @@ namespace Emby.Server.Implementations.Dto
if (user is not null)
{
AttachUserSpecificInfo(
dto,
item,
user,
options,
userData,
childCountBatch,
playedCountBatch);
AttachUserSpecificInfo(dto, item, user, options);
}
if (item is IHasMediaSources
@@ -369,15 +268,13 @@ namespace Emby.Server.Implementations.Dto
AttachStudios(dto, item);
}
AttachBasicFields(dto, item, owner, options, artistsBatch);
AttachBasicFields(dto, item, owner, options);
if (options.ContainsField(ItemFields.CanDelete))
{
dto.CanDelete = user is null
? item.CanDelete()
: allCollectionFolders is not null
? item.CanDelete(user, allCollectionFolders)
: item.CanDelete(user);
: item.CanDelete(user);
}
if (options.ContainsField(ItemFields.CanDownload))
@@ -481,7 +378,37 @@ namespace Emby.Server.Implementations.Dto
return;
}
var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
var query = new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = new DtoOptions(false) { EnableImages = false },
IncludeItemTypes = relatedItemKinds
};
switch (dto.Type)
{
case BaseItemKind.Genre:
case BaseItemKind.MusicGenre:
query.GenreIds = [dto.Id];
break;
case BaseItemKind.MusicArtist:
query.ArtistIds = [dto.Id];
break;
case BaseItemKind.Person:
query.PersonIds = [dto.Id];
break;
case BaseItemKind.Studio:
query.StudioIds = [dto.Id];
break;
case BaseItemKind.Year
when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year):
query.Years = [year];
break;
default:
return;
}
var counts = _libraryManager.GetItemCounts(query);
dto.AlbumCount = counts.AlbumCount;
dto.ArtistCount = counts.ArtistCount;
@@ -531,14 +458,7 @@ namespace Emby.Server.Implementations.Dto
/// <summary>
/// Attaches the user specific info.
/// </summary>
private void AttachUserSpecificInfo(
BaseItemDto dto,
BaseItem item,
User user,
DtoOptions options,
UserItemData? userData = null,
Dictionary<Guid, int>? childCountBatch = null,
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options)
{
if (item.IsFolder)
{
@@ -546,19 +466,7 @@ namespace Emby.Server.Implementations.Dto
if (options.EnableUserData)
{
if (userData is not null)
{
// Use pre-fetched user data
dto.UserData = GetUserItemDataDto(userData, item.Id);
(int Played, int Total)? precomputed = playedCountBatch is not null
&& playedCountBatch.TryGetValue(item.Id, out var counts) ? counts : null;
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options, precomputed);
}
else
{
// Fall back to individual fetch
dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
}
dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
}
if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
@@ -577,7 +485,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.ChildCount))
{
dto.ChildCount ??= GetChildCount(folder, user, childCountBatch);
dto.ChildCount ??= GetChildCount(folder, user);
}
}
@@ -595,17 +503,7 @@ namespace Emby.Server.Implementations.Dto
{
if (options.EnableUserData)
{
if (userData is not null)
{
// Use pre-fetched user data
dto.UserData = GetUserItemDataDto(userData, item.Id);
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
}
else
{
// Fall back to individual fetch
dto.UserData = _userDataRepository.GetUserDataDto(item, user);
}
dto.UserData = _userDataRepository.GetUserDataDto(item, user);
}
}
@@ -615,25 +513,7 @@ namespace Emby.Server.Implementations.Dto
}
}
private static UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
{
ArgumentNullException.ThrowIfNull(data);
return new UserItemDataDto
{
IsFavorite = data.IsFavorite,
Likes = data.Likes,
PlaybackPositionTicks = data.PlaybackPositionTicks,
PlayCount = data.PlayCount,
Rating = data.Rating,
Played = data.Played,
LastPlayedDate = data.LastPlayedDate,
ItemId = itemId,
Key = data.Key
};
}
private static int GetChildCount(Folder folder, User user, Dictionary<Guid, int>? childCountBatch)
private static int GetChildCount(Folder folder, User user)
{
// Right now this is too slow to calculate for top level folders on a per-user basis
// Just return something so that apps that are expecting a value won't think the folders are empty
@@ -642,13 +522,6 @@ namespace Emby.Server.Implementations.Dto
return Random.Shared.Next(1, 10);
}
// Use pre-fetched batch data if available
if (childCountBatch is not null && childCountBatch.TryGetValue(folder.Id, out var count))
{
return count;
}
// Fall back to individual query for special cases (Series, Season, etc.)
return folder.GetChildCount(user);
}
@@ -942,8 +815,7 @@ namespace Emby.Server.Implementations.Dto
/// <param name="item">The item.</param>
/// <param name="owner">The owner.</param>
/// <param name="options">The options.</param>
/// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param>
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options)
{
if (options.ContainsField(ItemFields.DateCreated))
{
@@ -1067,8 +939,6 @@ namespace Emby.Server.Implementations.Dto
dto.OriginalTitle = item.OriginalTitle;
}
dto.OriginalLanguage = item.OriginalLanguage;
if (options.ContainsField(ItemFields.ParentId))
{
dto.ParentId = item.DisplayParentId;
@@ -1149,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))
@@ -1190,8 +1051,7 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
var artistsLookup = artistsBatch
?? _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
dto.ArtistItems = hasArtist.Artists
.Where(name => !string.IsNullOrWhiteSpace(name))
@@ -1225,8 +1085,7 @@ namespace Emby.Server.Implementations.Dto
// })
// .ToList();
var albumArtistsLookup = artistsBatch
?? _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
.Where(name => !string.IsNullOrWhiteSpace(name))
@@ -1264,6 +1123,11 @@ namespace Emby.Server.Implementations.Dto
}
}
if (options.ContainsField(ItemFields.Chapters))
{
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
}
if (options.ContainsField(ItemFields.Trickplay))
{
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
@@ -1277,11 +1141,6 @@ namespace Emby.Server.Implementations.Dto
dto.ExtraType = video.ExtraType;
}
if (options.ContainsField(ItemFields.Chapters))
{
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
}
if (options.ContainsField(ItemFields.MediaStreams))
{
// Add VideoInfo

View File

@@ -1,6 +1,5 @@
using System;
using System.Buffers;
using System.Globalization;
using System.IO.Pipelines;
using System.Net;
using System.Net.WebSockets;
@@ -70,11 +69,6 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public IPAddress? RemoteEndPoint { get; }
/// <summary>
/// Gets or initializes the UI culture captured from the upgrade request.
/// </summary>
public CultureInfo? RequestUICulture { get; init; }
/// <inheritdoc />
public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
@@ -87,17 +81,6 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public WebSocketState State => _socket.State;
/// <inheritdoc />
public void ApplyRequestCulture()
{
if (RequestUICulture is null)
{
return;
}
CultureInfo.CurrentUICulture = RequestUICulture;
}
/// <inheritdoc />
public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
{

View File

@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Threading.Tasks;
@@ -48,18 +47,14 @@ namespace Emby.Server.Implementations.HttpServer
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
var connection = new WebSocketConnection(
_loggerFactory.CreateLogger<WebSocketConnection>(),
webSocket,
authorizationInfo,
context.GetNormalizedRemoteIP())
{
RequestUICulture = CultureInfo.CurrentUICulture
};
connection.OnReceive = result =>
{
connection.ApplyRequestCulture();
return ProcessWebSocketMessageReceived(result);
OnReceive = ProcessWebSocketMessageReceived
};
await using (connection.ConfigureAwait(false))
{

View File

@@ -21,7 +21,6 @@ namespace Emby.Server.Implementations.IO
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
/// <summary>
/// The file system watchers.
@@ -48,23 +47,19 @@ namespace Emby.Server.Implementations.IO
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
public LibraryMonitor(
ILogger<LibraryMonitor> logger,
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
IHostApplicationLifetime appLifetime,
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
IHostApplicationLifetime appLifetime)
{
_libraryManager = libraryManager;
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
appLifetime.ApplicationStarted.Register(Start);
appLifetime.ApplicationStopping.Register(Stop);
}
/// <inheritdoc />
@@ -358,7 +353,7 @@ namespace Emby.Server.Implementations.IO
}
var fileInfo = _fileSystem.GetFileSystemInfo(path);
if (_dotIgnoreIgnoreRule.ShouldIgnore(fileInfo, null))
if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
{
return;
}

View File

@@ -586,12 +586,6 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
if (!Directory.Exists(path))
{
_logger.LogWarning("Directory does not exist: {Path}", path);
return [];
}
var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and macOS the search pattern is case-sensitive

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

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using BitFaster.Caching.Lru;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Resolvers;
@@ -17,36 +15,22 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
{
private static readonly bool IsWindows = OperatingSystem.IsWindows();
private readonly FastConcurrentLru<string, IgnoreFileCacheEntry> _directoryCache;
private readonly FastConcurrentLru<string, ParsedIgnoreCacheEntry> _rulesCache;
/// <summary>
/// Initializes a new instance of the <see cref="DotIgnoreIgnoreRule"/> class.
/// </summary>
public DotIgnoreIgnoreRule()
private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
{
var cacheSize = Math.Max(100, Environment.ProcessorCount * 100);
_directoryCache = new FastConcurrentLru<string, IgnoreFileCacheEntry>(
Environment.ProcessorCount,
cacheSize,
StringComparer.Ordinal);
_rulesCache = new FastConcurrentLru<string, ParsedIgnoreCacheEntry>(
Environment.ProcessorCount,
Math.Max(32, cacheSize / 4),
StringComparer.Ordinal);
for (var current = directory; current is not null; current = current.Parent)
{
var ignorePath = Path.Join(current.FullName, ".ignore");
if (File.Exists(ignorePath))
{
return new FileInfo(ignorePath);
}
}
return null;
}
/// <inheritdoc />
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnoredInternal(fileInfo, parent);
/// <summary>
/// Clears the directory lookup cache. The parsed rules cache is not cleared
/// as it validates file modification time on each access.
/// </summary>
public void ClearDirectoryCache()
{
_directoryCache.Clear();
}
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
/// <summary>
/// Checks whether or not the file is ignored.
@@ -54,38 +38,40 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
/// <param name="fileInfo">The file information.</param>
/// <param name="parent">The parent BaseItem.</param>
/// <returns>True if the file should be ignored.</returns>
public bool IsIgnoredInternal(FileSystemMetadata fileInfo, BaseItem? parent)
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
{
var searchDirectory = fileInfo.IsDirectory
? fileInfo.FullName
: Path.GetDirectoryName(fileInfo.FullName);
? new DirectoryInfo(fileInfo.FullName)
: new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
if (string.IsNullOrEmpty(searchDirectory))
if (string.IsNullOrEmpty(searchDirectory.FullName))
{
return false;
}
var ignoreFile = FindIgnoreFileCached(searchDirectory);
var ignoreFile = FindIgnoreFile(searchDirectory);
if (ignoreFile is null)
{
return false;
}
var parsedEntry = GetParsedRules(ignoreFile);
if (parsedEntry is null)
{
// File was deleted after we cached the path - clear the directory cache entry and return false
_directoryCache.TryRemove(searchDirectory, out _);
return false;
}
// Empty file means ignore everything
if (parsedEntry.IsEmpty)
// Fast path in case the ignore files isn't a symlink and is empty
if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
{
// Ignore directory if we just have the file
return true;
}
return parsedEntry.Rules.IsIgnored(GetPathToCheck(fileInfo.FullName, fileInfo.IsDirectory));
var content = GetFileContent(ignoreFile);
return string.IsNullOrWhiteSpace(content)
|| CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
}
private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
{
// If file has content, base ignoring off the content .gitignore-style rules
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return CheckIgnoreRules(path, rules, isDirectory);
}
/// <summary>
@@ -131,8 +117,8 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
return true;
}
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
// See https://github.com/jellyfin/jellyfin/issues/15484
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
// See https://github.com/jellyfin/jellyfin/issues/15484
var pathToCheck = normalizePath ? path.NormalizePath('/') : path;
// Add trailing slash for directories to match "folder/"
@@ -144,196 +130,11 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
return ignore.IsIgnored(pathToCheck);
}
private FileInfo? FindIgnoreFileCached(string directory)
private static string GetFileContent(FileInfo ignoreFile)
{
// Check if we have a cached result for this directory
if (_directoryCache.TryGet(directory, out var cached))
{
return cached.IgnoreFileDirectory is null
? null
: new FileInfo(Path.Join(cached.IgnoreFileDirectory, ".ignore"));
}
DirectoryInfo startDir;
try
{
startDir = new DirectoryInfo(directory);
}
catch (ArgumentException)
{
return null;
}
// Walk up the directory tree to find .ignore file using DirectoryInfo.Parent
var checkedDirs = new List<string> { directory };
for (var current = startDir; current is not null; current = current.Parent)
{
var currentPath = current.FullName;
// Check if this intermediate directory is cached
if (current != startDir && _directoryCache.TryGet(currentPath, out var parentCached))
{
// Cache the result for all directories we checked
var entry = new IgnoreFileCacheEntry(parentCached.IgnoreFileDirectory);
foreach (var dir in checkedDirs)
{
_directoryCache.AddOrUpdate(dir, entry);
}
return parentCached.IgnoreFileDirectory is null
? null
: new FileInfo(Path.Join(parentCached.IgnoreFileDirectory, ".ignore"));
}
var ignoreFile = new FileInfo(Path.Join(currentPath, ".ignore"));
if (ignoreFile.Exists)
{
// Cache for all directories we checked
var entry = new IgnoreFileCacheEntry(currentPath);
foreach (var dir in checkedDirs)
{
_directoryCache.AddOrUpdate(dir, entry);
}
return ignoreFile;
}
if (current != startDir)
{
checkedDirs.Add(currentPath);
}
}
// No .ignore file found - cache null result for all directories
var nullEntry = new IgnoreFileCacheEntry((string?)null);
foreach (var dir in checkedDirs)
{
_directoryCache.AddOrUpdate(dir, nullEntry);
}
return null;
}
private ParsedIgnoreCacheEntry? GetParsedRules(FileInfo ignoreFile)
{
if (!ignoreFile.Exists)
{
_rulesCache.TryRemove(ignoreFile.FullName, out _);
return null;
}
var lastModified = ignoreFile.LastWriteTimeUtc;
var fileLength = ignoreFile.Length;
var key = ignoreFile.FullName;
// Check cache
if (_rulesCache.TryGet(key, out var cached))
{
if (cached.FileLastModified == lastModified && cached.FileLength == fileLength)
{
return cached;
}
// Stale - need to reparse
_rulesCache.TryRemove(key, out _);
}
// Parse the file
var parsedEntry = ParseIgnoreFile(ignoreFile, lastModified, fileLength);
_rulesCache.AddOrUpdate(key, parsedEntry);
return parsedEntry;
}
private static ParsedIgnoreCacheEntry ParseIgnoreFile(FileInfo ignoreFile, DateTime lastModified, long fileLength)
{
if (ignoreFile.LinkTarget is null && fileLength == 0)
{
return new ParsedIgnoreCacheEntry
{
Rules = new Ignore.Ignore(),
FileLastModified = lastModified,
FileLength = fileLength,
IsEmpty = true
};
}
// Resolve symlinks
var resolvedFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
if (!resolvedFile.Exists)
{
return new ParsedIgnoreCacheEntry
{
Rules = new Ignore.Ignore(),
FileLastModified = lastModified,
FileLength = fileLength,
IsEmpty = true
};
}
var content = File.ReadAllText(resolvedFile.FullName);
if (string.IsNullOrWhiteSpace(content))
{
return new ParsedIgnoreCacheEntry
{
Rules = new Ignore.Ignore(),
FileLastModified = lastModified,
FileLength = fileLength,
IsEmpty = true
};
}
var rules = content.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var ignore = new Ignore.Ignore();
var validRulesAdded = 0;
foreach (var rule in rules)
{
try
{
ignore.Add(rule);
validRulesAdded++;
}
catch (RegexParseException)
{
// Ignore invalid patterns
}
}
// No valid rules means treat as empty (ignore all)
return new ParsedIgnoreCacheEntry
{
Rules = ignore,
FileLastModified = lastModified,
FileLength = fileLength,
IsEmpty = validRulesAdded == 0
};
}
private static string GetPathToCheck(string path, bool isDirectory)
{
// Normalize Windows paths
var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
// Add trailing slash for directories to match "folder/"
if (isDirectory)
{
pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
}
return pathToCheck;
}
private readonly record struct IgnoreFileCacheEntry(string? IgnoreFileDirectory);
private sealed class ParsedIgnoreCacheEntry
{
public required Ignore.Ignore Rules { get; init; }
public required DateTime FileLastModified { get; init; }
public required long FileLength { get; init; }
public required bool IsEmpty { get; init; }
ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
return ignoreFile.Exists
? File.ReadAllText(ignoreFile.FullName)
: string.Empty;
}
}

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

@@ -13,7 +13,6 @@ using System.Threading.Tasks;
using BitFaster.Caching.Lru;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Naming.Video;
using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
@@ -31,17 +30,18 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -77,18 +77,12 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
private readonly IItemPersistenceService _persistenceService;
private readonly INextUpService _nextUpService;
private readonly IItemCountService _countService;
private readonly ILinkedChildrenService _linkedChildrenService;
private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions;
private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager;
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
private readonly IMediaStreamRepository _mediaStreamRepository;
/// <summary>
/// The _root folder sync lock.
@@ -121,17 +115,11 @@ namespace Emby.Server.Implementations.Library
/// <param name="userViewManagerFactory">The user view manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="persistenceService">The item persistence service.</param>
/// <param name="nextUpService">The next up service.</param>
/// <param name="countService">The item count service.</param>
/// <param name="linkedChildrenService">The linked children service.</param>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
/// <param name="peopleRepository">The people repository.</param>
/// <param name="pathManager">The path manager.</param>
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
/// <param name="mediaStreamRepository">The media stream repository.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -145,17 +133,11 @@ namespace Emby.Server.Implementations.Library
Lazy<IUserViewManager> userViewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IItemPersistenceService persistenceService,
INextUpService nextUpService,
IItemCountService countService,
ILinkedChildrenService linkedChildrenService,
IImageProcessor imageProcessor,
NamingOptions namingOptions,
IDirectoryService directoryService,
IPeopleRepository peopleRepository,
IPathManager pathManager,
DotIgnoreIgnoreRule dotIgnoreIgnoreRule,
IMediaStreamRepository mediaStreamRepository)
IPathManager pathManager)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -169,10 +151,6 @@ namespace Emby.Server.Implementations.Library
_userViewManagerFactory = userViewManagerFactory;
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_persistenceService = persistenceService;
_nextUpService = nextUpService;
_countService = countService;
_linkedChildrenService = linkedChildrenService;
_imageProcessor = imageProcessor;
_cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
@@ -180,13 +158,10 @@ namespace Emby.Server.Implementations.Library
_namingOptions = namingOptions;
_peopleRepository = peopleRepository;
_pathManager = pathManager;
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
_mediaStreamRepository = mediaStreamRepository;
RecordConfigurationValues(_configurationManager.Configuration);
}
@@ -352,17 +327,9 @@ namespace Emby.Server.Implementations.Library
DeleteItem(item, options, parent, notifyParentItem);
}
public void DeleteItemsUnsafeFast(IReadOnlyCollection<BaseItem> items, bool deleteSourceFiles = false)
public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items)
{
if (items.Count == 0)
{
return;
}
var pathMaps = items.Select(e =>
(Item: e,
InternalPath: GetInternalMetadataPaths(e),
DeletePaths: deleteSourceFiles ? e.GetDeletePaths() : [])).ToArray();
var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray();
foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
{
@@ -396,7 +363,7 @@ namespace Emby.Server.Implementations.Library
}
}
_persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
_itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
}
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
@@ -439,99 +406,6 @@ namespace Emby.Server.Implementations.Library
item.Id);
}
// If deleting a primary version video, clear PrimaryVersionId from alternate versions
// OwnerId check: items with OwnerId set are alternate versions or extras, not primaries
if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty())
{
var localAlternateIds = GetLocalAlternateVersionIds(video).ToHashSet();
var allAlternateVersions = localAlternateIds
.Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
.Distinct()
.Select(id => GetItemById(id))
.OfType<Video>()
.ToList();
// Partition alternates by whether their files still exist on disk
var alternateVersions = new List<Video>();
var missingAlternates = new List<Video>();
foreach (var alt in allAlternateVersions)
{
if (!string.IsNullOrEmpty(alt.Path) && !_fileSystem.FileExists(alt.Path))
{
missingAlternates.Add(alt);
}
else
{
alternateVersions.Add(alt);
}
}
// Delete alternates whose files no longer exist to avoid ghost items.
// Clear PrimaryVersionId first so DeleteItem doesn't try to update the primary being deleted.
foreach (var missing in missingAlternates)
{
_logger.LogInformation(
"Deleting missing alternate version {Name} ({Path})",
missing.Name ?? "Unknown name",
missing.Path ?? string.Empty);
missing.SetPrimaryVersionId(null);
missing.OwnerId = Guid.Empty;
missing.LocalAlternateVersions = [];
missing.LinkedAlternateVersions = [];
DeleteItem(missing, new DeleteOptions { DeleteFileLocation = false }, false);
}
if (alternateVersions.Count > 0)
{
_logger.LogInformation(
"Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
alternateVersions.Count,
item.Name ?? "Unknown name");
// Promote the first alternate version to be the new primary
var newPrimary = alternateVersions[0];
newPrimary.SetPrimaryVersionId(null);
newPrimary.OwnerId = Guid.Empty;
// Transfer alternate version arrays from old primary to new primary
// so UpdateToRepositoryAsync creates correct LinkedChildren entries
newPrimary.LocalAlternateVersions = video.LocalAlternateVersions
.Where(p => !string.Equals(p, newPrimary.Path, StringComparison.OrdinalIgnoreCase))
.ToArray();
newPrimary.LinkedAlternateVersions = video.LinkedAlternateVersions
.Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(newPrimary.Id))
.ToArray();
newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
// Re-route playlist/collection references from deleted primary to new primary
RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult();
// Update remaining alternates to point to new primary
foreach (var alternate in alternateVersions.Skip(1))
{
alternate.SetPrimaryVersionId(newPrimary.Id);
// Only set OwnerId for local alternates; linked alternates are independent items
alternate.OwnerId = localAlternateIds.Contains(alternate.Id) ? newPrimary.Id : Guid.Empty;
alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
}
}
else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue)
{
// If deleting an alternate version, re-route references to its primary
RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter().GetResult();
// Remove deleted alternate from primary's LinkedAlternateVersions
if (GetItemById(alternateVideo.PrimaryVersionId.Value) is Video primaryVideo)
{
primaryVideo.LinkedAlternateVersions = primaryVideo.LinkedAlternateVersions
.Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(alternateVideo.Id))
.ToArray();
primaryVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
}
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
: [];
@@ -576,7 +450,7 @@ namespace Emby.Server.Implementations.Library
item.SetParent(null);
_persistenceService.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
_itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
_cache.TryRemove(item.Id, out _);
foreach (var child in children)
{
@@ -702,9 +576,6 @@ namespace Emby.Server.Implementations.Library
// Trickplay
list.Add(_pathManager.GetTrickplayDirectory(video));
// Chapter Images
list.Add(_pathManager.GetChapterImageFolderPath(video));
// Subtitles and attachments
foreach (var mediaSource in item.GetMediaSources(false))
{
@@ -786,99 +657,8 @@ namespace Emby.Server.Implementations.Library
return key.GetMD5();
}
public BaseItem? ResolvePath(
FileSystemMetadata fileInfo,
Folder? parent = null,
IDirectoryService? directoryService = null,
CollectionType? collectionType = null)
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType);
private void SetAdditionalPartsFromStack(Video altVideo, string path)
{
if (altVideo.AdditionalParts is { Length: > 0 })
{
return;
}
var directory = Path.GetDirectoryName(path);
if (string.IsNullOrEmpty(directory))
{
return;
}
IEnumerable<FileSystemMetadata> siblings;
try
{
siblings = _fileSystem.GetFiles(directory);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to enumerate siblings to detect stack for {Path}", path);
return;
}
var stacks = StackResolver.Resolve(siblings, _namingOptions);
foreach (var stack in stacks)
{
if (stack.Files.Count > 1
&& string.Equals(stack.Files[0], path, StringComparison.OrdinalIgnoreCase))
{
altVideo.AdditionalParts = stack.Files.Skip(1).ToArray();
return;
}
}
}
/// <inheritdoc />
public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? collectionType)
{
// Clean up any existing item saved with wrong type (e.g. Video instead of Movie).
// This happens when items were previously resolved without proper type context
// in mixed-content libraries where collectionType is null.
var expectedId = GetNewItemId(path, expectedVideoType);
if (expectedVideoType != typeof(Video))
{
var wrongTypeId = GetNewItemId(path, typeof(Video));
if (!wrongTypeId.Equals(expectedId) && GetItemById(wrongTypeId) is Video wrongTypeItem)
{
_logger.LogInformation(
"Removing alternate version with wrong type {WrongType}, expected {ExpectedType}: {Path}",
wrongTypeItem.GetType().Name,
expectedVideoType.Name,
path);
DeleteItem(wrongTypeItem, new DeleteOptions { DeleteFileLocation = false });
}
}
var resolved = ResolvePath(
_fileSystem.GetFileSystemInfo(path),
parent,
collectionType: collectionType) as Video;
if (resolved is null)
{
return null;
}
// Ensure the alternate version has the same concrete type as the primary video.
// ResolvePath may return a generic Video for files in mixed-content libraries
// where collectionType is null, even though the primary is a Movie/Episode/etc.
if (resolved.GetType() != expectedVideoType)
{
if (Activator.CreateInstance(expectedVideoType) is Video correctVideo)
{
correctVideo.Path = resolved.Path;
correctVideo.Name = resolved.Name;
correctVideo.VideoType = resolved.VideoType;
correctVideo.ProductionYear = resolved.ProductionYear;
correctVideo.ExtraType = resolved.ExtraType;
resolved = correctVideo;
}
}
resolved.Id = expectedId;
return resolved;
}
public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null)
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
private BaseItem? ResolvePath(
FileSystemMetadata fileInfo,
@@ -1261,7 +1041,7 @@ namespace Emby.Server.Implementations.Library
public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
{
return _linkedChildrenService.FindArtists(names);
return _itemRepository.FindArtists(names);
}
public MusicArtist GetArtist(string name, DtoOptions options)
@@ -1351,7 +1131,6 @@ namespace Emby.Server.Implementations.Library
public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
{
IsScanRunning = true;
ClearIgnoreRuleCache();
LibraryMonitor.Stop();
try
@@ -1360,7 +1139,6 @@ namespace Emby.Server.Implementations.Library
}
finally
{
ClearIgnoreRuleCache();
LibraryMonitor.Start();
IsScanRunning = false;
}
@@ -1368,7 +1146,6 @@ namespace Emby.Server.Implementations.Library
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
{
ClearIgnoreRuleCache();
RootFolder.Children = null;
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
@@ -1409,16 +1186,8 @@ namespace Emby.Server.Implementations.Library
if (toDelete.Count > 0)
{
_persistenceService.DeleteItem(toDelete.ToArray());
_itemRepository.DeleteItem(toDelete.ToArray());
}
ClearIgnoreRuleCache();
}
/// <inheritdoc />
public void ClearIgnoreRuleCache()
{
_dotIgnoreIgnoreRule.ClearDirectoryCache();
}
private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
@@ -1493,7 +1262,7 @@ namespace Emby.Server.Implementations.Library
progress.Report(percent * 100);
}
_persistenceService.UpdateInheritedValues();
_itemRepository.UpdateInheritedValues();
progress.Report(100);
}
@@ -1652,7 +1421,14 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User, allowExternalContent);
}
return _itemRepository.GetItemList(query);
var itemList = _itemRepository.GetItemList(query);
var user = query.User;
if (user is not null)
{
return itemList.Where(i => i.IsVisible(user)).ToList();
}
return itemList;
}
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
@@ -1676,7 +1452,7 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User);
}
return _countService.GetCount(query);
return _itemRepository.GetCount(query);
}
public ItemCounts GetItemCounts(InternalItemsQuery query)
@@ -1695,30 +1471,7 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User);
}
return _countService.GetItemCounts(query);
}
/// <inheritdoc/>
public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? user)
{
var query = new InternalItemsQuery(user);
if (user is not null)
{
AddUserToQuery(query, user);
}
return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
}
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
{
return _countService.GetChildCountBatch(parentIds, userId);
}
/// <inheritdoc/>
public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user)
{
return _countService.GetPlayedAndTotalCountBatch(folderIds, user);
return _itemRepository.GetItemCounts(query);
}
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
@@ -1763,17 +1516,7 @@ namespace Emby.Server.Implementations.Library
}
}
return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff);
}
/// <inheritdoc />
public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisodesBatch(
InternalItemsQuery query,
IReadOnlyList<string> seriesKeys,
bool includeSpecials,
bool includeWatchedForRewatching)
{
return _nextUpService.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching);
return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
}
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
@@ -1940,25 +1683,6 @@ namespace Emby.Server.Implementations.Library
query.TopParentIds = [Guid.NewGuid()];
}
}
else if (parents.Count == 1 && parents.First() is Folder folder
&& (folder is Playlist || folder is BoxSet)
&& folder.LinkedChildren.Length > 0)
{
// Playlists and BoxSets store their contents in LinkedChildren and never
// populate AncestorIds for those items, so a recursive AncestorIds query
// would return zero rows. Resolve to the linked child IDs up front and
// route through the existing indexed ItemIds filter.
query.ItemIds = folder.LinkedChildren
.Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
.Select(lc => lc.ItemId!.Value)
.ToArray();
// Empty linked-children should still return empty rather than scanning everything.
if (query.ItemIds.Length == 0)
{
query.ItemIds = [Guid.NewGuid()];
}
}
else
{
// We need to be able to query from any arbitrary ancestor up the tree
@@ -1976,11 +1700,6 @@ namespace Emby.Server.Implementations.Library
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
{
if (query.User is null)
{
query.SetUser(user);
}
if (query.AncestorIds.Length == 0 &&
query.ParentId.IsEmpty() &&
query.ChannelIds.Count == 0 &&
@@ -2006,15 +1725,6 @@ namespace Emby.Server.Implementations.Library
}
}
/// <inheritdoc/>
public void ConfigureUserAccess(InternalItemsQuery query, User user)
{
ArgumentNullException.ThrowIfNull(query);
ArgumentNullException.ThrowIfNull(user);
AddUserToQuery(query, user);
}
private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
{
if (item is UserView view)
@@ -2179,44 +1889,6 @@ namespace Emby.Server.Implementations.Library
return video;
}
/// <inheritdoc />
public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
{
ArgumentNullException.ThrowIfNull(video);
var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LocalAlternateVersion);
if (linkedIds.Count > 0)
{
return linkedIds;
}
return [];
}
/// <inheritdoc />
public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
{
ArgumentNullException.ThrowIfNull(video);
var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LinkedAlternateVersion);
if (linkedIds.Count > 0)
{
return linkedIds
.Select(id => GetItemById(id))
.Where(i => i is not null)
.OfType<Video>()
.OrderBy(i => i.SortName);
}
return [];
}
/// <inheritdoc />
public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType childType)
{
_linkedChildrenService.UpsertLinkedChild(parentId, childId, childType);
}
/// <inheritdoc />
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
{
@@ -2321,48 +1993,9 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
{
// Resolve and add any local alternate version items that don't exist yet
// This ensures they exist in the database when LinkedChildren are processed
var allItems = new List<BaseItem>(items);
var parentFolder = parent as Folder;
var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null;
_itemRepository.SaveItems(items, cancellationToken);
foreach (var item in items)
{
if (item is Video video && video.LocalAlternateVersions.Length > 0)
{
var videoType = video.GetType();
foreach (var path in video.LocalAlternateVersions)
{
if (string.IsNullOrEmpty(path))
{
continue;
}
// Use the primary video's type for ID calculation to ensure consistency
var altId = GetNewItemId(path, videoType);
if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
{
// Alternate version doesn't exist, resolve and create it
// ensuring it has the same type as the primary video
var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
if (altVideo is not null)
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
// ResolveAlternateVersion only sees the alternate's primary file.
// If the alternate is itself a stack (e.g. 1080p part1 + part2),
// detect its parts from sibling files so its AdditionalParts persist.
SetAdditionalPartsFromStack(altVideo, path);
allItems.Add(altVideo);
}
}
}
}
}
_persistenceService.SaveItems(allItems, cancellationToken);
foreach (var item in allItems)
{
RegisterItem(item);
}
@@ -2511,7 +2144,7 @@ namespace Emby.Server.Implementations.Library
item.ValidateImages();
await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false);
await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false);
RegisterItem(item);
}
@@ -2528,54 +2161,7 @@ namespace Emby.Server.Implementations.Library
item.DateLastSaved = DateTime.UtcNow;
}
// Resolve and add any local alternate version items that don't exist yet
// This ensures they exist in the database when LinkedChildren are processed
var allItems = new List<BaseItem>(items);
var parentFolder = parent as Folder;
var parentCollectionType = GetTopFolderContentType(parent);
foreach (var item in items)
{
if (item is Video video && video.LocalAlternateVersions.Length > 0)
{
var videoType = video.GetType();
foreach (var path in video.LocalAlternateVersions)
{
if (string.IsNullOrEmpty(path))
{
continue;
}
// Use the primary video's type for ID calculation to ensure consistency
var altId = GetNewItemId(path, videoType);
if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
{
// Alternate version doesn't exist, resolve and create it
// ensuring it has the same type as the primary video
var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
if (altVideo is not null)
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
// ResolveAlternateVersion only sees the alternate's primary file.
// If the alternate is itself a stack (e.g. 1080p part1 + part2),
// detect its parts from sibling files so its AdditionalParts persist.
SetAdditionalPartsFromStack(altVideo, path);
allItems.Add(altVideo);
}
}
}
}
}
_persistenceService.SaveItems(allItems, cancellationToken);
foreach (var item in allItems)
{
if (!items.Contains(item))
{
RegisterItem(item);
}
}
_itemRepository.SaveItems(items, cancellationToken);
if (parent is Folder folder)
{
@@ -2619,7 +2205,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
{
await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
}
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
@@ -2703,7 +2289,7 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
return [];
return new List<Folder>();
}
return GetCollectionFoldersInternal(item, allUserRootChildren);
@@ -3247,9 +2833,8 @@ namespace Emby.Server.Implementations.Library
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
// Apply .ignore rules
var filtered = fileSystemChildren.Where(c => !_dotIgnoreIgnoreRule.ShouldIgnore(c, owner)).ToList();
var isFolder = owner.IsFolder || (owner is Video video && (video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd));
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList();
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
if (ownerVideoInfo is null)
{
yield break;
@@ -3311,16 +2896,10 @@ namespace Emby.Server.Implementations.Library
extra.ExtraType = extraType;
}
// Only return items that are actual extras (have ExtraType set)
// Note: OwnerId and ParentId are set by RefreshExtras, not here,
// so that RefreshExtras can detect when they need updating and set ForceSave.
if (extra.ExtraType is not null)
{
extra.IsInMixedFolder = isInMixedFolder;
return extra;
}
return null;
extra.ParentId = Guid.Empty;
extra.OwnerId = owner.Id;
extra.IsInMixedFolder = isInMixedFolder;
return extra;
}
}
@@ -3339,7 +2918,7 @@ namespace Emby.Server.Implementations.Library
public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
{
return _peopleRepository.GetPeople(query).Items;
return _peopleRepository.GetPeople(query);
}
public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
@@ -3360,33 +2939,24 @@ namespace Emby.Server.Implementations.Library
return [];
}
public QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query)
public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
{
var queryResult = _peopleRepository.GetPeople(query);
var baseItems = queryResult.Items.Select(i =>
{
try
{
return GetPerson(i.Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "error retrieving BaseItem for person: {0}", i.Name);
return null;
}
})
.Where(i => i is not null)
.Where(i => query.User is null || i!.IsVisible(query.User))
.OfType<BaseItem>()
.ToList()
.AsReadOnly();
return new QueryResult<BaseItem>
return _peopleRepository.GetPeopleNames(query)
.Select(i =>
{
StartIndex = queryResult.StartIndex,
TotalRecordCount = queryResult.TotalRecordCount,
Items = baseItems,
};
try
{
return GetPerson(i);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting person");
return null;
}
})
.Where(i => i is not null)
.Where(i => query.User is null || i!.IsVisible(query.User))
.ToList()!; // null values are filtered out
}
public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
@@ -3815,46 +3385,5 @@ namespace Emby.Server.Implementations.Library
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
RemoveContentTypeOverrides(path);
}
/// <inheritdoc />
public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId)
{
var affectedParentIds = _linkedChildrenService.RerouteLinkedChildren(fromChildId, toChildId);
// Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents
foreach (var parentId in affectedParentIds)
{
if (GetItemById(parentId) is Folder parent)
{
foreach (var lc in parent.LinkedChildren)
{
if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId))
{
lc.ItemId = toChildId;
}
}
await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
}
}
}
/// <inheritdoc />
public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query)
{
if (query.User is not null)
{
AddUserToQuery(query, query.User);
}
SetTopParentOrAncestorIds(query);
return _itemRepository.GetQueryFiltersLegacy(query);
}
/// <inheritdoc />
public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
{
return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
}
}
}

View File

@@ -23,7 +23,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
@@ -424,7 +423,7 @@ namespace Emby.Server.Implementations.Library
MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLanguage);
}
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection, string originalLanguage)
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
if (userData is not null && userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
{
@@ -438,42 +437,7 @@ namespace Emby.Server.Implementations.Library
}
}
if (string.Equals(user.AudioLanguagePreference, "OriginalLanguage", StringComparison.OrdinalIgnoreCase))
{
originalLanguage = !string.IsNullOrWhiteSpace(originalLanguage)
? originalLanguage.Split(',').FirstOrDefault()
: null;
if (user.PlayDefaultAudioTrack)
{
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(
source.MediaStreams,
NormalizeLanguage(originalLanguage),
user.PlayDefaultAudioTrack);
return;
}
var originalIndex = source.MediaStreams.FindIndex(i => i.Type == MediaStreamType.Audio && i.IsOriginal);
if (!string.IsNullOrWhiteSpace(originalLanguage) && originalIndex != -1)
{
var mediaLanguageOriginal = source.MediaStreams[originalIndex].Language;
if (NormalizeLanguage(mediaLanguageOriginal).Contains(NormalizeLanguage(originalLanguage).FirstOrDefault()))
{
source.DefaultAudioStreamIndex = originalIndex;
return;
}
}
else if (originalIndex != -1)
{
source.DefaultAudioStreamIndex = originalIndex;
return;
}
}
var preferredAudio = string.Equals(user.AudioLanguagePreference, "OriginalLanguage", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(originalLanguage)
? NormalizeLanguage(originalLanguage)
: NormalizeLanguage(user.AudioLanguagePreference);
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
if (user.PlayDefaultAudioTrack)
@@ -498,19 +462,7 @@ namespace Emby.Server.Implementations.Library
var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections;
var originalLanguage = item?.OriginalLanguage ?? item switch
{
Episode episode => episode.Series.OriginalLanguage,
Video video => video.GetOwner() switch
{
Episode ownerEpisode => ownerEpisode.OriginalLanguage ?? ownerEpisode.Series.OriginalLanguage,
BaseItem owner => owner.OriginalLanguage,
null => null
},
_ => null
};
SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection, originalLanguage);
SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
}
else if (mediaType == MediaType.Audio)

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();
}
}
@@ -70,16 +60,6 @@ namespace Emby.Server.Implementations.Library
return match ? imdbId.ToString() : null;
}
// Allow tmdb as an alias for tmdbid
if (attribute.Equals("tmdbid", StringComparison.OrdinalIgnoreCase))
{
var tmdbValue = str.GetAttributeValue("tmdb");
if (tmdbValue is not null)
{
return tmdbValue;
}
}
return null;
}

View File

@@ -6,7 +6,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library;
@@ -15,22 +14,18 @@ namespace Emby.Server.Implementations.Library;
/// </summary>
public class PathManager : IPathManager
{
private readonly ILogger<PathManager> _logger;
private readonly IServerConfigurationManager _config;
private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="PathManager"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="config">The server configuration manager.</param>
/// <param name="appPaths">The application paths.</param>
public PathManager(
ILogger<PathManager> logger,
IServerConfigurationManager config,
IApplicationPaths appPaths)
{
_logger = logger;
_config = config;
_appPaths = appPaths;
}
@@ -40,43 +35,31 @@ public class PathManager : IPathManager
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
/// <inheritdoc />
public string? GetAttachmentPath(string mediaSourceId, string fileName)
public string GetAttachmentPath(string mediaSourceId, string fileName)
{
var folder = GetAttachmentFolderPath(mediaSourceId);
return folder is null ? null : Path.Combine(folder, fileName);
return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
}
/// <inheritdoc />
public string? GetAttachmentFolderPath(string mediaSourceId)
public string GetAttachmentFolderPath(string mediaSourceId)
{
if (!Guid.TryParse(mediaSourceId, out var parsed))
{
_logger.LogDebug("MediaSource Id '{MediaSourceId}' is not a GUID; no on-disk attachment folder.", mediaSourceId);
return null;
}
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
var id = parsed.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return Path.Join(AttachmentCachePath, id[..2], id);
}
/// <inheritdoc />
public string? GetSubtitleFolderPath(string mediaSourceId)
public string GetSubtitleFolderPath(string mediaSourceId)
{
if (!Guid.TryParse(mediaSourceId, out var parsed))
{
_logger.LogDebug("MediaSource Id '{MediaSourceId}' is not a GUID; no on-disk subtitle folder.", mediaSourceId);
return null;
}
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
var id = parsed.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return Path.Join(SubtitleCachePath, id[..2], id);
}
/// <inheritdoc />
public string? GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
{
var folder = GetSubtitleFolderPath(mediaSourceId);
return folder is null ? null : Path.Combine(folder, streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
}
/// <inheritdoc />
@@ -107,23 +90,12 @@ public class PathManager : IPathManager
public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
{
var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
List<string> paths = [];
var attachmentFolder = GetAttachmentFolderPath(mediaSourceId);
if (attachmentFolder is not null)
{
paths.Add(attachmentFolder);
}
var subtitleFolder = GetSubtitleFolderPath(mediaSourceId);
if (subtitleFolder is not null)
{
paths.Add(subtitleFolder);
}
paths.Add(GetTrickplayDirectory(item, false));
paths.Add(GetTrickplayDirectory(item, true));
paths.Add(GetChapterImageFolderPath(item));
return paths;
return [
GetAttachmentFolderPath(mediaSourceId),
GetSubtitleFolderPath(mediaSourceId),
GetTrickplayDirectory(item, false),
GetTrickplayDirectory(item, true),
GetChapterImageFolderPath(item)
];
}
}

View File

@@ -28,16 +28,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{
private readonly IImageProcessor _imageProcessor;
private readonly VideoListResolver _videoListResolver;
private static readonly CollectionType[] _validCollectionTypes =
[
private static readonly CollectionType[] _validCollectionTypes = new[]
{
CollectionType.movies,
CollectionType.homevideos,
CollectionType.musicvideos,
CollectionType.tvshows,
CollectionType.photos
];
};
/// <summary>
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
@@ -46,12 +45,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
/// <param name="videoListResolver">The video list resolver.</param>
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService, VideoListResolver videoListResolver)
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
: base(logger, namingOptions, directoryService)
{
_imageProcessor = imageProcessor;
_videoListResolver = videoListResolver;
}
/// <summary>
@@ -231,7 +228,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (collectionType == CollectionType.tvshows)
{
return ResolveVideos<Episode>(parent, files, true, collectionType, true);
return ResolveVideos<Episode>(parent, files, false, collectionType, true);
}
return null;
@@ -277,7 +274,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
.Where(f => f is not null)
.ToList();
var resolverResult = _videoListResolver.Resolve(videoInfos, supportMultiEditions, parseName, parent.ContainingFolderPath, collectionType);
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
var result = new MultiItemResolverResult
{
@@ -305,7 +302,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
ProductionYear = video.Year,
Name = parseName ? video.Name : firstVideo.Name,
AdditionalParts = additionalParts,
LocalAlternateVersions = video.AlternateVersions.Select(av => av.Files[0].Path).ToArray()
LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
};
SetVideoType(videoItem, firstVideo);
@@ -334,13 +331,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
for (var j = 0; j < current.AlternateVersions.Count; j++)
{
var alternate = current.AlternateVersions[j];
for (var k = 0; k < alternate.Files.Count; k++)
if (ContainsFile(current.AlternateVersions[j], file))
{
if (ContainsFile(alternate.Files[k], file))
{
return true;
}
return true;
}
}
}

View File

@@ -1,10 +1,8 @@
#nullable disable
using System;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Server.Implementations.Library;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -83,34 +81,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
episode.ParentIndexNumber = 1;
}
SetProviderIdFromPath(episode, args.Path);
return episode;
}
return null;
}
/// <summary>
/// Sets provider ids from the episode file name.
/// </summary>
/// <param name="item">The episode.</param>
/// <param name="path">The episode file path.</param>
private static void SetProviderIdFromPath(Episode item, string path)
{
var justName = Path.GetFileNameWithoutExtension(path.AsSpan());
var imdbId = justName.GetAttributeValue("imdbid");
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
var tvdbId = justName.GetAttributeValue("tvdbid");
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
var tvmazeId = justName.GetAttributeValue("tvmazeid");
item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId);
var tmdbId = justName.GetAttributeValue("tmdbid");
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
}
}
}

View File

@@ -1,15 +1,10 @@
#nullable disable
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Server.Implementations.Library;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using Microsoft.Extensions.Logging;
@@ -82,14 +77,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
var hasAnyVideo = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
.Any(file => _namingOptions.VideoFileExtensions.Contains(Path.GetExtension(file)));
if (!hasAnyVideo)
{
return null;
}
}
if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name))
@@ -104,31 +91,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
args.LibraryOptions.PreferredMetadataLanguage);
}
SetProviderIdFromPath(season, path);
return season;
}
return null;
}
/// <summary>
/// Sets provider ids from the season folder name.
/// </summary>
/// <param name="item">The season.</param>
/// <param name="path">The season folder path.</param>
private static void SetProviderIdFromPath(Season item, string path)
{
var justName = Path.GetFileName(path.AsSpan());
var tvdbId = justName.GetAttributeValue("tvdbid");
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
var tvmazeId = justName.GetAttributeValue("tvmazeid");
item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId);
var tmdbId = justName.GetAttributeValue("tmdbid");
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
}
}
}

View File

@@ -1,55 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
namespace Emby.Server.Implementations.Library.SimilarItems;
/// <summary>
/// Provides similar items for audio tracks.
/// </summary>
public class AudioSimilarItemsProvider : ILocalSimilarItemsProvider<Audio>
{
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="AudioSimilarItemsProvider"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public AudioSimilarItemsProvider(ILibraryManager libraryManager)
{
_libraryManager = libraryManager;
}
/// <inheritdoc/>
public string Name => "Local Genre/Tag";
/// <inheritdoc/>
public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
/// <inheritdoc/>
public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Audio item, SimilarItemsQuery query, CancellationToken cancellationToken)
{
var internalQuery = new InternalItemsQuery(query.User)
{
Genres = item.Genres,
Tags = item.Tags,
Limit = query.Limit,
DtoOptions = query.DtoOptions ?? new DtoOptions(),
ExcludeItemIds = [.. query.ExcludeItemIds],
ExcludeArtistIds = [.. query.ExcludeArtistIds],
IncludeItemTypes = [BaseItemKind.Audio],
EnableGroupByMetadataKey = false,
EnableTotalRecordCount = true,
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
};
return Task.FromResult(_libraryManager.GetItemList(internalQuery));
}
}

View File

@@ -1,94 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Configuration;
namespace Emby.Server.Implementations.Library.SimilarItems;
/// <summary>
/// Provides similar items for Live TV programs.
/// </summary>
public class LiveTvProgramSimilarItemsProvider : ILocalSimilarItemsProvider<LiveTvProgram>
{
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="LiveTvProgramSimilarItemsProvider"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="serverConfigurationManager">The server configuration manager.</param>
public LiveTvProgramSimilarItemsProvider(
ILibraryManager libraryManager,
IServerConfigurationManager serverConfigurationManager)
{
_libraryManager = libraryManager;
_serverConfigurationManager = serverConfigurationManager;
}
/// <inheritdoc/>
public string Name => "Local Genre/Tag";
/// <inheritdoc/>
public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
/// <inheritdoc/>
public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(LiveTvProgram item, SimilarItemsQuery query, CancellationToken cancellationToken)
{
BaseItemKind[] includeItemTypes;
bool enableGroupByMetadataKey;
bool enableTotalRecordCount;
if (item.IsMovie)
{
// Movie-like program
var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(BaseItemKind.Trailer);
itemTypes.Add(BaseItemKind.LiveTvProgram);
}
includeItemTypes = [.. itemTypes];
enableGroupByMetadataKey = true;
enableTotalRecordCount = false;
}
else if (item.IsSeries)
{
// Series-like program
includeItemTypes = [BaseItemKind.Series];
enableGroupByMetadataKey = false;
enableTotalRecordCount = true;
}
else
{
// Default - match same type
includeItemTypes = [item.GetBaseItemKind()];
enableGroupByMetadataKey = false;
enableTotalRecordCount = true;
}
var internalQuery = new InternalItemsQuery(query.User)
{
Genres = item.Genres,
Tags = item.Tags,
Limit = query.Limit,
DtoOptions = query.DtoOptions ?? new DtoOptions(),
ExcludeItemIds = [.. query.ExcludeItemIds],
IncludeItemTypes = includeItemTypes,
EnableGroupByMetadataKey = enableGroupByMetadataKey,
EnableTotalRecordCount = enableTotalRecordCount,
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
};
return Task.FromResult(_libraryManager.GetItemList(internalQuery));
}
}

View File

@@ -1,91 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
namespace Emby.Server.Implementations.Library.SimilarItems;
/// <summary>
/// Provides similar items for movies and trailers.
/// </summary>
public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie>, ILocalSimilarItemsProvider<Trailer>
{
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="MovieSimilarItemsProvider"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="serverConfigurationManager">The server configuration manager.</param>
public MovieSimilarItemsProvider(
ILibraryManager libraryManager,
IServerConfigurationManager serverConfigurationManager)
{
_libraryManager = libraryManager;
_serverConfigurationManager = serverConfigurationManager;
}
/// <inheritdoc/>
public string Name => "Local Genre/Tag";
/// <inheritdoc/>
public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
/// <inheritdoc/>
public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Movie item, SimilarItemsQuery query, CancellationToken cancellationToken)
{
return Task.FromResult(GetSimilarMovieItems(item, query));
}
/// <inheritdoc/>
public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Trailer item, SimilarItemsQuery query, CancellationToken cancellationToken)
{
return Task.FromResult(GetSimilarMovieItems(item, query));
}
bool ILocalSimilarItemsProvider.Supports(Type itemType)
=> typeof(Movie).IsAssignableFrom(itemType) || typeof(Trailer).IsAssignableFrom(itemType);
Task<IReadOnlyList<BaseItem>> ILocalSimilarItemsProvider.GetSimilarItemsAsync(BaseItem item, SimilarItemsQuery query, CancellationToken cancellationToken)
=> item switch
{
Movie movie => GetSimilarItemsAsync(movie, query, cancellationToken),
Trailer trailer => GetSimilarItemsAsync(trailer, query, cancellationToken),
_ => throw new ArgumentException($"Unsupported item type {item.GetType()}", nameof(item))
};
private IReadOnlyList<BaseItem> GetSimilarMovieItems(BaseItem item, SimilarItemsQuery query)
{
var includeItemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
includeItemTypes.Add(BaseItemKind.Trailer);
includeItemTypes.Add(BaseItemKind.LiveTvProgram);
}
var internalQuery = new InternalItemsQuery(query.User)
{
Genres = item.Genres,
Tags = item.Tags,
Limit = query.Limit,
DtoOptions = query.DtoOptions ?? new DtoOptions(),
ExcludeItemIds = [.. query.ExcludeItemIds],
IncludeItemTypes = [.. includeItemTypes],
EnableGroupByMetadataKey = true,
EnableTotalRecordCount = false,
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
};
return _libraryManager.GetItemList(internalQuery);
}
}

View File

@@ -1,55 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
namespace Emby.Server.Implementations.Library.SimilarItems;
/// <summary>
/// Provides similar items for music albums.
/// </summary>
public class MusicAlbumSimilarItemsProvider : ILocalSimilarItemsProvider<MusicAlbum>
{
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="MusicAlbumSimilarItemsProvider"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public MusicAlbumSimilarItemsProvider(ILibraryManager libraryManager)
{
_libraryManager = libraryManager;
}
/// <inheritdoc/>
public string Name => "Local Genre/Tag";
/// <inheritdoc/>
public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
/// <inheritdoc/>
public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(MusicAlbum item, SimilarItemsQuery query, CancellationToken cancellationToken)
{
var internalQuery = new InternalItemsQuery(query.User)
{
Genres = item.Genres,
Tags = item.Tags,
Limit = query.Limit,
DtoOptions = query.DtoOptions ?? new DtoOptions(),
ExcludeItemIds = [.. query.ExcludeItemIds],
ExcludeArtistIds = [.. query.ExcludeArtistIds],
IncludeItemTypes = [BaseItemKind.MusicAlbum],
EnableGroupByMetadataKey = false,
EnableTotalRecordCount = true,
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
};
return Task.FromResult(_libraryManager.GetItemList(internalQuery));
}
}

View File

@@ -1,55 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
namespace Emby.Server.Implementations.Library.SimilarItems;
/// <summary>
/// Provides similar items for music artists.
/// </summary>
public class MusicArtistSimilarItemsProvider : ILocalSimilarItemsProvider<MusicArtist>
{
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="MusicArtistSimilarItemsProvider"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public MusicArtistSimilarItemsProvider(ILibraryManager libraryManager)
{
_libraryManager = libraryManager;
}
/// <inheritdoc/>
public string Name => "Local Genre/Tag";
/// <inheritdoc/>
public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
/// <inheritdoc/>
public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(MusicArtist item, SimilarItemsQuery query, CancellationToken cancellationToken)
{
var internalQuery = new InternalItemsQuery(query.User)
{
Genres = item.Genres,
Tags = item.Tags,
Limit = query.Limit,
DtoOptions = query.DtoOptions ?? new DtoOptions(),
ExcludeItemIds = [.. query.ExcludeItemIds],
ExcludeArtistIds = [.. query.ExcludeArtistIds],
IncludeItemTypes = [BaseItemKind.MusicArtist],
EnableGroupByMetadataKey = false,
EnableTotalRecordCount = true,
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
};
return Task.FromResult(_libraryManager.GetItemList(internalQuery));
}
}

View File

@@ -1,54 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
namespace Emby.Server.Implementations.Library.SimilarItems;
/// <summary>
/// Provides similar items for TV series.
/// </summary>
public class SeriesSimilarItemsProvider : ILocalSimilarItemsProvider<Series>
{
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="SeriesSimilarItemsProvider"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public SeriesSimilarItemsProvider(ILibraryManager libraryManager)
{
_libraryManager = libraryManager;
}
/// <inheritdoc/>
public string Name => "Local Genre/Tag";
/// <inheritdoc/>
public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
/// <inheritdoc/>
public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Series item, SimilarItemsQuery query, CancellationToken cancellationToken)
{
var internalQuery = new InternalItemsQuery(query.User)
{
Genres = item.Genres,
Tags = item.Tags,
Limit = query.Limit,
DtoOptions = query.DtoOptions ?? new DtoOptions(),
ExcludeItemIds = [.. query.ExcludeItemIds],
IncludeItemTypes = [BaseItemKind.Series],
EnableGroupByMetadataKey = false,
EnableTotalRecordCount = true,
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
};
return Task.FromResult(_libraryManager.GetItemList(internalQuery));
}
}

View File

@@ -1,406 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.SimilarItems;
/// <summary>
/// Manages similar items providers and orchestrates similar items operations.
/// </summary>
public class SimilarItemsManager : ISimilarItemsManager
{
private readonly ILogger<SimilarItemsManager> _logger;
private readonly IServerApplicationPaths _appPaths;
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
private ISimilarItemsProvider[] _similarItemsProviders = [];
/// <summary>
/// Initializes a new instance of the <see cref="SimilarItemsManager"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="appPaths">The server application paths.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="fileSystem">The file system.</param>
public SimilarItemsManager(
ILogger<SimilarItemsManager> logger,
IServerApplicationPaths appPaths,
ILibraryManager libraryManager,
IFileSystem fileSystem)
{
_logger = logger;
_appPaths = appPaths;
_libraryManager = libraryManager;
_fileSystem = fileSystem;
}
/// <inheritdoc/>
public void AddParts(IEnumerable<ISimilarItemsProvider> providers)
{
_similarItemsProviders = providers.ToArray();
}
/// <inheritdoc/>
public IReadOnlyList<ISimilarItemsProvider> GetSimilarItemsProviders<T>()
where T : BaseItem
{
var itemType = typeof(T);
return _similarItemsProviders
.Where(p => (p is ILocalSimilarItemsProvider local && local.Supports(itemType))
|| (p is IRemoteSimilarItemsProvider remote && remote.Supports(itemType)))
.ToList();
}
/// <inheritdoc/>
public async Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(
BaseItem item,
IReadOnlyList<Guid> excludeArtistIds,
User? user,
DtoOptions dtoOptions,
int? limit,
LibraryOptions? libraryOptions,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(item);
ArgumentNullException.ThrowIfNull(excludeArtistIds);
var itemType = item.GetType();
var requestedLimit = limit ?? 50;
var itemKind = item.GetBaseItemKind();
// Ensure ProviderIds is included in DtoOptions for matching remote provider responses
if (!dtoOptions.Fields.Contains(ItemFields.ProviderIds))
{
dtoOptions.Fields = dtoOptions.Fields.Concat([ItemFields.ProviderIds]).ToArray();
}
// Local providers are always enabled. Remote providers must be explicitly enabled.
var localProviders = _similarItemsProviders
.OfType<ILocalSimilarItemsProvider>()
.Where(p => p.Supports(itemType))
.ToList();
var remoteProviders = _similarItemsProviders
.OfType<IRemoteSimilarItemsProvider>()
.Where(p => p.Supports(itemType));
var matchingProviders = new List<ISimilarItemsProvider>(localProviders);
var typeOptions = libraryOptions?.GetTypeOptions(itemType.Name);
if (typeOptions?.SimilarItemProviders?.Length > 0)
{
matchingProviders.AddRange(remoteProviders
.Where(p => typeOptions.SimilarItemProviders.Contains(p.Name, StringComparer.OrdinalIgnoreCase)));
}
var orderConfig = typeOptions?.SimilarItemProviderOrder is { Length: > 0 } order
? order
: typeOptions?.SimilarItemProviders;
var orderedProviders = matchingProviders
.OrderBy(p => GetConfiguredSimilarProviderOrder(orderConfig, p.Name))
.ToList();
var allResults = new List<(BaseItem Item, float Score)>();
var excludeIds = new HashSet<Guid> { item.Id };
foreach (var (providerOrder, provider) in orderedProviders.Index())
{
if (allResults.Count >= requestedLimit || cancellationToken.IsCancellationRequested)
{
break;
}
try
{
if (provider is ILocalSimilarItemsProvider localProvider)
{
var query = new SimilarItemsQuery
{
User = user,
Limit = requestedLimit - allResults.Count,
DtoOptions = dtoOptions,
ExcludeItemIds = [.. excludeIds],
ExcludeArtistIds = excludeArtistIds
};
var items = await localProvider.GetSimilarItemsAsync(item, query, cancellationToken).ConfigureAwait(false);
foreach (var (position, resultItem) in items.Index())
{
if (excludeIds.Add(resultItem.Id))
{
var score = CalculateScore(null, providerOrder, position);
allResults.Add((resultItem, score));
}
}
}
else if (provider is IRemoteSimilarItemsProvider remoteProvider)
{
var cachePath = GetSimilarItemsCachePath(provider.Name, itemType.Name, item.Id);
var cachedReferences = await TryReadSimilarItemsCacheAsync(cachePath, cancellationToken).ConfigureAwait(false);
if (cachedReferences is not null)
{
var resolvedItems = ResolveRemoteReferences(cachedReferences, providerOrder, user, dtoOptions, itemKind, excludeIds);
allResults.AddRange(resolvedItems);
continue;
}
var query = new SimilarItemsQuery
{
User = user,
Limit = requestedLimit - allResults.Count,
DtoOptions = dtoOptions,
ExcludeItemIds = [.. excludeIds],
ExcludeArtistIds = excludeArtistIds
};
// Collect references in batches and resolve against local library.
// Stop fetching once we have enough resolved local items.
const int BatchSize = 20;
var remaining = requestedLimit - allResults.Count;
var collectedReferences = new List<SimilarItemReference>();
var pendingBatch = new List<SimilarItemReference>();
await foreach (var reference in remoteProvider.GetSimilarItemsAsync(item, query, cancellationToken).ConfigureAwait(false))
{
collectedReferences.Add(reference);
pendingBatch.Add(reference);
if (pendingBatch.Count >= BatchSize)
{
var resolvedItems = ResolveRemoteReferences(pendingBatch, providerOrder, user, dtoOptions, itemKind, excludeIds);
allResults.AddRange(resolvedItems);
remaining -= resolvedItems.Count;
pendingBatch.Clear();
if (remaining <= 0)
{
break;
}
}
}
// Resolve any remaining references in the last partial batch
if (pendingBatch.Count > 0)
{
var resolvedItems = ResolveRemoteReferences(pendingBatch, providerOrder, user, dtoOptions, itemKind, excludeIds);
allResults.AddRange(resolvedItems);
}
if (collectedReferences.Count > 0 && provider.CacheDuration is not null)
{
await SaveSimilarItemsCacheAsync(cachePath, collectedReferences, provider.CacheDuration.Value, cancellationToken).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Similar items provider {ProviderName} failed for item {ItemId}", provider.Name, item.Id);
}
}
return allResults
.OrderByDescending(x => x.Score)
.Select(x => x.Item)
.Take(requestedLimit)
.ToList();
}
private List<(BaseItem Item, float Score)> ResolveRemoteReferences(
IReadOnlyList<SimilarItemReference> references,
int providerOrder,
User? user,
DtoOptions dtoOptions,
BaseItemKind itemKind,
HashSet<Guid> excludeIds)
{
if (references.Count == 0)
{
return [];
}
var resolvedById = new Dictionary<Guid, (BaseItem Item, float Score)>();
var providerLookup = new Dictionary<(string ProviderName, string ProviderId), (float? Score, int Position)>(StringTupleComparer.Instance);
foreach (var (position, match) in references.Index())
{
var lookupKey = (match.ProviderName, match.ProviderId);
if (!providerLookup.TryGetValue(lookupKey, out var existing))
{
providerLookup[lookupKey] = (match.Score, position);
}
else if (match.Score > existing.Score || (match.Score == existing.Score && position < existing.Position))
{
providerLookup[lookupKey] = (match.Score, position);
}
}
var allProviderIds = providerLookup
.GroupBy(kvp => kvp.Key.ProviderName)
.ToDictionary(g => g.Key, g => g.Select(x => x.Key.ProviderId).ToArray());
var query = new InternalItemsQuery(user)
{
HasAnyProviderIds = allProviderIds,
IncludeItemTypes = [itemKind],
DtoOptions = dtoOptions
};
var items = _libraryManager.GetItemList(query);
foreach (var item in items)
{
if (excludeIds.Contains(item.Id) || resolvedById.ContainsKey(item.Id))
{
continue;
}
foreach (var providerName in allProviderIds.Keys)
{
if (item.TryGetProviderId(providerName, out var itemProviderId) && providerLookup.TryGetValue((providerName, itemProviderId), out var matchInfo))
{
var score = CalculateScore(matchInfo.Score, providerOrder, matchInfo.Position);
if (!resolvedById.TryGetValue(item.Id, out var existing) || existing.Score < score)
{
excludeIds.Add(item.Id);
resolvedById[item.Id] = (item, score);
}
break;
}
}
}
return [.. resolvedById.Values];
}
private static float CalculateScore(float? matchScore, int providerOrder, int position)
{
// Use provider-supplied score if available, otherwise derive from position
var baseScore = matchScore ?? (1.0f - (position * 0.02f));
// Apply small boost based on provider order (higher priority providers get small bonus)
var priorityBoost = Math.Max(0, 10 - providerOrder) * 0.005f;
return Math.Clamp(baseScore + priorityBoost, 0f, 1f);
}
private static int GetConfiguredSimilarProviderOrder(string[]? orderConfig, string providerName)
{
if (orderConfig is null || orderConfig.Length == 0)
{
return int.MaxValue;
}
var index = Array.FindIndex(orderConfig, name => string.Equals(name, providerName, StringComparison.OrdinalIgnoreCase));
return index >= 0 ? index : int.MaxValue;
}
private string GetSimilarItemsCachePath(string providerName, string baseItemType, Guid itemId)
{
var dataPath = Path.Combine(
_appPaths.CachePath,
$"{providerName.ToLowerInvariant()}-similar-{baseItemType.ToLowerInvariant()}");
return Path.Combine(dataPath, $"{itemId.ToString("N", CultureInfo.InvariantCulture)}.json");
}
private async Task<List<SimilarItemReference>?> TryReadSimilarItemsCacheAsync(string cachePath, CancellationToken cancellationToken)
{
var fileInfo = _fileSystem.GetFileSystemInfo(cachePath);
if (!fileInfo.Exists || fileInfo.Length == 0)
{
return null;
}
try
{
var stream = File.OpenRead(cachePath);
await using (stream.ConfigureAwait(false))
{
var cache = await JsonSerializer.DeserializeAsync<SimilarItemsCache>(stream, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
if (cache?.References is not null && DateTime.UtcNow < cache.ExpiresAt)
{
return cache.References;
}
}
}
catch (IOException ex)
{
_logger.LogWarning(ex, "Failed to read similar items cache from {CachePath}", cachePath);
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "Failed to parse similar items cache from {CachePath}", cachePath);
}
return null;
}
private async Task SaveSimilarItemsCacheAsync(string cachePath, List<SimilarItemReference> references, TimeSpan cacheDuration, CancellationToken cancellationToken)
{
try
{
var directory = Path.GetDirectoryName(cachePath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var cache = new SimilarItemsCache
{
References = references,
ExpiresAt = DateTime.UtcNow.Add(cacheDuration)
};
var stream = File.Create(cachePath);
await using (stream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(stream, cache, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
}
}
catch (IOException ex)
{
_logger.LogWarning(ex, "Failed to save similar items cache to {CachePath}", cachePath);
}
}
private sealed class SimilarItemsCache
{
public List<SimilarItemReference>? References { get; set; }
public DateTime ExpiresAt { get; set; }
}
private sealed class StringTupleComparer : IEqualityComparer<(string Key, string Value)>
{
public static readonly StringTupleComparer Instance = new();
public bool Equals((string Key, string Value) x, (string Key, string Value) y)
=> string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase) &&
string.Equals(x.Value, y.Value, StringComparison.OrdinalIgnoreCase);
public int GetHashCode((string Key, string Value) obj)
=> HashCode.Combine(
StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Key),
StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Value));
}
}

View File

@@ -177,74 +177,53 @@ namespace Emby.Server.Implementations.Library
};
}
/// <inheritdoc />
public Dictionary<Guid, UserItemData> GetUserDataBatch(IReadOnlyList<BaseItem> items, User user)
private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
{
var result = new Dictionary<Guid, UserItemData>(items.Count);
var itemsNeedingQuery = new List<(BaseItem Item, List<string> Keys)>();
var cacheKey = GetCacheKey(user.InternalId, itemId);
foreach (var item in items)
if (_cache.TryGet(cacheKey, out var data))
{
var cacheKey = GetCacheKey(user.InternalId, item.Id);
if (_cache.TryGet(cacheKey, out var cachedData))
{
result[item.Id] = cachedData;
}
else
{
var userData = item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault();
if (userData is not null)
{
result[item.Id] = userData;
_cache.AddOrUpdate(cacheKey, userData);
}
else
{
var keys = item.GetUserDataKeys();
itemsNeedingQuery.Add((item, keys));
}
}
return data;
}
if (itemsNeedingQuery.Count == 0)
{
return result;
}
data = GetUserDataInternal(user.Id, itemId, keys);
// Build a single query for all missing items
var allItemIds = itemsNeedingQuery.Select(x => x.Item.Id).ToList();
var allKeys = itemsNeedingQuery.SelectMany(x => x.Keys).Distinct().ToList();
if (allKeys.Count > 0)
if (data is null)
{
using var context = _repository.CreateDbContext();
var userDataArray = context.UserData
.AsNoTracking()
.Where(e => e.UserId.Equals(user.Id))
.WhereOneOrMany(allItemIds, e => e.ItemId)
.WhereOneOrMany(allKeys, e => e.CustomDataKey)
.ToArray();
var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray());
foreach (var (item, keys) in itemsNeedingQuery)
return new UserItemData()
{
UserItemData userData;
if (userDataByItem.TryGetValue(item.Id, out var itemUserData) && itemUserData.Length > 0)
{
var directDataReference = itemUserData.FirstOrDefault(e => e.CustomDataKey == item.Id.ToString("N"));
userData = directDataReference is not null ? Map(directDataReference) : Map(itemUserData.First());
}
else
{
userData = new UserItemData { Key = keys.Count > 0 ? keys[0] : string.Empty };
}
result[item.Id] = userData;
var cacheKey = GetCacheKey(user.InternalId, item.Id);
_cache.AddOrUpdate(cacheKey, userData);
}
Key = keys[0],
};
}
return result;
return _cache.GetOrAdd(cacheKey, _ => data);
}
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
{
if (keys.Count == 0)
{
return null;
}
using var context = _repository.CreateDbContext();
var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
if (userData.Length > 0)
{
var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
if (directDataReference is not null)
{
return Map(directDataReference);
}
return Map(userData.First());
}
return new UserItemData
{
Key = keys.Last()!
};
}
/// <summary>

View File

@@ -59,8 +59,8 @@ namespace Emby.Server.Implementations.Library
var collectionFolder = folder as ICollectionFolder;
var folderViewType = collectionFolder?.CollectionType;
// Playlist and BoxSet libraries require special handling because the folder only references linked items
if (folderViewType == CollectionType.playlists || folderViewType == CollectionType.boxsets)
// Playlist library requires special handling because the folder only references user playlists
if (folderViewType == CollectionType.playlists)
{
var items = folder.GetItemList(new InternalItemsQuery(user)
{
@@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Library
list = list.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
}
var sorted = _libraryManager.Sort(list, user, [ItemSortBy.SortName], SortOrder.Ascending).ToList();
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
return list
@@ -205,7 +205,7 @@ namespace Emby.Server.Implementations.Library
var libraryItems = GetItemsForLatestItems(request.User, request, options);
var list = new List<Tuple<BaseItem, List<BaseItem>>>();
var containerIndexMap = new Dictionary<Guid, int>();
foreach (var item in libraryItems)
{
// Only grab the index container for media
@@ -213,16 +213,20 @@ namespace Emby.Server.Implementations.Library
if (container is null)
{
list.Add(new Tuple<BaseItem, List<BaseItem>>(null!, new List<BaseItem> { item }));
}
else if (containerIndexMap.TryGetValue(container.Id, out var existingIndex))
{
list[existingIndex].Item2.Add(item);
list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item }));
}
else
{
containerIndexMap[container.Id] = list.Count;
list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
var current = list.FirstOrDefault(i => i.Item1 is not null && i.Item1.Id.Equals(container.Id));
if (current is not null)
{
current.Item2.Add(item);
}
else
{
list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
}
}
if (list.Count >= request.Limit)
@@ -251,7 +255,7 @@ namespace Emby.Server.Implementations.Library
return _channelManager.GetLatestChannelItemsInternal(
new InternalItemsQuery(user)
{
ChannelIds = [parentId],
ChannelIds = new[] { parentId },
IsPlayed = request.IsPlayed,
StartIndex = request.StartIndex,
Limit = request.Limit,
@@ -297,11 +301,11 @@ namespace Emby.Server.Implementations.Library
{
if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
{
includeItemTypes = [BaseItemKind.Movie];
includeItemTypes = new[] { BaseItemKind.Movie };
}
else if (hasCollectionType.All(i => i.CollectionType == CollectionType.tvshows))
{
includeItemTypes = [BaseItemKind.Episode];
includeItemTypes = new[] { BaseItemKind.Episode };
}
}
}
@@ -340,29 +344,29 @@ namespace Emby.Server.Implementations.Library
}
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0
?
[
? new[]
{
BaseItemKind.Person,
BaseItemKind.Studio,
BaseItemKind.Year,
BaseItemKind.MusicGenre,
BaseItemKind.Genre
]
}
: Array.Empty<BaseItemKind>();
var query = new InternalItemsQuery(user)
{
IncludeItemTypes = includeItemTypes,
OrderBy =
[
OrderBy = new[]
{
(ItemSortBy.DateCreated, SortOrder.Descending),
(ItemSortBy.SortName, SortOrder.Descending),
(ItemSortBy.ProductionYear, SortOrder.Descending)
],
},
IsFolder = includeItemTypes.Length == 0 ? false : null,
ExcludeItemTypes = excludeItemTypes,
IsVirtualItem = false,
Limit = limit * 2,
Limit = limit * 5,
IsPlayed = isPlayed,
DtoOptions = options,
MediaTypes = mediaTypes
@@ -390,12 +394,6 @@ namespace Emby.Server.Implementations.Library
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
}
if (collectionType == CollectionType.movies)
{
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.movies);
}
}
return _libraryManager.GetItemList(query, parents);

View File

@@ -50,40 +50,21 @@ public class ArtistsValidator
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetAllArtistNames();
var existingArtistIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.MusicArtist]
}).ToHashSet();
var existingArtists = _libraryManager.GetArtists(names);
var numComplete = 0;
var count = names.Count;
var refreshed = 0;
foreach (var name in names)
{
try
{
MusicArtist? item = null;
if (existingArtists.TryGetValue(name, out var artists) && artists.Length > 0)
{
item = artists.OrderBy(i => i.IsAccessedByName ? 1 : 0).First();
}
var item = _libraryManager.GetArtist(name);
// Fall back to GetArtist if not found (creates new item if needed)
item ??= _libraryManager.GetArtist(name);
var isNew = !existingArtistIds.Contains(item.Id);
var neverRefreshed = item.DateLastRefreshed == default;
if (isNew || neverRefreshed)
{
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
refreshed++;
}
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
@@ -99,23 +80,30 @@ public class ArtistsValidator
progress.Report(percent);
}
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new artists out of {TotalCount} total", refreshed, count);
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.MusicArtist],
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>()
.Where(item => item.IsAccessedByName)
.ToList();
}).Cast<MusicArtist>().ToList();
foreach (var item in deadEntities)
{
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
}
if (!item.IsAccessedByName)
{
continue;
}
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
}
progress.Report(100);
}

View File

@@ -74,7 +74,7 @@ public class CollectionPostScanTask : ILibraryPostScanTask
foreach (var m in movies)
{
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName) && !movie.PrimaryVersionId.HasValue)
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
{
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
{

View File

@@ -1,6 +1,5 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
@@ -49,40 +48,17 @@ public class GenresValidator
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetGenreNames();
var existingGenreIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Genre]
}).ToHashSet();
var existingGenres = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Genre]
}).Cast<Genre>()
.GroupBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
var numComplete = 0;
var count = names.Count;
var refreshed = 0;
foreach (var name in names)
{
try
{
Genre? item = null;
if (existingGenres.TryGetValue(name, out var existingGenre))
{
item = existingGenre;
}
var item = _libraryManager.GetGenre(name);
// Fall back to GetGenre if not found (creates new item if needed)
item ??= _libraryManager.GetGenre(name);
if (!existingGenreIds.Contains(item.Id))
{
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
refreshed++;
}
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -102,8 +78,6 @@ public class GenresValidator
progress.Report(percent);
}
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new genres out of {TotalCount} total", refreshed, count);
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
@@ -114,9 +88,15 @@ public class GenresValidator
foreach (var item in deadEntities)
{
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
}
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
}
progress.Report(100);
}

View File

@@ -1,9 +1,6 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
@@ -48,25 +45,17 @@ public class MusicGenresValidator
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetMusicGenreNames();
var existingMusicGenreIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.MusicGenre]
}).ToHashSet();
var numComplete = 0;
var count = names.Count;
var refreshed = 0;
foreach (var name in names)
{
try
{
var item = _libraryManager.GetMusicGenre(name);
if (!existingMusicGenreIds.Contains(item.Id))
{
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
refreshed++;
}
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -86,8 +75,6 @@ public class MusicGenresValidator
progress.Report(percent);
}
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new music genres out of {TotalCount} total", refreshed, count);
progress.Report(100);
}
}

View File

@@ -109,7 +109,7 @@ public class PeopleValidator
var i = 0;
foreach (var item in deadEntities.Chunk(500))
{
_libraryManager.DeleteItemsUnsafeFast(item, true);
_libraryManager.DeleteItemsUnsafeFast(item);
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
@@ -50,40 +49,17 @@ public class StudiosValidator
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetStudioNames();
var existingStudioIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Studio]
}).ToHashSet();
var existingStudios = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Studio]
}).Cast<Studio>()
.GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
var numComplete = 0;
var count = names.Count;
var refreshed = 0;
foreach (var name in names)
{
try
{
Studio? item = null;
if (existingStudios.TryGetValue(name, out var existingStudio))
{
item = existingStudio;
}
var item = _libraryManager.GetStudio(name);
// Fall back to GetStudio if not found (creates new item if needed)
item ??= _libraryManager.GetStudio(name);
if (!existingStudioIds.Contains(item.Id))
{
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
refreshed++;
}
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -103,8 +79,6 @@ public class StudiosValidator
progress.Report(percent);
}
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new studios out of {TotalCount} total", refreshed, count);
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Studio],
@@ -115,9 +89,15 @@ public class StudiosValidator
foreach (var item in deadEntities)
{
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
}
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
}
progress.Report(100);
}

View File

@@ -1,3 +1,3 @@
{
"AppDeviceValues": "Апп: {0}, Априбор: {1}"
"Albums": "аальбомқәа"
}

View File

@@ -1,8 +1,11 @@
{
"Artists": "Kunstenare",
"Channels": "Kanale",
"Folders": "Lêergidse",
"Favorites": "Gunstelinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album kunstenaars",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
"Movies": "Flieks",
@@ -10,13 +13,24 @@
"HeaderContinueWatching": "Hou aan kyk",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Foto's",
"Playlists": "Snitlyste",
"HeaderFavoriteArtists": "Gunsteling Kunstenaars",
"HeaderFavoriteAlbums": "Gunsteling Albums",
"Sync": "Sinkroniseer",
"HeaderFavoriteSongs": "Gunsteling Liedjies",
"Songs": "Liedjies",
"DeviceOnlineWithName": "{0} is aanlyn",
"DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
"HeaderLiveTV": "Lewendige TV",
"Application": "Program",
"AppDeviceValues": "App: {0}, Toestel: {1}",
"VersionNumber": "Weergawe {0}",
"ValueHasBeenAddedToLibrary": "{0} is by jou media biblioteek bygevoeg",
"UserStoppedPlayingItemWithValues": "{0} het klaar {1} op {2} gespeel",
"UserStartedPlayingItemWithValues": "{0} is besig om {1} op {2} te speel",
"UserPolicyUpdatedWithName": "Gebruiker beleid is verander vir {0}",
"UserPasswordChangedWithName": "Gebruiker {0} se wagwoord is verander",
"UserOnlineFromDevice": "{0} is aanlyn van {1}",
"UserOfflineFromDevice": "{0} is ontkoppel van {1}",
@@ -24,13 +38,19 @@
"UserDownloadingItemWithValues": "{0} is besig om {1} af te laai",
"UserDeletedWithName": "Gebruiker {0} is verwyder",
"UserCreatedWithName": "Gebruiker {0} is geskep",
"User": "Gebruiker",
"TvShows": "TV Programme",
"System": "Stelsel",
"SubtitleDownloadFailureFromForItem": "Ondertitels het misluk om af te laai van {0} vir {1}",
"StartupEmbyServerIsLoading": "Jellyfin Bediener is besig om te laai. Probeer weer in 'n kort tyd.",
"ServerNameNeedsToBeRestarted": "{0} moet herbegin word",
"ScheduledTaskStartedWithName": "{0} het begin",
"ScheduledTaskFailedWithName": "{0} het misluk",
"ProviderValue": "Voorsiener: {0}",
"PluginUpdatedWithName": "{0} was opgedateer",
"PluginUninstalledWithName": "{0} was verwyder",
"PluginInstalledWithName": "{0} is geïnstalleer",
"Plugin": "Inprop module",
"NotificationOptionVideoPlaybackStopped": "Video terugspeel het gestop",
"NotificationOptionVideoPlayback": "Video terugspeel het begin",
"NotificationOptionUserLockedOut": "Gebruiker uitgeslyt",
@@ -54,14 +74,23 @@
"MusicVideos": "Musiek Videos",
"Music": "Musiek",
"MixedContent": "Gemengde inhoud",
"MessageServerConfigurationUpdated": "Bediener konfigurasie is opgedateer",
"MessageNamedServerConfigurationUpdatedWithValue": "Bediener konfigurasie seksie {0} is opgedateer",
"MessageApplicationUpdatedTo": "Jellyfin Bediener is opgedateer na {0}",
"MessageApplicationUpdated": "Jellyfin Bediener is opgedateer",
"Latest": "Nuutste",
"LabelRunningTimeValue": "Werktyd: {0}",
"LabelIpAddressValue": "IP adres: {0}",
"ItemRemovedWithName": "{0} is uit versameling verwyder",
"ItemAddedWithName": "{0} is by die versameling gevoeg",
"HomeVideos": "Tuis Videos",
"HeaderRecordingGroups": "Groep Opnames",
"Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"ChapterNameValue": "Hoofstuk {0}",
"CameraImageUploadedFrom": "'n Nuwe kamera foto is opgelaai vanaf {0}",
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
"Albums": "Albums",
"TasksChannelsCategory": "Internet kanale",
"TasksApplicationCategory": "aansoek",
"TasksLibraryCategory": "biblioteek",
@@ -99,12 +128,12 @@
"TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling.",
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
"TaskAudioNormalization": "Odio Normalisering",
"TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
"TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.",
"TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
"TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
"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

@@ -1,111 +1,141 @@
{
"AppDeviceValues": "التطبيق: {0}، الجهاز: {1}",
"Artists": "الفنانون",
"AuthenticationSucceededWithUserName": "تمت مصادقة {0} بنجاح",
"Albums": "ألبومات",
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
"Application": "تطبيق",
"Artists": "فنانون",
"AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}",
"Books": "الكتب",
"CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}",
"Channels": "القنوات",
"ChapterNameValue": "الفصل {0}",
"Collections": "المجموعات",
"FailedLoginAttemptWithUserName": "محاولة تسجيل دخول فاشلة من {0}",
"Collections": "مجموعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}",
"Favorites": "المفضلة",
"Folders": "المجلدات",
"Genres": "الأنواع",
"Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم",
"HeaderContinueWatching": "متابعة المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
"HeaderFavoriteShows": "المسلسلات المفضلة",
"HeaderLiveTV": "البث التلفزيوني المباشر",
"HeaderFavoriteSongs": "الأغاني المفضلة",
"HeaderLiveTV": "التلفاز المباشر",
"HeaderNextUp": "التالي",
"HomeVideos": "فيديوهات منزلية",
"Inherit": "وراثة",
"LabelIpAddressValue": "عنوان IP: {0}",
"HeaderRecordingGroups": "مجموعات التسجيل",
"HomeVideos": "الفيديوهات الشخصية",
"Inherit": "توريث",
"ItemAddedWithName": "أُضيف {0} للمكتبة",
"ItemRemovedWithName": "أُزيل {0} من المكتبة",
"LabelIpAddressValue": "عنوان الآي بي: {0}",
"LabelRunningTimeValue": "مدة التشغيل: {0}",
"Latest": "الأحدث",
"MessageApplicationUpdated": "حُدث خادم Jellyfin",
"MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}",
"MessageServerConfigurationUpdated": "حُدثت إعدادات الخادم",
"MixedContent": "محتوى مختلط",
"Movies": "الأفلام",
"Music": "الموسيقى",
"MusicVideos": "الفيديوهات الموسيقية",
"NameInstallFailed": "فشل تثبيت {0}",
"NameSeasonNumber": "الموسم {0}",
"NameSeasonUnknown": "موسم غير معروف",
"NewVersionIsAvailable": "يتوفر إصدار جديد من خادم Jellyfin للتنزيل.",
"NotificationOptionApplicationUpdateAvailable": "تحديث التطبيق متاح",
"NotificationOptionApplicationUpdateInstalled": "تم تثبيت تحديث التطبيق",
"NotificationOptionAudioPlayback": "بدأ تشغيل الصوت",
"NotificationOptionAudioPlaybackStopped": "توقف تشغيل الصوت",
"NotificationOptionCameraImageUploaded": "تم رفع صورة كاميرا",
"NotificationOptionInstallationFailed": "فشل التثبيت",
"NotificationOptionNewLibraryContent": "تمت إضافة محتوى جديد",
"NotificationOptionPluginError": "خطأ في الملحق",
"NotificationOptionPluginInstalled": م تثبيت الملحق",
"NameSeasonUnknown": "الموسم غير معروف",
"NewVersionIsAvailable": "نسخة جديدة من خادم Jellyfin متوفرة للتحميل.",
"NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق",
"NotificationOptionApplicationUpdateInstalled": "نُصب تحديث التطبيق",
"NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
"NotificationOptionAudioPlaybackStopped": "أُوقف تشغيل المقطع الصوتي",
"NotificationOptionCameraImageUploaded": ُفعت صورة الكاميرا",
"NotificationOptionInstallationFailed": "فشل في التثبيت",
"NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا",
"NotificationOptionPluginError": "فشل في الملحق",
"NotificationOptionPluginInstalled": "ثُبتت الملحق",
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
"NotificationOptionPluginUpdateInstalled": "تم تحديث الملحق",
"NotificationOptionServerRestartRequired": "مطلوب إعادة تشغيل الخادم",
"NotificationOptionTaskFailed": "فشل المهمة المجدولة",
"NotificationOptionUserLockedOut": "تم قفل حساب المستخدم",
"NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
"NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
"NotificationOptionTaskFailed": "فشل في المهمة المجدولة",
"NotificationOptionUserLockedOut": "تم إقفال حساب المستخدم",
"NotificationOptionVideoPlayback": "بدأ تشغيل الفيديو",
"NotificationOptionVideoPlaybackStopped": وقف تشغيل الفيديو",
"NotificationOptionVideoPlaybackStopped": م إيقاف تشغيل الفيديو",
"Photos": "الصور",
"Playlists": "قوائم التشغيل",
"Plugin": "الملحق",
"PluginInstalledWithName": "تم تثبيت {0}",
"PluginUninstalledWithName": "تمت إزالة {0}",
"PluginUpdatedWithName": "تم تحديث {0}",
"ScheduledTaskFailedWithName": "فشلت {0}",
"Shows": "المسلسلات",
"StartupEmbyServerIsLoading": "يتم الآن تحميل خادم Jellyfin. يرجى المحاولة مرة أخرى بعد قليل.",
"SubtitleDownloadFailureFromForItem": "فشل تنزيل الترجمات من {0} لـ {1}",
"ProviderValue": "المزود: {0}",
"ScheduledTaskFailedWithName": "فشلت العملية {0}",
"ScheduledTaskStartedWithName": "تم بدء العملية {0}",
"ServerNameNeedsToBeRestarted": "يحتاج {0} لإعادة التشغيل",
"Shows": "العروض",
"Songs": "الأغاني",
"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}",
"UserStartedPlayingItemWithValues": "{0} يقوم بتشغيل {1} على {2}",
"UserStoppedPlayingItemWithValues": "أنهى {0} تشغيل {1} على {2}",
"UserDownloadingItemWithValues": "يقوم {0} بتنزيل {1}",
"UserLockedOutWithName": "تم منع المستخدم {0} من الدخول",
"UserOfflineFromDevice": "تم قطع اتصال {0} من {1}",
"UserOnlineFromDevice": "{0} متصل عبر {1}",
"UserPasswordChangedWithName": "تم تغيير كلمة السر للمستخدم {0}",
"UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم {0}",
"UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}",
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
"ValueSpecialEpisodeName": "حلقة خاصه - {0}",
"VersionNumber": "الإصدار {0}",
"TaskCleanCacheDescription": "يحذف ملفات ذاكرة التخزين المؤقت التي لم يعد النظام بحاجة إليها.",
"TaskCleanCache": "تنظيف مجلد ذاكرة التخزين المؤقت",
"TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
"TaskCleanCache": "حذف الملفات المؤقتة",
"TasksChannelsCategory": "قنوات الإنترنت",
"TasksLibraryCategory": "المكتبة",
"TasksMaintenanceCategory": "الصيانة",
"TaskRefreshLibraryDescription": "يفحص مكتبة المحتوى الخاصة بك بحثاً عن ملفات جديدة ويحدّث البيانات الوصفية.",
"TaskRefreshLibrary": "فحص مكتبة المحتوى",
"TaskRefreshChapterImagesDescription": "ينشئ صوراً مصغرة للفيديوهات التي تحتوي على فصول.",
"TaskRefreshChapterImages": "استخراج صور الفصول",
"TasksApplicationCategory": "التطبيق",
"TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت عن الترجمات المفقودة بناءً على إعدادات البيانات الوصفية.",
"TaskDownloadMissingSubtitles": نزيل الترجمات المفقودة",
"TaskRefreshChannelsDescription": "يحدّث معلومات قنوات الإنترنت.",
"TaskRefreshChannels": "تحديث القنوات",
"TaskCleanTranscodeDescription": "يحذف ملفات تحويل الترميز التي مر عليها أكثر من يوم واحد.",
"TaskCleanTranscode": "تنظيف مجلد تحويل الترميز",
"TaskUpdatePluginsDescription": نزّل ويثبّت التحديثات للملحقات المهيأة للتحديث التلقائي.",
"TaskUpdatePlugins": "تحديث الملحقات",
"TaskRefreshPeopleDescription": "يحدّث البيانات الوصفية للممثلين والمخرجين في مكتبة المحتوى الخاصة بك.",
"TaskRefreshPeople": "تحديث الأشخاص",
"TaskCleanLogsDescription": "يحذف ملفات السجل التي يزيد عمرها عن {0} أيام.",
"TaskCleanLogs": "تنظيف مجلد السجلات",
"TaskCleanActivityLogDescription": "يحذف إدخالات سجل النشاط الأقدم من العمر المحدد.",
"TaskCleanActivityLog": "تنظيف سجل النشاط",
"Default": "الافتراضي",
"Undefined": "غير محدد",
"Forced": "إجباري",
"TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقلل المساحة الحرة. قد يؤدي تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات على قاعدة البيانات إلى تحسين الأداء.",
"TasksLibraryCategory": "مكتبة",
"TasksMaintenanceCategory": "صيانة",
"TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يُحدث البيانات الوصفية.",
"TaskRefreshLibrary": "افحص مكتبة الوسائط",
"TaskRefreshChapterImagesDescription": ُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
"TaskRefreshChapterImages": "استخراج صور الفصل",
"TasksApplicationCategory": "تطبيق",
"TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت على الترجمات الناقصة استنادا على البيانات الوصفية.",
"TaskDownloadMissingSubtitles": حميل الترجمات الناقصة",
"TaskRefreshChannelsDescription": "يحدث معلومات قنوات الإنترنت.",
"TaskRefreshChannels": "إعادة تحديث القنوات",
"TaskCleanTranscodeDescription": "يحذف ملفات الترميز الأقدم من يوم واحد.",
"TaskCleanTranscode": "حذف ما بمجلد الترميز",
"TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.",
"TaskUpdatePlugins": "تحديث الإضافات",
"TaskRefreshPeopleDescription": قوم بتحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
"TaskRefreshPeople": "إعادة تحميل الأشخاص",
"TaskCleanLogsDescription": "يحذف السجلات الأقدم من {0} يوم.",
"TaskCleanLogs": "حذف مسار السجل",
"TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الذي تم تحديده.",
"TaskCleanActivityLog": "حذف سجل الأنشطة",
"Default": "افتراضي",
"Undefined": "غير معرف",
"Forced": "ملحقة",
"TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات في قاعدة البيانات قد تؤدي إلى تحسين الأداء.",
"TaskOptimizeDatabase": "تحسين قاعدة البيانات",
"TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لإنشاء قوائم تشغيل HLS أكثر دقة. قد يستغرق تشغيل هذه المهمة وقتاً طويلاً.",
"TaskKeyframeExtractor": "مستخرج الإطارات الرئيسية",
"TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.",
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
"External": "خارجي",
"HearingImpaired": "لضعاف السمع",
"TaskRefreshTrickplayImages": "إنشاء صور معاينات التنقل (Trickplay)",
"TaskRefreshTrickplayImagesDescription": نشئ صور معاينات التنقل السريع للفيديوهات في المكتبات المفعّلة.",
"TaskAudioNormalization": "تطبيع الصوت",
"TaskAudioNormalizationDescription": "يفحص الملفات لجمع بيانات تطبيع الصوت.",
"TaskDownloadMissingLyrics": "تنزيل الكلمات المفقودة",
"TaskDownloadMissingLyricsDescription": "ينزّل الكلمات للأغاني.",
"TaskExtractMediaSegments": "فحص مقاطع المحتوى",
"TaskExtractMediaSegmentsDescription": "يستخرج أو يحصل على مقاطع المحتوى من الملحقات المفعّلة لمقاطع المحتوى (MediaSegment).",
"TaskMoveTrickplayImages": "نقل موقع صور معاينات التنقل",
"TaskMoveTrickplayImagesDescription": نقل ملفات معاينات التنقل الحالية وفقاً لإعدادات المكتبة.",
"HearingImpaired": "ضعاف السمع",
"TaskRefreshTrickplayImages": "توليد صور المعاينة السريعة",
"TaskRefreshTrickplayImagesDescription": ُولّد معاينات تنقل سريع لمقاطع الفيديو ضمن المكتبات المفعّلة.",
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
"TaskAudioNormalization": "تسوية الصوت",
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.",
"TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة",
"TaskDownloadMissingLyricsDescription": "كلمات",
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
"TaskExtractMediaSegmentsDescription": ستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
"CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل.",
"Original": "فريد"
"CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
}

View File

@@ -1,13 +1,18 @@
{
"Albums": "এলবাম",
"Application": "আবেদন",
"AppDeviceValues": "এপ্‌: {0}, ডিভাইচ: {1}",
"Artists": "শিল্পী",
"Channels": "চেনেলস",
"Default": "ডিফল্ট",
"AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত",
"Books": "পুস্তক",
"Movies": "চলচ্চিত্ৰ",
"CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}",
"Collections": "সংগ্রহ",
"HeaderFavoriteShows": "প্রিয় শোসমূহ",
"Latest": "শেহতীয়া",
"MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে",
"MixedContent": "মিশ্ৰিত সমগ্ৰতা",
"NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.",
"NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল",
@@ -16,14 +21,20 @@
"Folders": "ফোল্ডাৰ",
"Forced": "বলপূর্বক",
"Genres": "শ্রেণী",
"HeaderAlbumArtists": "অ্যালবাম শিল্পী",
"HeaderContinueWatching": "দেখা চালিয়ে যান",
"FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}",
"HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ",
"HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ",
"HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ",
"HeaderFavoriteSongs": "প্ৰিয় গীত",
"HeaderLiveTV": "প্ৰতিবেদন টিভি",
"HeaderNextUp": "পৰৱৰ্তী অংশ",
"HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ",
"HearingImpaired": "শ্ৰবণ অক্ষম",
"HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ",
"Inherit": "উত্তপ্ত কৰা",
"MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে",
"NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ",
"NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল",
"NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল",

View File

@@ -1,24 +1,36 @@
{
"Sync": "Сінхранізаваць",
"Playlists": "Плэй-лісты",
"Latest": "Апошняе",
"LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} даданы ў бібліятэку",
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны",
"UserCreatedWithName": "Карыстальнік {0} быў створаны",
"Albums": "Альбомы",
"Application": "Праграма",
"AuthenticationSucceededWithUserName": "{0} паспяхова аўтарызаваны",
"Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі",
"Default": радвызначана",
"Default": а змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі",
"Favorites": "Абранае",
"External": "Знешні",
"Genres": "Жанры",
"HeaderContinueWatching": "Працягнуць прагляд",
"HeaderFavoriteAlbums": "Абраныя альбомы",
"HeaderFavoriteEpisodes": "Абраныя серыі",
"HeaderFavoriteShows": "Абраныя шоу",
"HeaderFavoriteSongs": "Абраныя песні",
"HeaderLiveTV": "Прамы эфір",
"HeaderAlbumArtists": "Выканаўцы альбома",
"LabelRunningTimeValue": "Працягласць: {0}",
"HomeVideos": "Хатнія відэа",
"ItemRemovedWithName": "{0} выдалены з бібліятэкі",
"MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да версіі {0}",
"Movies": "Фільмы",
"Music": "Музыка",
"MusicVideos": "Музычныя кліпы",
@@ -29,13 +41,19 @@
"NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна ўсталявана",
"NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера",
"Photos": "Фотаздымкі",
"Plugin": "Плагін",
"PluginUninstalledWithName": "{0} быў выдалены",
"PluginUpdatedWithName": "{0} быў абноўлены",
"ProviderValue": "Пастаўшчык: {0}",
"Songs": "Песні",
"System": "Сістэма",
"User": "Карыстальнік",
"UserDeletedWithName": "Карыстальнік {0} быў выдалены",
"UserDownloadingItemWithValues": "{0} спампоўваецца {1}",
"TaskOptimizeDatabase": "Аптымізацыя базы даных",
"Artists": "Выканаўцы",
"UserOfflineFromDevice": "{0} адлучыўся ад {1}",
"UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
"TaskCleanActivityLogDescription": "Выдаляе запісы старэйшыя за зададзены ўзрост ў журнале актыўнасці.",
"TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.",
"TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.",
@@ -47,17 +65,24 @@
"TasksApplicationCategory": "Праграма",
"AppDeviceValues": "Праграма: {0}, Прылада: {1}",
"Books": "Кнігі",
"CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}",
"DeviceOfflineWithName": "{0} адлучыўся",
"DeviceOnlineWithName": "{0} падлучаны",
"Forced": "Прымусова",
"HeaderRecordingGroups": "Групы запісаў",
"HeaderNextUp": "Наступнае",
"HeaderFavoriteArtists": "Абраныя выканаўцы",
"HearingImpaired": "Са слабым слыхам",
"Inherit": "Атрымаць у спадчыну",
"MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера (секцыя {0}) абноўлена",
"MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена",
"MixedContent": "Змешаны змест",
"NameSeasonUnknown": "Невядомы сезон",
"NotificationOptionInstallationFailed": "Збой усталёўкі",
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыя спынена",
"NotificationOptionAudioPlayback": "Прайграванне аўдыя пачалося",
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена",
"NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося",
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
"NotificationOptionPluginError": "Збой плагіна",
"NotificationOptionPluginUninstalled": "Плагін выдалены",
@@ -66,6 +91,8 @@
"NotificationOptionVideoPlayback": "Пачалося прайграванне відэа",
"NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена",
"ScheduledTaskFailedWithName": "{0} не атрымалася",
"ScheduledTaskStartedWithName": "{0} пачалося",
"ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску",
"Shows": "Шоу",
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
"SubtitleDownloadFailureFromForItem": "Субцітры для {1} не ўдалося спампаваць з {0}",
@@ -76,6 +103,8 @@
"UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}",
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}",
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
"ValueSpecialEpisodeName": "Спецвыпуск - {0}",
"VersionNumber": "Версія {0}",
"TasksMaintenanceCategory": "Абслугоўванне",
"TasksLibraryCategory": "Бібліятэка",
@@ -94,9 +123,11 @@
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
"TaskRefreshChannels": "Абнавіць каналы",
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа выконвацца доўга.",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.",
"TaskRefreshTrickplayImages": "Стварыць выявы Trickplay",
"TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.",
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты",
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.",
"TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.",
"TaskAudioNormalization": "Нармалізацыя гуку",
"TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.",

View File

@@ -1,24 +1,41 @@
{
"Albums": "Албуми",
"AppDeviceValues": "Програма: {0}, Устройство: {1}",
"Application": "Програма",
"Artists": "Артисти",
"AuthenticationSucceededWithUserName": "{0} се удостовери успешно",
"Books": "Книги",
"CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}",
"Channels": "Канали",
"ChapterNameValue": "Глава {0}",
"Collections": "Колекции",
"DeviceOfflineWithName": "{0} се разкачи",
"DeviceOnlineWithName": "{0} е свързан",
"FailedLoginAttemptWithUserName": "Неуспешен опит за влизане от {0}",
"Favorites": "Любими",
"Folders": "Папки",
"Genres": "Жанрове",
"HeaderAlbumArtists": "Изпълнители на албуми",
"HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители",
"HeaderFavoriteEpisodes": "Любими епизоди",
"HeaderFavoriteShows": "Любими сериали",
"HeaderFavoriteSongs": "Любими песни",
"HeaderLiveTV": "Телевизия на живо",
"HeaderNextUp": "Следва",
"HeaderRecordingGroups": "Запис групи",
"HomeVideos": "Домашни Клипове",
"Inherit": "Наследяване",
"ItemAddedWithName": "{0} е добавено към библиотеката",
"ItemRemovedWithName": "{0} е премахнато от библиотеката",
"LabelIpAddressValue": "IP адрес: {0}",
"LabelRunningTimeValue": "Продължителност: {0}",
"Latest": "Последни",
"MessageApplicationUpdated": "Сървърът беше обновен",
"MessageApplicationUpdatedTo": "Сървърът беше обновен до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Секцията {0} от сървърната конфигурация беше актуализирана",
"MessageServerConfigurationUpdated": "Конфигурацията на сървъра беше актуализирана",
"MixedContent": "Смесено съдържание",
"Movies": "Филми",
"Music": "Музика",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Възпроизвеждането на видео започна",
"NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно",
"Photos": "Снимки",
"Playlists": "Списъци",
"Plugin": "Добавка",
"PluginInstalledWithName": "{0} е инсталиранa",
"PluginUninstalledWithName": "{0} е деинсталиранa",
"PluginUpdatedWithName": "{0} е обновенa",
"ProviderValue": "Доставчик: {0}",
"ScheduledTaskFailedWithName": "{0} се провали",
"ScheduledTaskStartedWithName": "{0} започна",
"ServerNameNeedsToBeRestarted": "{0} трябва да се рестартира",
"Shows": "Сериали",
"Songs": "Песни",
"StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.",
"SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени",
"Sync": "Синхронизиране",
"System": "Система",
"TvShows": "Телевизионни сериали",
"User": "Потребител",
"UserCreatedWithName": "Потребителят {0} е създаден",
"UserDeletedWithName": "Потребителят {0} е изтрит",
"UserDownloadingItemWithValues": "{0} изтегля {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} се разкачи от {1}",
"UserOnlineFromDevice": "{0} е на линия от {1}",
"UserPasswordChangedWithName": "Паролата на потребителя {0} е променена",
"UserPolicyUpdatedWithName": "Потребителската политика за {0} се актуализира",
"UserStartedPlayingItemWithValues": "{0} пусна {1}",
"UserStoppedPlayingItemWithValues": "{0} спря {1}",
"ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека",
"ValueSpecialEpisodeName": "Специални - {0}",
"VersionNumber": "Версия {0}",
"TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи субтитри, на база конфигурацията за мета-данни.",
"TaskDownloadMissingSubtitles": "Изтегляне на липсващи субтитри",
@@ -99,6 +128,8 @@
"TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки.",
"TaskDownloadMissingLyrics": "Свали липсващи текстове",
"TaskDownloadMissingLyricsDescription": "Свали текстове за песни",
"TaskCleanCollectionsAndPlaylists": "Изчисти колекциите и плейлистите",
"TaskCleanCollectionsAndPlaylistsDescription": "Премахни несъществуващи файлове в колекциите и плейлистите.",
"TaskAudioNormalization": "Нормализиране на звука",
"TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука.",
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",

View File

@@ -1,19 +1,31 @@
{
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
"Collections": "সংগ্রহশালা",
"ChapterNameValue": "অধ্যায় {0}",
"Channels": "চ্যানেলসমূহ",
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "পুস্তকসমূহ",
"AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন",
"Artists": "শিল্পীগণ",
"Application": "অ্যাপ্লিকেশন",
"Albums": "অ্যালবামসমূহ",
"HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো",
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
"HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
"Genres": "ধরণ",
"Folders": "ফোল্ডারসমূহ",
"Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}",
"VersionNumber": "সংস্করণ {0}",
"ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
"UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}",
"UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}",
"UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে",
"UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে",
"UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে",
"UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে",
@@ -21,14 +33,23 @@
"UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে",
"UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে",
"UserCreatedWithName": "ব্যবহারকারী {0} সৃষ্টি করা হয়েছে",
"User": "ব্যবহারকারী",
"TvShows": "টিভি শোগুলো",
"System": "সিস্টেম",
"Sync": "সমন্বয় করুন",
"SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে",
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
"Songs": "সঙ্গীত সমূহ",
"Shows": "শো সমূহ",
"ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন",
"ScheduledTaskStartedWithName": "{0} শুরু হয়েছে",
"ScheduledTaskFailedWithName": "{0} ব্যর্থ",
"ProviderValue": "প্রদানকারী: {0}",
"PluginUpdatedWithName": "{0} আপডেট করা হয়েছে",
"PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে",
"PluginInstalledWithName": "{0} ইন্সটল হয়েছে",
"Plugin": "প্লাগিন",
"Playlists": "প্লে লিস্ট সমূহ",
"Photos": "ছবিসমূহ",
"NotificationOptionVideoPlaybackStopped": "ভিডিও প্লেব্যাক বন্ধ হয়েছে",
"NotificationOptionVideoPlayback": "ভিডিও প্লেব্যাক শুরু হয়েছে",
@@ -54,13 +75,21 @@
"Music": "গান",
"Movies": "চলচ্চিত্রসমূহ",
"MixedContent": "মিশ্র কন্টেন্ট",
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
"HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো",
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে",
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
"MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
"Latest": "সর্বশেষ",
"LabelRunningTimeValue": "চলার সময়: {0}",
"LabelIpAddressValue": "আইপি এড্রেস: {0}",
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
"Inherit": "উত্তরাধিকারসূত্র থেকে গ্রহণ করুন",
"HomeVideos": "হোম ভিডিও",
"HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি",
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
"HeaderFavoriteShows": "প্রিয় শোগুলো",
"TasksLibraryCategory": "লাইব্রেরি",
"TasksMaintenanceCategory": "রক্ষণাবেক্ষণ",
@@ -98,6 +127,8 @@
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
"TaskDownloadMissingLyricsDescription": "গানের জন্য লিরিকস ডাউনলোড করুন",
"TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
"TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্ট বের করে বা অর্জন করে।",
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",

View File

@@ -1,110 +0,0 @@
{
"Artists": "Umjetnici",
"Books": "Knjige",
"Collections": "Zbirke",
"Default": "Zadano",
"Favorites": "Omiljeni",
"Folders": "Mape",
"Genres": "Žanrovi",
"HeaderContinueWatching": "Nastavi gledati",
"Movies": "Filmovi",
"MusicVideos": "Muzički spotovi",
"Photos": "Slike",
"Shows": "Pokazuje",
"AppDeviceValues": "Aplikacija: {0}, Uređaj: {1}",
"AuthenticationSucceededWithUserName": "{0} uspješno autentificirano",
"ChapterNameValue": "Poglavlje {0}",
"External": "Vanjsko",
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave sa {0}",
"Forced": "Prisilno",
"HeaderFavoriteEpisodes": "Omiljene epizode",
"HeaderFavoriteShows": "Omiljene emisije",
"HeaderLiveTV": "TV uživo",
"HeaderNextUp": "Slijedi",
"HearingImpaired": "Oštećen sluh",
"HomeVideos": "Kućni videozapisi",
"Inherit": "Nasljedi",
"LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Trajanje: {0}",
"Latest": "Posljednje dodano",
"MixedContent": "Miješani sadržaj",
"Music": "Muzika",
"NameInstallFailed": "{0} instalacija je propala",
"NameSeasonNumber": "Sezona {0}",
"NameSeasonUnknown": "Sezona nepoznata",
"NewVersionIsAvailable": "Dostupna je nova verzija Jellyfin Servera za preuzimanje.",
"NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
"NotificationOptionApplicationUpdateInstalled": "Ažuriranje aplikacije instalirano",
"NotificationOptionAudioPlayback": "Pokrenuto je reproduciranje zvuka",
"NotificationOptionAudioPlaybackStopped": "Zaustavljeno je reproduciranje zvuka",
"NotificationOptionCameraImageUploaded": "Učitana slika s kamere",
"NotificationOptionInstallationFailed": "Neuspjeh instalacije",
"NotificationOptionNewLibraryContent": "Dodan novi sadržaj",
"NotificationOptionPluginError": "Neuspjeh dodatka",
"NotificationOptionPluginInstalled": "Dodatak je instaliran",
"NotificationOptionPluginUninstalled": "Dodatak je deinstaliran",
"NotificationOptionPluginUpdateInstalled": "Ažuriranje dodatka je instalirano",
"NotificationOptionServerRestartRequired": "Potreban je ponovni pokret servera",
"NotificationOptionTaskFailed": "Neuspjeh zakazane zadatke",
"NotificationOptionUserLockedOut": "Korisnik je zaključan",
"NotificationOptionVideoPlayback": "Pokrenuto je reproduciranje videa",
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videa je zaustavljena",
"PluginInstalledWithName": "{0} je instaliran",
"PluginUninstalledWithName": "{0} je deinstaliran",
"PluginUpdatedWithName": "{0} je ažurirano",
"ScheduledTaskFailedWithName": "{0} nije uspjelo",
"StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Molimo pokušajte ponovo za kratko vrijeme.",
"SubtitleDownloadFailureFromForItem": "Podtitlovi nisu uspjeli preuzeti sa {0} za {1}",
"TvShows": "TV serije",
"Undefined": "Nedefinirano",
"UserCreatedWithName": "Korisnik {0} je kreiran",
"UserDeletedWithName": "Korisnik {0} je izbrisan",
"UserDownloadingItemWithValues": "{0} preuzima {1}",
"UserLockedOutWithName": "Korisnik {0} je zaključan",
"UserOfflineFromDevice": "{0} se odspojio od {1}",
"UserOnlineFromDevice": "{0} je online od {1}",
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
"UserStartedPlayingItemWithValues": "{0} igra protiv {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je završio igru protiv {1} na {2}",
"VersionNumber": "Verzija {0}",
"TasksMaintenanceCategory": "Održavanje",
"TasksLibraryCategory": "Biblioteka",
"TasksApplicationCategory": "Prijava",
"TasksChannelsCategory": "Internetski kanali",
"TaskCleanActivityLog": "Očisti dnevnik aktivnosti",
"TaskCleanActivityLogDescription": "Brisanje unosa u dnevnik aktivnosti starijih od konfigurisane starosti.",
"TaskCleanCache": "Očistite direktorij keša",
"TaskCleanCacheDescription": "Brisanje keš datoteka koje sistemu više nisu potrebne.",
"TaskRefreshChapterImages": "Izvadi slike iz poglavlja",
"TaskRefreshChapterImagesDescription": "Stvara minijature za videozapise koji imaju poglavlja.",
"TaskAudioNormalization": "Normalizacija zvuka",
"TaskAudioNormalizationDescription": "Skeneriše datoteke radi podataka za normalizaciju zvuka.",
"TaskRefreshLibrary": "Skenerisati medijsku biblioteku",
"TaskRefreshLibraryDescription": "Skenerira vašu medijsku biblioteku na nove datoteke i osvježava metapodatke.",
"TaskCleanLogs": "Očisti direktorij dnevnika",
"TaskCleanLogsDescription": "Brisanje dnevničkih datoteka starijih od {0} dana.",
"TaskRefreshPeople": "Osvježite ljude",
"TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i režisere u vašoj medijskoj biblioteci.",
"TaskRefreshTrickplayImages": "Generirajte Trickplay slike",
"TaskRefreshTrickplayImagesDescription": "Stvara pregled trik-igara za videozapise u omogućenim bibliotekama.",
"TaskUpdatePlugins": "Ažuriraj dodatke",
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja dodataka koji su konfigurisani da se automatski ažuriraju.",
"TaskCleanTranscode": "Očisti Transcode direktorij",
"TaskCleanTranscodeDescription": "Brisanje transkodiranih datoteka starijih od jednog dana.",
"TaskRefreshChannels": "Osvježi kanale",
"TaskRefreshChannelsDescription": "Osvježava informacije o internetskom kanalu.",
"TaskDownloadMissingLyrics": "Preuzmi nedostajuće tekstove",
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
"TaskDownloadMissingSubtitles": "Preuzmite nedostajuće titlove",
"TaskDownloadMissingSubtitlesDescription": "Pretražuje internet u potrazi za nedostajućim titlovima na osnovu konfiguracije metapodataka.",
"TaskOptimizeDatabase": "Optimizirajte bazu podataka",
"TaskOptimizeDatabaseDescription": "Komprimira bazu podataka i čisti slobodan prostor. Pokretanje ovog zadatka nakon skeniranja biblioteke ili izvođenja drugih promjena koje podrazumijevaju izmjene baze podataka može poboljšati performanse.",
"TaskKeyframeExtractor": "Izvađač ključnih sličica",
"TaskKeyframeExtractorDescription": "Izvlači ključne okvire iz video datoteka kako bi kreirao preciznije HLS playliste. Ovaj zadatak može trajati dugo.",
"TaskExtractMediaSegments": "Analiza medijskog segmenta",
"TaskExtractMediaSegmentsDescription": "Izvlači ili dobija medijske segmente iz dodataka koji podržavaju MediaSegment.",
"TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay",
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke trik-igara prema postavkama biblioteke.",
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
"CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana."
}

View File

@@ -1,24 +1,41 @@
{
"Albums": "Àlbums",
"AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}",
"Application": "Aplicació",
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
"Books": "Llibres",
"CameraImageUploadedFrom": "S'ha pujat una nova imatge de càmera des de {0}",
"Channels": "Canals",
"ChapterNameValue": "Capítol {0}",
"Collections": "Col·leccions",
"DeviceOfflineWithName": "{0} s'ha desconnectat",
"DeviceOnlineWithName": "{0} està connectat",
"FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
"Favorites": "Preferits",
"Folders": "Directoris",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes de l'àlbum",
"HeaderContinueWatching": "Continueu mirant",
"HeaderFavoriteAlbums": "Àlbums preferits",
"HeaderFavoriteArtists": "Artistes preferits",
"HeaderFavoriteEpisodes": "Episodis preferits",
"HeaderFavoriteShows": "Sèries preferides",
"HeaderFavoriteSongs": "Cançons preferides",
"HeaderLiveTV": "TV en directe",
"HeaderNextUp": "A continuació",
"HeaderRecordingGroups": "Grups musicals",
"HomeVideos": "Vídeos domèstics",
"Inherit": "Heretat",
"ItemAddedWithName": "{0} s'ha afegit a la mediateca",
"ItemRemovedWithName": "{0} s'ha eliminat de la mediateca",
"LabelIpAddressValue": "Adreça IP: {0}",
"LabelRunningTimeValue": "Temps en marxa: {0}",
"Latest": "Darrers",
"MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
"MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
"MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
"MixedContent": "Contingut barrejat",
"Movies": "Pel·lícules",
"Music": "Música",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reproducció de vídeo aturada",
"Photos": "Fotos",
"PluginInstalledWithName": "S'ha instal·lat {0}",
"PluginUninstalledWithName": "S'ha desinstal·lat {0}",
"Playlists": "Llistes de reproducció",
"Plugin": "Complement",
"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",
"ScheduledTaskStartedWithName": "S'ha iniciat {0}",
"ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}",
"Shows": "Sèries",
"Songs": "Cançons",
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.",
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
"Sync": "Sincronitza",
"System": "Sistema",
"TvShows": "Sèries de TV",
"User": "Usuari",
"UserCreatedWithName": "S'ha creat l'usuari {0}",
"UserDeletedWithName": "S'ha eliminat l'usuari {0}",
"UserDownloadingItemWithValues": "{0} està descarregant {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
"UserOnlineFromDevice": "{0} està connectat des de {1}",
"UserPasswordChangedWithName": "S'ha canviat la contrasenya per a l'usuari {0}",
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}",
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}",
"ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la mediateca",
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versió {0}",
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
"TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin",
@@ -75,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.",
@@ -97,6 +126,8 @@
"HearingImpaired": "Discapacitat auditiva",
"TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
"TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.",
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
"TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció",
"TaskAudioNormalization": "Estabilització de l'àudio",
"TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.",
"TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
@@ -106,6 +137,5 @@
"TaskMoveTrickplayImages": "Migració de la ubicació de la imatge de previsualització",
"TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.",
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari",
"Original": "Original"
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari"
}

View File

@@ -1,30 +1,44 @@
{
"ChapterNameValue": "Didanedi {0}",
"HeaderAlbumArtists": "Didanidanolisgisgi",
"HeaderFavoriteAlbums": "Dvganidi didanidisgisgi",
"HeaderLiveTV": "Anigadi didanidisgosgi",
"HeaderRecordingGroups": "Didanisquodiisgisgi",
"HomeVideos": "Diganadi dinagadisgisgi",
"Inherit": "Anigwe",
"MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}",
"MixedContent": "Ganinidi dininoladisgisgi",
"Movies": "Anidvnisgisgi",
"MusicVideos": "Danodisgisgi didanidisgosgi",
"NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi",
"NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi",
"NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi",
"Albums": "Anigawidaniyv",
"Application": "Didanvyi",
"Artists": "Dinidaniyi",
"AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani",
"Books": "Didanedi",
"CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}",
"Channels": "Diganadasgi",
"Collections": "Diganadisgi",
"Default": "Dinadi",
"DeviceOfflineWithName": "{0} Aniyvolehvi nasgi",
"External": "Amohdi",
"Favorites": "Nvdayelvdisgi",
"Folders": "Didanididisgi",
"Forced": "Ganedi",
"Genres": "Diganadisgi",
"HeaderContinueWatching": "Uwoditsu asdanidisgisgi",
"HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi",
"HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi",
"HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)",
"HeaderFavoriteSongs": "Dvganidi danodisgisgi",
"HeaderNextUp": "Anidvli uwodoli",
"HearingImpaired": "Anitsunidi talunidisgisgi",
"ItemAddedWithName": "{0} Dinigwe anididanidisgi",
"Latest": "Uwodoli",
"MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe",
"MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi",
"Music": "Danodisgisgi",
"NameSeasonUnknown": "Tsunita anidvdisgi",
"NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.",

View File

@@ -1,24 +1,41 @@
{
"Albums": "Alba",
"AppDeviceValues": "Aplikace: {0}, Zařízení: {1}",
"Application": "Aplikace",
"Artists": "Umělci",
"AuthenticationSucceededWithUserName": "{0} úspěšně ověřen",
"Books": "Knihy",
"CameraImageUploadedFrom": "Z {0} byla nahrána nová fotografie z fotoaparátu",
"Channels": "Kanály",
"ChapterNameValue": "Kapitola {0}",
"Collections": "Kolekce",
"DeviceOfflineWithName": "{0} se odpojil",
"DeviceOnlineWithName": "{0} je připojen",
"FailedLoginAttemptWithUserName": "Neúspěšný pokus o přihlášení z {0}",
"Favorites": "Oblíbené",
"Folders": "Složky",
"Genres": "Žánry",
"HeaderAlbumArtists": "Umělci alba",
"HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení interpreti",
"HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály",
"HeaderFavoriteSongs": "Oblíbená hudba",
"HeaderLiveTV": "TV vysílání",
"HeaderNextUp": "Další díly",
"HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domácí videa",
"Inherit": "Zdědit",
"ItemAddedWithName": "{0} byl přidán do knihovny",
"ItemRemovedWithName": "{0} byl odstraněn z knihovny",
"LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Délka média: {0}",
"Latest": "Nejnovější",
"MessageApplicationUpdated": "Jellyfin Server byl aktualizován",
"MessageApplicationUpdatedTo": "Jellyfin server byl aktualizován na verzi {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurace sekce {0} na serveru byla aktualizována",
"MessageServerConfigurationUpdated": "Konfigurace serveru aktualizována",
"MixedContent": "Smíšený obsah",
"Movies": "Filmy",
"Music": "Hudba",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Přehrávání videa zahájeno",
"NotificationOptionVideoPlaybackStopped": "Přehrávání videa ukončeno",
"Photos": "Fotky",
"Playlists": "Seznamy skladeb",
"Plugin": "Zásuvný modul",
"PluginInstalledWithName": "{0} byl nainstalován",
"PluginUninstalledWithName": "{0} byl odinstalován",
"PluginUpdatedWithName": "{0} byl aktualizován",
"ProviderValue": "Poskytl: {0}",
"ScheduledTaskFailedWithName": "{0} selhalo",
"ScheduledTaskStartedWithName": "{0} zahájeno",
"ServerNameNeedsToBeRestarted": "{0} vyžaduje restart",
"Shows": "Seriály",
"Songs": "Skladby",
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
"SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo",
"Sync": "Synchronizace",
"System": "Systém",
"TvShows": "Seriály",
"User": "Uživatel",
"UserCreatedWithName": "Uživatel {0} byl vytvořen",
"UserDeletedWithName": "Uživatel {0} byl smazán",
"UserDownloadingItemWithValues": "{0} stahuje {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} se odpojil ze zařízení {1}",
"UserOnlineFromDevice": "{0} se připojil ze zařízení {1}",
"UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}",
"UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány",
"UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}",
"UserStoppedPlayingItemWithValues": "{0} zastavil přehrávání {1}",
"ValueHasBeenAddedToLibrary": "{0} byl přidán do vaší knihovny médií",
"ValueSpecialEpisodeName": "Speciál - {0}",
"VersionNumber": "Verze {0}",
"TaskDownloadMissingSubtitlesDescription": "Vyhledá na internetu chybějící titulky na základě nastavení metadat.",
"TaskDownloadMissingSubtitles": "Stáhnout chybějící titulky",
@@ -97,6 +126,8 @@
"HearingImpaired": "Sluchově postižení",
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
"TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.",
"TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání",
"TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání.",
"TaskAudioNormalization": "Normalizace zvuku",
"TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.",
"TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni",
@@ -106,6 +137,5 @@
"TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay",
"TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.",
"CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.",
"CleanupUserDataTask": "Pročistit uživatelská data",
"Original": "Originál"
"CleanupUserDataTask": "Pročistit uživatelská data"
}

View File

@@ -1,11 +1,16 @@
{
"DeviceOnlineWithName": "Mae {0} wedi'i gysylltu",
"DeviceOfflineWithName": "Mae {0} wedi datgysylltu",
"Default": "Diofyn",
"Collections": "Casgliadau",
"ChapterNameValue": "Pennod {0}",
"Channels": "Sianeli",
"CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
"Books": "Llyfrau",
"AuthenticationSucceededWithUserName": "{0} wedii ddilysun llwyddiannus",
"Artists": "Crewyr",
"AppDeviceValues": "Ap: {0}, Dyfais: {1}",
"Albums": "Albwmau",
"Genres": "Genres",
"Folders": "Ffolderi",
"Favorites": "Ffefrynnau",
@@ -15,7 +20,9 @@
"TaskRefreshPeople": "Adnewyddu Pobl",
"TasksChannelsCategory": "Sianeli Internet",
"VersionNumber": "Fersiwn {0}",
"ScheduledTaskStartedWithName": "{0} wedi dechrau",
"ScheduledTaskFailedWithName": "{0} wedi methu",
"ProviderValue": "Darparwr: {0}",
"NotificationOptionInstallationFailed": "Fethu Gosod",
"NameSeasonUnknown": "Tymor Anhysbys",
"NameSeasonNumber": "Tymor {0}",
@@ -23,20 +30,31 @@
"MixedContent": "Cynnwys amrywiol",
"HomeVideos": "Genres",
"HeaderNextUp": "Nesaf i Fyny",
"HeaderFavoriteArtists": "Ffefryn Artistiaid",
"HeaderFavoriteAlbums": "Ffefryn Albwmau",
"HeaderContinueWatching": "Parhewch i Wylio",
"TasksApplicationCategory": "Rhaglen",
"TasksLibraryCategory": "Llyfrgell",
"TasksMaintenanceCategory": "Cynnal a Chadw",
"System": "System",
"Plugin": "Ategyn",
"Music": "Cerddoriaeth",
"Latest": "Diweddaraf",
"Inherit": "Etifeddu",
"Forced": "Orfodi",
"Application": "Rhaglen",
"HeaderAlbumArtists": "Artistiaid albwm",
"Sync": "Cysoni",
"Songs": "Caneuon",
"Shows": "Rhaglenni",
"Playlists": "Rhestri Chwarae",
"Photos": "Lluniau",
"ValueSpecialEpisodeName": "Arbennig - {0}",
"Movies": "Ffilmiau",
"Undefined": "Heb ddiffiniad",
"TvShows": "Rhaglenni teledu",
"HeaderLiveTV": "Teledu Byw",
"User": "Defnyddiwr",
"TaskCleanLogsDescription": "Dileu ffeiliau log sy'n fwy na {0} diwrnod oed.",
"TaskCleanLogs": "Glanhau ffolder log",
"TaskRefreshLibraryDescription": "Sganio'ch llyfrgell gyfryngau am ffeiliau newydd ac yn adnewyddu metaddata.",
@@ -47,9 +65,13 @@
"NotificationOptionPluginError": "Methodd ategyn",
"NotificationOptionAudioPlaybackStopped": "Stopiwyd chwarae sain",
"NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain",
"MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru",
"MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru",
"FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu o {0}",
"ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau",
"UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}",
"UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}",
"UserPolicyUpdatedWithName": "Polisi defnyddiwr wedi'i newid ar gyfer {0}",
"UserPasswordChangedWithName": "Cyfrinair wedi'i newid ar gyfer defnyddiwr {0}",
"UserOnlineFromDevice": "Mae {0} ar-lein o {1}",
"UserOfflineFromDevice": "Mae {0} wedi datgysylltu o {1}",
@@ -58,6 +80,7 @@
"UserDeletedWithName": "Defnyddiwr {0} wedi'i ddileu",
"UserCreatedWithName": "Defnyddiwr {0} wedi'i greu",
"StartupEmbyServerIsLoading": "Gweinydd Jellyfin yn llwytho. Triwch eto mewn ychydig.",
"ServerNameNeedsToBeRestarted": "Mae angen ailddechrau {0}",
"PluginUpdatedWithName": "{0} wedi'i ddiweddaru",
"PluginUninstalledWithName": "{0} wedi'i ddadosod",
"PluginInstalledWithName": "{0} wedi'i osod",
@@ -75,7 +98,13 @@
"NotificationOptionApplicationUpdateAvailable": "Diweddariad ap ar gael",
"NewVersionIsAvailable": "Mae fersiwn diweddarach o'r gweinydd Jellyfin ar gael.",
"NameInstallFailed": "Gosodiad {0} wedi methu",
"MessageApplicationUpdatedTo": "Gweinydd Jellyfin wedi'i ddiweddaru i {0}",
"MessageApplicationUpdated": "Gweinydd Jellyfin wedi'i ddiweddaru",
"LabelIpAddressValue": "Cyfeiriad IP: {0}",
"ItemRemovedWithName": "{0} wedi'i dynnu o'r llyfrgell",
"ItemAddedWithName": "{0} wedi'i adio i'r llyfrgell",
"HeaderRecordingGroups": "Grwpiau Recordio",
"HeaderFavoriteSongs": "Ffefryn Ganeuon",
"HeaderFavoriteShows": "Ffefryn Shoeau",
"HeaderFavoriteEpisodes": "Ffefryn Rhaglenni",
"TaskDownloadMissingSubtitlesDescription": "Chwilio'r rhyngrwyd am is-deitlau coll yn seiliedig ar gosodiadau metaddata.",
@@ -101,5 +130,7 @@
"TaskRefreshTrickplayImagesDescription": "Creu rhagolygon Trickplay ar gyfer fideos mewn llyfrgelloedd gweithredol.",
"TaskDownloadMissingLyrics": "Lawrlwytho geiriau coll",
"TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon",
"TaskCleanCollectionsAndPlaylists": "Glanhau casgliadau a rhestrau chwarae",
"TaskCleanCollectionsAndPlaylistsDescription": "Dileu eitemau o gasgliadau a rhestrau chwarae sydd ddim yn bodoli bellach.",
"TaskExtractMediaSegments": "Sganio Darnau Cyfryngau"
}

View File

@@ -1,24 +1,41 @@
{
"Albums": "Albummer",
"AppDeviceValues": "App: {0}, Enhed: {1}",
"Application": "Applikation",
"Artists": "Kunstnere",
"AuthenticationSucceededWithUserName": "{0} er logget ind",
"Books": "Bøger",
"CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}",
"Channels": "Kanaler",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Samlinger",
"DeviceOfflineWithName": "{0} har afbrudt forbindelsen",
"DeviceOnlineWithName": "{0} er forbundet",
"FailedLoginAttemptWithUserName": "Mislykket loginforsøg fra {0}",
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
"HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritkunstnere",
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
"HeaderFavoriteShows": "Yndlingsserier",
"HeaderFavoriteSongs": "Yndlingssange",
"HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper",
"HomeVideos": "Hjemmevideoer",
"Inherit": "Nedarv",
"ItemAddedWithName": "{0} blev tilføjet til biblioteket",
"ItemRemovedWithName": "{0} blev fjernet fra biblioteket",
"LabelIpAddressValue": "IP-adresse: {0}",
"LabelRunningTimeValue": "Spilletid: {0}",
"Latest": "Seneste",
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
"MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
"MixedContent": "Blandet indhold",
"Movies": "Film",
"Music": "Musik",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
"NotificationOptionVideoPlaybackStopped": "Videoafspilning blev stoppet",
"Photos": "Fotos",
"Playlists": "Afspilningslister",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} blev installeret",
"PluginUninstalledWithName": "{0} blev afinstalleret",
"PluginUpdatedWithName": "{0} blev opdateret",
"ProviderValue": "Udbyder: {0}",
"ScheduledTaskFailedWithName": "{0} mislykkedes",
"ScheduledTaskStartedWithName": "{0} påbegyndte",
"ServerNameNeedsToBeRestarted": "{0} skal genstartes",
"Shows": "Serier",
"Songs": "Sange",
"StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
"Sync": "Synkroniser",
"System": "System",
"TvShows": "TV-serier",
"User": "Bruger",
"UserCreatedWithName": "Bruger {0} er blevet oprettet",
"UserDeletedWithName": "Brugeren {0} er nu slettet",
"UserDownloadingItemWithValues": "{0} henter {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} har afbrudt fra {1}",
"UserOnlineFromDevice": "{0} er online fra {1}",
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
"UserStartedPlayingItemWithValues": "{0} afspiller {1} på {2}",
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
@@ -97,6 +126,8 @@
"HearingImpaired": "Hørehæmmet",
"TaskRefreshTrickplayImages": "Generer trickplay-billeder",
"TaskRefreshTrickplayImagesDescription": "Laver trickplay-billeder for videoer i aktiverede biblioteker.",
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende lydnormalisering.",
"TaskAudioNormalization": "Lydnormalisering",
"TaskDownloadMissingLyricsDescription": "Søger på internettet efter manglende sangtekster baseret på metadata-konfigurationen",

View File

@@ -1,24 +1,41 @@
{
"Albums": "Alben",
"AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung",
"Artists": "Interpreten",
"AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
"Books": "Bücher",
"CameraImageUploadedFrom": "Ein neues Kamerabild wurde von {0} hochgeladen",
"Channels": "Kanäle",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Sammlungen",
"DeviceOfflineWithName": "{0} ist offline",
"DeviceOnlineWithName": "{0} ist online",
"FailedLoginAttemptWithUserName": "Anmeldung von {0} fehlgeschlagen",
"Favorites": "Favoriten",
"Folders": "Verzeichnisse",
"Genres": "Genres",
"HeaderAlbumArtists": "Album-Interpreten",
"HeaderContinueWatching": "Weiterschauen",
"HeaderFavoriteEpisodes": "Lieblingsfolgen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblingsinterpreten",
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
"HeaderFavoriteShows": "Lieblingsserien",
"HeaderFavoriteSongs": "Lieblingssongs",
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Als Nächstes",
"HeaderRecordingGroups": "Aufnahme-Gruppen",
"HomeVideos": "Heimvideos",
"Inherit": "Vererben",
"ItemAddedWithName": "{0} wurde der Bibliothek hinzugefügt",
"ItemRemovedWithName": "{0} wurde aus der Bibliothek entfernt",
"LabelIpAddressValue": "IP-Adresse: {0}",
"LabelRunningTimeValue": "Laufzeit: {0}",
"Latest": "Neueste",
"MessageApplicationUpdated": "Jellyfin-Server wurde aktualisiert",
"MessageApplicationUpdatedTo": "Jellyfin-Server wurde auf Version {0} aktualisiert",
"MessageNamedServerConfigurationUpdatedWithValue": "Der Server-Einstellungsbereich {0} wurde aktualisiert",
"MessageServerConfigurationUpdated": "Servereinstellungen wurden aktualisiert",
"MixedContent": "Gemischte Inhalte",
"Movies": "Filme",
"Music": "Musik",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Video wird abgespielt",
"NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt",
"Photos": "Fotos",
"Playlists": "Wiedergabelisten",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} wurde installiert",
"PluginUninstalledWithName": "{0} wurde deinstalliert",
"PluginUpdatedWithName": "{0} wurde aktualisiert",
"ProviderValue": "Anbieter: {0}",
"ScheduledTaskFailedWithName": "{0} ist fehlgeschlagen",
"ScheduledTaskStartedWithName": "{0} wurde gestartet",
"ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden",
"Shows": "Serien",
"Songs": "Lieder",
"StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.",
"SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden",
"Sync": "Synchronisation",
"System": "System",
"TvShows": "Serien",
"User": "Benutzer",
"UserCreatedWithName": "Benutzer {0} wurde erstellt",
"UserDeletedWithName": "Benutzer {0} wurde gelöscht",
"UserDownloadingItemWithValues": "{0} lädt {1} herunter",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} wurde getrennt von {1}",
"UserOnlineFromDevice": "{0} ist online von {1}",
"UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert",
"UserPolicyUpdatedWithName": "Benutzerrichtlinie von {0} wurde aktualisiert",
"UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet",
"UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet",
"ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt",
"ValueSpecialEpisodeName": "Extra {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
"TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen",
@@ -97,6 +126,8 @@
"HearingImpaired": "Hörgeschädigt",
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
"TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
"TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
"TaskCleanCollectionsAndPlaylistsDescription": "Löscht nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
"TaskAudioNormalization": "Audio Normalisierung",
"TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
"TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
@@ -106,7 +137,5 @@
"TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.",
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind.",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}"
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind."
}

View File

@@ -1,24 +1,41 @@
{
"Albums": "Άλμπουμ",
"AppDeviceValues": "Εφαρμογή: {0}, Συσκευή: {1}",
"Application": "Εφαρμογή",
"Artists": "Καλλιτέχνες",
"AuthenticationSucceededWithUserName": "Ο χρήστης {0} επαληθεύτηκε επιτυχώς",
"Books": "Βιβλία",
"CameraImageUploadedFrom": "Μια νέα φωτογραφία φορτώθηκε από {0}",
"Channels": "Κανάλια",
"ChapterNameValue": "Κεφάλαιο {0}",
"Collections": "Συλλογές",
"DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
"DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
"FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}",
"Favorites": "Αγαπημένα",
"Folders": "Φάκελοι",
"Genres": "Είδη",
"HeaderAlbumArtists": "Καλλιτέχνες άλμπουμ",
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
"HeaderFavoriteEpisodes": "Αγαπημένα Επεισόδια",
"HeaderFavoriteShows": "Αγαπημένες Σειρές",
"HeaderFavoriteSongs": "Αγαπημένα Τραγούδια",
"HeaderLiveTV": "Ζωντανή Τηλεόραση",
"HeaderNextUp": "Επόμενο",
"HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
"HomeVideos": "Προσωπικά Βίντεο",
"Inherit": "Κληρονόμηση",
"ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη",
"ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη",
"LabelIpAddressValue": "Διεύθυνση IP: {0}",
"LabelRunningTimeValue": "Διάρκεια: {0}",
"Latest": "Πρόσφατα",
"MessageApplicationUpdated": "Ο διακομιστής Jellyfin έχει ενημερωθεί",
"MessageApplicationUpdatedTo": "Ο διακομιστής Jellyfin αναβαθμίστηκε στην έκδοση {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Η ενότητα {0} ρύθμισης παραμέτρων του διακομιστή έχει ενημερωθεί",
"MessageServerConfigurationUpdated": "Η ρύθμιση παραμέτρων του διακομιστή έχει ενημερωθεί",
"MixedContent": "Ανάμεικτο Περιεχόμενο",
"Movies": "Ταινίες",
"Music": "Μουσική",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Η αναπαραγωγή βίντεο ξεκίνησε",
"NotificationOptionVideoPlaybackStopped": "Η αναπαραγωγή βίντεο σταμάτησε",
"Photos": "Φωτογραφίες",
"Playlists": "Λίστες αναπαραγωγής",
"Plugin": "Πρόσθετο",
"PluginInstalledWithName": "Το {0} εγκαταστάθηκε",
"PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί",
"PluginUpdatedWithName": "Το {0} ενημερώθηκε",
"ProviderValue": "Πάροχος: {0}",
"ScheduledTaskFailedWithName": "{0} αποτυχία",
"ScheduledTaskStartedWithName": "{0} ξεκίνησε",
"ServerNameNeedsToBeRestarted": "{0} χρειάζεται επανεκκίνηση",
"Shows": "Σειρές",
"Songs": "Τραγούδια",
"StartupEmbyServerIsLoading": "Ο διακομιστής Jellyfin φορτώνει. Περιμένετε λίγο και δοκιμάστε ξανά.",
"SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}",
"Sync": "Συγχρονισμός",
"System": "Σύστημα",
"TvShows": "Τηλεοπτικές Σειρές",
"User": "Χρήστης",
"UserCreatedWithName": "Ο χρήστης {0} δημιουργήθηκε",
"UserDeletedWithName": "Ο χρήστης {0} έχει διαγραφεί",
"UserDownloadingItemWithValues": "{0} κατεβάζει {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} αποσυνδέθηκε από {1}",
"UserOnlineFromDevice": "{0} είναι online απο {1}",
"UserPasswordChangedWithName": "Ο κωδικός του χρήστη {0} έχει αλλάξει",
"UserPolicyUpdatedWithName": "Η πολιτική χρήστη έχει ενημερωθεί για {0}",
"UserStartedPlayingItemWithValues": "{0} παίζει {1} σε {2}",
"UserStoppedPlayingItemWithValues": "{0} τελείωσε να παίζει {1} σε {2}",
"ValueHasBeenAddedToLibrary": "{0} προστέθηκαν στη βιβλιοθήκη πολυμέσων σας",
"ValueSpecialEpisodeName": "Σπέσιαλ - {0}",
"VersionNumber": "Έκδοση {0}",
"TaskRefreshPeople": "Ανανέωση Ατόμων",
"TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
@@ -99,6 +128,8 @@
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
"TaskAudioNormalization": "Ομοιομορφία ήχου",
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
"TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
"TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον.",
"TaskMoveTrickplayImages": "Αλλαγή τοποθεσίας εικόνων Trickplay",
"TaskDownloadMissingLyrics": "Λήψη στίχων που λείπουν",
"TaskMoveTrickplayImagesDescription": "Μετακινεί τα υπάρχοντα αρχεία trickplay σύμφωνα με τις ρυθμίσεις της βιβλιοθήκης.",

View File

@@ -1,24 +1,41 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Device: {1}",
"Application": "Application",
"Artists": "Artists",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
"Channels": "Channels",
"ChapterNameValue": "Chapter {0}",
"Collections": "Collections",
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favourites",
"Folders": "Folders",
"Genres": "Genres",
"HeaderAlbumArtists": "Album artists",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favourite Albums",
"HeaderFavoriteArtists": "Favourite Artists",
"HeaderFavoriteEpisodes": "Favourite Episodes",
"HeaderFavoriteShows": "Favourite Shows",
"HeaderFavoriteSongs": "Favourite Songs",
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
"HeaderRecordingGroups": "Recording Groups",
"HomeVideos": "Home Videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
"LabelIpAddressValue": "IP address: {0}",
"LabelRunningTimeValue": "Running time: {0}",
"Latest": "Latest",
"MessageApplicationUpdated": "Jellyfin Server has been updated",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
"MessageServerConfigurationUpdated": "Server configuration has been updated",
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Video playback started",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
"Photos": "Photos",
"Playlists": "Playlists",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed",
"PluginUninstalledWithName": "{0} was uninstalled",
"PluginUpdatedWithName": "{0} was updated",
"ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} failed",
"ScheduledTaskStartedWithName": "{0} started",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"Shows": "Shows",
"Songs": "Songs",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
"Sync": "Sync",
"System": "System",
"TvShows": "TV Shows",
"User": "User",
"UserCreatedWithName": "User {0} has been created",
"UserDeletedWithName": "User {0} has been deleted",
"UserDownloadingItemWithValues": "{0} is downloading {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} has disconnected from {1}",
"UserOnlineFromDevice": "{0} is online from {1}",
"UserPasswordChangedWithName": "Password has been changed for user {0}",
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
"UserStartedPlayingItemWithValues": "{0} has started playing {1}",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.",
"TaskDownloadMissingSubtitles": "Download missing subtitles",
@@ -97,6 +126,8 @@
"HearingImpaired": "Hearing Impaired",
"TaskRefreshTrickplayImages": "Generate Trickplay Images",
"TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
"TaskAudioNormalization": "Audio Normalisation",
"TaskAudioNormalizationDescription": "Scans files for audio normalisation data.",
"TaskDownloadMissingLyrics": "Download missing lyrics",

View File

@@ -1,29 +1,45 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Device: {1}",
"Application": "Application",
"Artists": "Artists",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
"Channels": "Channels",
"ChapterNameValue": "Chapter {0}",
"Collections": "Collections",
"Default": "Default",
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
"External": "External",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites",
"Folders": "Folders",
"Forced": "Forced",
"Genres": "Genres",
"HeaderAlbumArtists": "Album artists",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
"HeaderFavoriteEpisodes": "Favorite Episodes",
"HeaderFavoriteShows": "Favorite Shows",
"HeaderFavoriteSongs": "Favorite Songs",
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
"HeaderRecordingGroups": "Recording Groups",
"HearingImpaired": "Hearing Impaired",
"HomeVideos": "Home Videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
"LabelIpAddressValue": "IP address: {0}",
"LabelRunningTimeValue": "Running time: {0}",
"Latest": "Latest",
"LyricDownloadFailureFromForItem": "Lyrics failed to download from {0} for {1}",
"MessageApplicationUpdated": "Jellyfin Server has been updated",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
"MessageServerConfigurationUpdated": "Server configuration has been updated",
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
@@ -48,17 +64,25 @@
"NotificationOptionUserLockedOut": "User locked out",
"NotificationOptionVideoPlayback": "Video playback started",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
"Original": "Original",
"Photos": "Photos",
"Playlists": "Playlists",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed",
"PluginUninstalledWithName": "{0} was uninstalled",
"PluginUpdatedWithName": "{0} was updated",
"ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} failed",
"ScheduledTaskStartedWithName": "{0} started",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"Shows": "Shows",
"Songs": "Songs",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
"Sync": "Sync",
"System": "System",
"TvShows": "TV Shows",
"Undefined": "Undefined",
"User": "User",
"UserCreatedWithName": "User {0} has been created",
"UserDeletedWithName": "User {0} has been deleted",
"UserDownloadingItemWithValues": "{0} is downloading {1}",
@@ -66,8 +90,11 @@
"UserOfflineFromDevice": "{0} has disconnected from {1}",
"UserOnlineFromDevice": "{0} is online from {1}",
"UserPasswordChangedWithName": "Password has been changed for user {0}",
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TasksMaintenanceCategory": "Maintenance",
"TasksLibraryCategory": "Library",
@@ -103,6 +130,8 @@
"TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.",
"TaskKeyframeExtractor": "Keyframe Extractor",
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
"TaskExtractMediaSegments": "Media Segment Scan",
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",

View File

@@ -7,22 +7,35 @@
"NameInstallFailed": "{0} instalado fiaskis",
"Music": "Muziko",
"Movies": "Filmoj",
"ItemRemovedWithName": "{0} forigis el la plurmediteko",
"ItemAddedWithName": "{0} aldonis al la plurmediteko",
"HeaderLiveTV": "TV-etero",
"HeaderContinueWatching": "Daŭrigi Spektadon",
"HeaderAlbumArtists": "Artistoj de albumo",
"Folders": "Dosierujoj",
"DeviceOnlineWithName": "{0} estas konektita",
"Default": "Defaŭlte",
"Collections": "Kolektoj",
"ChapterNameValue": "Ĉapitro {0}",
"Channels": "Kanaloj",
"Books": "Libroj",
"Artists": "Artistoj",
"Application": "Aplikaĵo",
"AppDeviceValues": "Aplikaĵo: {0}, Aparato: {1}",
"Albums": "Albumoj",
"TasksLibraryCategory": "Plurmediteko",
"VersionNumber": "Versio {0}",
"UserDownloadingItemWithValues": "{0} elŝutas {1}",
"UserCreatedWithName": "Uzanto {0} kreiĝis",
"User": "Uzanto",
"System": "Sistemo",
"Songs": "Kantoj",
"ScheduledTaskStartedWithName": "{0} lanĉis",
"ScheduledTaskFailedWithName": "{0} malsukcesis",
"PluginUninstalledWithName": "{0} malinstaliĝis",
"PluginInstalledWithName": "{0} instaliĝis",
"Plugin": "Kromprogramo",
"Playlists": "Ludlistoj",
"Photos": "Fotoj",
"NotificationOptionPluginUninstalled": "Kromprogramo malinstaliĝis",
"NotificationOptionNewLibraryContent": "Nova enhavo aldoniĝis",
@@ -30,28 +43,36 @@
"MusicVideos": "Muzikvideoj",
"LabelIpAddressValue": "IP-adreso: {0}",
"Genres": "Ĝenroj",
"DeviceOfflineWithName": "{0} malkonektis",
"HeaderFavoriteArtists": "Favorataj Artistoj",
"Shows": "Serioj",
"HeaderFavoriteShows": "Favorataj Serioj",
"TvShows": "TV-serioj",
"Favorites": "Favorataj",
"TaskCleanLogs": "Purigi Ĵurnalan Katalogon",
"TaskRefreshLibrary": "Skani Plurmeditekon",
"ValueSpecialEpisodeName": "Speciala - {0}",
"TaskOptimizeDatabase": "Optimumigi datenbazon",
"TaskRefreshChannels": "Refreŝigi Kanalojn",
"TaskUpdatePlugins": "Ĝisdatigi Kromprogramojn",
"TaskRefreshPeople": "Refreŝigi Homojn",
"TasksChannelsCategory": "Interretaj Kanaloj",
"ProviderValue": "Provizanto: {0}",
"NotificationOptionPluginError": "Kromprogramo malsukcesis",
"MixedContent": "Miksita enhavo",
"TasksApplicationCategory": "Aplikaĵo",
"TasksMaintenanceCategory": "Prizorgado",
"Undefined": "Nedifinita",
"Sync": "Sinkronigo",
"Latest": "Plej novaj",
"Inherit": "Hereda",
"HomeVideos": "Hejmaj Videoj",
"HeaderNextUp": "Sekva Plue",
"HeaderFavoriteSongs": "Favorataj Kantoj",
"HeaderFavoriteEpisodes": "Favorataj Epizodoj",
"HeaderFavoriteAlbums": "Favorataj Albumoj",
"Forced": "Forcita",
"ServerNameNeedsToBeRestarted": "{0} devas esti relanĉita",
"NotificationOptionVideoPlayback": "La videoludado lanĉis",
"NotificationOptionServerRestartRequired": "Servila relanĉigo bezonata",
"TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la plurmediteka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.",
@@ -64,16 +85,22 @@
"TaskCleanCacheDescription": "Forigas stapla dosierojn ne plu necesajn de la sistemo.",
"TaskCleanActivityLogDescription": "Forigas aktivecan ĵurnalaĵojn pli malnovajn ol la agordita aĝo.",
"TaskCleanTranscodeDescription": "Forigas transkodajn dosierojn aĝajn pli ol unu tagon.",
"ValueHasBeenAddedToLibrary": "{0} estis aldonita al via plurmediteko",
"SubtitleDownloadFailureFromForItem": "Subtekstoj malsukcesis elŝuti de {0} por {1}",
"StartupEmbyServerIsLoading": "Jellyfin Server ŝarĝas. Provi denove baldaŭ.",
"TaskRefreshChapterImagesDescription": "Kreas bildetojn por videoj kiuj havas ĉapitrojn.",
"UserStoppedPlayingItemWithValues": "{0} finis ludi {1} ĉe {2}",
"UserPolicyUpdatedWithName": "Uzanta politiko estis ĝisdatigita por {0}",
"UserPasswordChangedWithName": "Pasvorto estis ŝanĝita por uzanto {0}",
"UserStartedPlayingItemWithValues": "{0} ludas {1} ĉe {2}",
"UserLockedOutWithName": "Uzanto {0} estas elŝlosita",
"UserOnlineFromDevice": "{0} estas enreta de {1}",
"UserOfflineFromDevice": "{0} malkonektis de {1}",
"UserDeletedWithName": "Uzanto {0} estis forigita",
"MessageServerConfigurationUpdated": "Servila agordaro estis ĝisdatigita",
"MessageNamedServerConfigurationUpdatedWithValue": "Servila agorda sekcio {0} estis ĝisdatigita",
"MessageApplicationUpdatedTo": "Jellyfin Server estis ĝisdatigita al {0}",
"MessageApplicationUpdated": "Jellyfin Server estis ĝisdatigita",
"TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
"TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn",
"TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
@@ -89,7 +116,9 @@
"NotificationOptionApplicationUpdateInstalled": "Aplikaĵa ĝisdatigo instalita",
"NotificationOptionApplicationUpdateAvailable": "Ĝisdatigo de aplikaĵo havebla",
"LabelRunningTimeValue": "Ludada tempo: {0}",
"HeaderRecordingGroups": "Rikordadaj Grupoj",
"FailedLoginAttemptWithUserName": "Malsukcesa ensaluta provo de {0}",
"CameraImageUploadedFrom": "Nova kamera bildo estis alŝutita de {0}",
"AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis",
"TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.",
"TaskKeyframeExtractor": "Eltiri Ĉefkadrojn",

View File

@@ -1,24 +1,41 @@
{
"Albums": "Álbumes",
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros",
"CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
"Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
"DeviceOfflineWithName": "{0} se ha desconectado",
"DeviceOnlineWithName": "{0} está conectado",
"FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión de {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
"HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Capítulos favoritos",
"HeaderFavoriteShows": "Series favoritas",
"HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "Siguiente",
"HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos caseros",
"Inherit": "Heredar",
"ItemAddedWithName": "{0} se ha añadido a la biblioteca",
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
"Latest": "Últimos",
"MessageApplicationUpdated": "El servidor Jellyfin fue actualizado",
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la sección {0} de la configuración del servidor",
"MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
"MixedContent": "Contenido mezclado",
"Movies": "Películas",
"Music": "Música",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Se inició la reproducción de video",
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
"Photos": "Fotos",
"Playlists": "Listas de reproducción",
"Plugin": "Complemento",
"PluginInstalledWithName": "{0} fue instalado",
"PluginUninstalledWithName": "{0} fue desinstalado",
"PluginUpdatedWithName": "{0} fue actualizado",
"ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciado",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Series",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
"TvShows": "Series de TV",
"User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido borrado",
"UserDownloadingItemWithValues": "{0} está descargando {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} se ha desconectado de {1}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
"UserPolicyUpdatedWithName": "Las política de usuario ha sido actualizada para {0}",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
"ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia",
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}",
"TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten basándose en la configuración de los metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
@@ -99,6 +128,8 @@
"TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.",
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.",
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
"TaskDownloadMissingLyrics": "Descargar letra faltante",
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
"TaskExtractMediaSegments": "Escanear Segmentos de Media",

View File

@@ -1,24 +1,41 @@
{
"Albums": "Álbumes",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"Application": "Aplicación",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
"Books": "Libros",
"CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}",
"Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
"DeviceOfflineWithName": "{0} se ha desconectado",
"DeviceOnlineWithName": "{0} está conectado",
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del Álbum",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "A continuación",
"HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos Caseros",
"Inherit": "Heredar",
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo corriendo: {0}",
"Latest": "Recientes",
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la sección {0} de la configuración del servidor",
"MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
"MixedContent": "Contenido mezclado",
"Movies": "Películas",
"Music": "Música",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
"Photos": "Fotos",
"Playlists": "Listas de reproducción",
"Plugin": "Complemento",
"PluginInstalledWithName": "{0} fue instalado",
"PluginUninstalledWithName": "{0} fue desinstalado",
"PluginUpdatedWithName": "{0} fue actualizado",
"ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciado",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Programas",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
"TvShows": "Programas de TV",
"User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido eliminado",
"UserDownloadingItemWithValues": "{0} está descargando {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
"UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
"ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios",
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}",
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
@@ -99,6 +128,8 @@
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
"TaskDownloadMissingLyrics": "descargar letras que faltan",
"TaskDownloadMissingLyricsDescription": "Descargar letras de canciones",
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",

View File

@@ -1,24 +1,41 @@
{
"Albums": "Álbumes",
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros",
"CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
"Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
"DeviceOfflineWithName": "{0} se ha desconectado",
"DeviceOnlineWithName": "{0} está conectado",
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
"HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Series favoritas",
"HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "Televisión en directo",
"HeaderNextUp": "Siguiente",
"HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Vídeos caseros",
"Inherit": "Heredar",
"ItemAddedWithName": "{0} se ha añadido a la biblioteca",
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Duración: {0}",
"Latest": "Últimas",
"MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada",
"MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
"MixedContent": "Contenido mixto",
"Movies": "Películas",
"Music": "Música",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Se inició la reproducción de vídeo",
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo detenida",
"Photos": "Fotos",
"Playlists": "Listas de reproducción",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} se ha instalado",
"PluginUninstalledWithName": "{0} se ha desinstalado",
"PluginUpdatedWithName": "{0} se actualizó",
"ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciada",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Series",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureFromForItem": "Fallo en la descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
"TvShows": "Series",
"User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido borrado",
"UserDownloadingItemWithValues": "{0} está descargando {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
"UserPolicyUpdatedWithName": "Actualizada política de usuario para {0}",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
"ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia",
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}",
"TasksMaintenanceCategory": "Mantenimiento",
"TasksLibraryCategory": "Biblioteca",
@@ -97,6 +126,8 @@
"HearingImpaired": "Discapacidad Auditiva",
"TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
"TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas.",
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización.",
"TaskDownloadMissingLyricsDescription": "Descargar letras para las canciones",
@@ -106,6 +137,5 @@
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
"CleanupUserDataTask": "Tarea de limpieza de datos del usuario",
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días.",
"Original": "Original"
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días."
}

View File

@@ -1,19 +1,29 @@
{
"LabelRunningTimeValue": "Tiempo en ejecución: {0}",
"ValueSpecialEpisodeName": "Especial - {0}",
"Sync": "Sincronizar",
"Songs": "Canciones",
"Shows": "Programas",
"Playlists": "Listas de reproducción",
"Photos": "Fotos",
"Movies": "Películas",
"HeaderNextUp": "A continuación",
"HeaderLiveTV": "TV en vivo",
"HeaderFavoriteSongs": "Canciones favoritas",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos",
"HeaderContinueWatching": "Continuar viendo",
"HeaderAlbumArtists": "Artistas de álbum",
"Genres": "Géneros",
"Folders": "Carpetas",
"Favorites": "Favoritos",
"Collections": "Colecciones",
"Channels": "Canales",
"Books": "Libros",
"Artists": "Artistas",
"Albums": "Álbumes",
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
"TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
@@ -37,8 +47,10 @@
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Mantenimiento",
"VersionNumber": "Versión {0}",
"ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
@@ -46,13 +58,19 @@
"UserDownloadingItemWithValues": "{0} está descargando {1}",
"UserDeletedWithName": "El usuario {0} ha sido eliminado",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"User": "Usuario",
"TvShows": "Programas de TV",
"System": "Sistema",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
"ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
"ScheduledTaskStartedWithName": "{0} iniciado",
"ScheduledTaskFailedWithName": "{0} falló",
"ProviderValue": "Proveedor: {0}",
"PluginUpdatedWithName": "{0} fue actualizado",
"PluginUninstalledWithName": "{0} fue desinstalado",
"PluginInstalledWithName": "{0} fue instalado",
"Plugin": "Complemento",
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
"NotificationOptionUserLockedOut": "Usuario bloqueado",
@@ -76,13 +94,24 @@
"MusicVideos": "Videos musicales",
"Music": "Música",
"MixedContent": "Contenido mezclado",
"MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
"MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la sección {0} de la configuración del servidor",
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
"Latest": "Recientes",
"LabelIpAddressValue": "Dirección IP: {0}",
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
"Inherit": "Heredar",
"HomeVideos": "Videos caseros",
"HeaderRecordingGroups": "Grupos de grabación",
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
"DeviceOnlineWithName": "{0} está conectado",
"DeviceOfflineWithName": "{0} se ha desconectado",
"ChapterNameValue": "Capítulo {0}",
"CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
"Application": "Aplicación",
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
"TaskCleanActivityLog": "Limpiar registro de actividades",
@@ -98,7 +127,9 @@
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
"TaskAudioNormalization": "Normalización de audio",
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
"TaskDownloadMissingLyrics": "Descargar letra faltante",
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de complementos habilitados para MediaSegment.",

View File

@@ -1,24 +1,35 @@
{
"Channels": "Canales",
"Books": "Libros",
"Albums": "Álbumes",
"Collections": "Colecciones",
"Artists": "Artistas",
"DeviceOnlineWithName": "{0} está conectado",
"DeviceOfflineWithName": "{0} se ha desconectado",
"ChapterNameValue": "Capítulo {0}",
"CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
"Application": "Aplicación",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"HeaderContinueWatching": "Continuar Viendo",
"HeaderAlbumArtists": "Artistas del álbum",
"Genres": "Géneros",
"Folders": "Carpetas",
"Favorites": "Favoritos",
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
"HeaderFavoriteSongs": "Canciones Favoritas",
"HeaderFavoriteEpisodes": "Episodios Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
"External": "Externo",
"Default": "Predeterminado",
"Movies": "Películas",
"MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de la configuración ha sido actualizada",
"MixedContent": "Contenido mixto",
"Music": "Música",
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
"Sync": "Sincronizar",
"Shows": "Series",
"UserDownloadingItemWithValues": "{0} está descargando {1}",
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
@@ -30,6 +41,8 @@
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.",
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
"TaskCleanCollectionsAndPlaylistsDescription": "Remover elementos de colecciones y listas de reproducción que no existen.",
"TvShows": "Series de TV",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"TaskRefreshChannels": "Actualizar canales",
@@ -37,12 +50,17 @@
"HeaderFavoriteShows": "Programas favoritos",
"TaskCleanActivityLog": "Limpiar registro de actividades",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
"System": "Sistema",
"User": "Usuario",
"Forced": "Forzado",
"PluginInstalledWithName": "{0} ha sido instalado",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"TaskUpdatePlugins": "Actualizar Plugins",
"Latest": "Recientes",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
"Songs": "Canciones",
"NotificationOptionPluginError": "Falla de plugin",
"ScheduledTaskStartedWithName": "{0} iniciado",
"TasksApplicationCategory": "Aplicación",
"UserDeletedWithName": "El usuario {0} ha sido eliminado",
"TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
@@ -55,26 +73,34 @@
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
"TasksLibraryCategory": "Biblioteca",
"NotificationOptionPluginInstalled": "Plugin instalado",
"UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
"VersionNumber": "Versión {0}",
"HeaderNextUp": "A continuación",
"ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"NameSeasonNumber": "Temporada {0}",
"NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
"Plugin": "Plugin",
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
"NotificationOptionTaskFailed": "Falló la tarea programada",
"LabelRunningTimeValue": "Tiempo en ejecución: {0}",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
"TaskRefreshLibrary": "Escanear biblioteca de medios",
"ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
"TasksMaintenanceCategory": "Mantenimiento",
"ProviderValue": "Proveedor: {0}",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"PluginUninstalledWithName": "{0} ha sido desinstalado",
"ValueSpecialEpisodeName": "Especial - {0}",
"ScheduledTaskFailedWithName": "{0} falló",
"TaskCleanLogs": "Limpiar directorio de registros",
"NameInstallFailed": "Falló la instalación de {0}",
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
"TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios para encontrar archivos nuevos y actualizar los metadatos.",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo en un momento.",
"Playlists": "Listas de reproducción",
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
"MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
"TaskRefreshPeople": "Actualizar personas",
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
"HeaderLiveTV": "TV en vivo",
@@ -84,10 +110,15 @@
"TaskCleanCache": "Limpiar directorio caché",
"TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
"Inherit": "Heredar",
"HeaderRecordingGroups": "Grupos de grabación",
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
"TaskOptimizeDatabase": "Optimizar base de datos",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"HearingImpaired": "Discapacidad auditiva",
"HomeVideos": "Videos caseros",
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
"MusicVideos": "Videos musicales",
"NewVersionIsAvailable": "Una nueva versión de Jellyfin está disponible para descargar.",
"PluginUpdatedWithName": "{0} ha sido actualizado",

View File

@@ -1,6 +1,7 @@
{
"TaskCleanActivityLogDescription": "Kustutab määratud ajast vanemad tegevuslogi kirjed.",
"UserDownloadingItemWithValues": "{0} laadib alla {1}",
"HeaderRecordingGroups": "Salvestusrühmad",
"TaskOptimizeDatabaseDescription": "Tihendab ja puhastab andmebaasi. Selle toimingu tegemine pärast meediakogu andmebaasiga seotud muudatuste skannimist võib jõudlust parandada.",
"TaskOptimizeDatabase": "Optimeeri andmebaasi",
"TaskDownloadMissingSubtitlesDescription": "Otsib veebist puuduvaid subtiitreid vastavalt määratud metaandmete seadetele.",
@@ -28,19 +29,30 @@
"TasksLibraryCategory": "Meediakogu",
"TasksMaintenanceCategory": "Hooldus",
"VersionNumber": "Versioon {0}",
"ValueSpecialEpisodeName": "Eriepisood - {0}",
"ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse",
"UserStartedPlayingItemWithValues": "{0} taasesitab {1} seadmes {2}",
"UserPasswordChangedWithName": "Kasutaja {0} parool muudeti",
"UserLockedOutWithName": "Kasutaja {0} lukustati",
"UserDeletedWithName": "Kasutaja {0} kustutati",
"UserCreatedWithName": "Kasutaja {0} on loodud",
"ScheduledTaskStartedWithName": "{0} käivitati",
"ProviderValue": "Allikas: {0}",
"StartupEmbyServerIsLoading": "Jellyfin server laadib. Proovi varsti uuesti.",
"User": "Kasutaja",
"Undefined": "Määratlemata",
"TvShows": "Sarjad",
"System": "Süsteem",
"Sync": "Sünkrooni",
"Songs": "Lood",
"Shows": "Sarjad",
"ServerNameNeedsToBeRestarted": "{0} tuleb taaskäivitada",
"ScheduledTaskFailedWithName": "{0} nurjus",
"PluginUpdatedWithName": "{0} uuendati",
"PluginUninstalledWithName": "{0} eemaldati",
"PluginInstalledWithName": "{0} paigaldati",
"Plugin": "Plugin",
"Playlists": "Esitusloendid",
"Photos": "Fotod",
"NotificationOptionVideoPlaybackStopped": "Video taasesitus lõppes",
"NotificationOptionVideoPlayback": "Video taasesitus algas",
@@ -66,29 +78,46 @@
"Music": "Muusika",
"Movies": "Filmid",
"MixedContent": "Segatud sisu",
"MessageServerConfigurationUpdated": "Serveri seadistust uuendati",
"MessageNamedServerConfigurationUpdatedWithValue": "Serveri seadistusosa {0} uuendati",
"MessageApplicationUpdatedTo": "Jellyfin server uuendati versioonile {0}",
"MessageApplicationUpdated": "Jellyfin server uuendati",
"Latest": "Uusimad",
"LabelRunningTimeValue": "Kestus: {0}",
"LabelIpAddressValue": "IP aadress: {0}",
"ItemRemovedWithName": "{0} eemaldati meediakogust",
"ItemAddedWithName": "{0} lisati meediakogusse",
"Inherit": "Päri",
"HomeVideos": "Koduvideod",
"HeaderNextUp": "Järgmisena",
"HeaderLiveTV": "Otse TV",
"HeaderFavoriteSongs": "Lemmiklood",
"HeaderFavoriteShows": "Lemmiksarjad",
"HeaderFavoriteEpisodes": "Lemmikepisoodid",
"HeaderFavoriteArtists": "Lemmikesitajad",
"HeaderFavoriteAlbums": "Lemmikalbumid",
"HeaderContinueWatching": "Jätka vaatamist",
"HeaderAlbumArtists": "Albumi esitajad",
"Genres": "Žanrid",
"Forced": "Sunnitud",
"Folders": "Kaustad",
"Favorites": "Lemmikud",
"FailedLoginAttemptWithUserName": "Sisselogimine nurjus aadressilt {0}",
"DeviceOnlineWithName": "{0} on ühendatud",
"DeviceOfflineWithName": "{0} katkestas ühenduse",
"Default": "Vaikimisi",
"ChapterNameValue": "Peatükk {0}",
"Channels": "Kanalid",
"CameraImageUploadedFrom": "Uus kaamera pilt laaditi üles allikalt {0}",
"Books": "Raamatud",
"AuthenticationSucceededWithUserName": "{0} autentimine õnnestus",
"Artists": "Esitajad",
"Application": "Rakendus",
"AppDeviceValues": "Rakendus: {0}, seade: {1}",
"Albums": "Albumid",
"UserOfflineFromDevice": "{0} katkestas ühenduse seadmega {1}",
"SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus",
"UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
"UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
"UserOnlineFromDevice": "{0} on ühendatud seadmest {1}",
"External": "Väline",
@@ -99,11 +128,13 @@
"TaskRefreshTrickplayImagesDescription": "Loob trickplay eelvaated videotele lubatud meediakogudes.",
"TaskAudioNormalization": "Normaliseeri helitugevus",
"TaskAudioNormalizationDescription": "Otsib failidest helitugevuse normaliseerimise teavet.",
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest üksused, mida enam ei eksisteeri.",
"TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid",
"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

@@ -1,16 +1,23 @@
{
"ValueSpecialEpisodeName": "Berezia - {0}",
"Sync": "Sinkronizatu",
"Songs": "Abestiak",
"Shows": "Serieak",
"Playlists": "Erreprodukzio-zerrendak",
"Photos": "Argazkiak",
"MusicVideos": "Bideo musikalak",
"Movies": "Filmak",
"HeaderContinueWatching": "Ikusten jarraitu",
"HeaderAlbumArtists": "Albumeko artistak",
"Genres": "Generoak",
"Folders": "Karpetak",
"Favorites": "Gogokoak",
"Default": "Lehenetsia",
"Collections": "Bildumak",
"Channels": "Kanalak",
"Books": "Liburuak",
"Artists": "Artistak",
"Albums": "Albumak",
"TaskOptimizeDatabase": "Datu basea optimizatu",
"TaskDownloadMissingSubtitlesDescription": "Falta diren azpitituluak bilatzen ditu interneten metadatuen konfigurazioaren arabera.",
"TaskDownloadMissingSubtitles": "Falta diren azpitituluak deskargatu",
@@ -37,8 +44,10 @@
"TasksLibraryCategory": "Liburutegia",
"TasksMaintenanceCategory": "Mantenua",
"VersionNumber": "Bertsioa {0}",
"ValueHasBeenAddedToLibrary": "{0} zure multimedia liburutegian gehitu da",
"UserStoppedPlayingItemWithValues": "{0} {1} ikusten bukatu du {2}-(e)n",
"UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(e)n",
"UserPolicyUpdatedWithName": "{0} erabiltzailearen politikak aldatu dira",
"UserPasswordChangedWithName": "{0} erabiltzailearen pasahitza aldatu da",
"UserOnlineFromDevice": "{0} online dago {1}-(e)tik",
"UserOfflineFromDevice": "{0} {1}-(e)tik deskonektatu da",
@@ -46,14 +55,19 @@
"UserDownloadingItemWithValues": "{0} {1} deskargatzen ari da",
"UserDeletedWithName": "{0} Erabiltzailea ezabatu da",
"UserCreatedWithName": "{0} Erabiltzailea sortu da",
"User": "Erabiltzailea",
"Undefined": "Ezezaguna",
"TvShows": "TB serieak",
"System": "Sistema",
"SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0}-tik deskargatzeak huts egin du",
"StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduago.",
"ServerNameNeedsToBeRestarted": "{0} berrabiarazi behar da",
"ScheduledTaskStartedWithName": "{0} hasi da",
"ScheduledTaskFailedWithName": "{0} huts egin du",
"PluginUpdatedWithName": "{0} eguneratu da",
"PluginUninstalledWithName": "{0} desinstalatu da",
"PluginInstalledWithName": "{0} instalatu da",
"Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Bideoa geldituta",
"NotificationOptionVideoPlayback": "Bideoa martxan",
"NotificationOptionUserLockedOut": "Erabiltzailea blokeatua",
@@ -76,22 +90,37 @@
"NameInstallFailed": "{0} instalazioak huts egin du",
"Music": "Musika",
"MixedContent": "Eduki mistoa",
"MessageServerConfigurationUpdated": "Zerbitzariaren konfigurazioa eguneratu da",
"MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren {0} konfigurazio atala eguneratu da",
"MessageApplicationUpdatedTo": "Jellyfin zerbitzaria {0}-ra eguneratu da",
"MessageApplicationUpdated": "Jellyfin zerbitzaria eguneratu da",
"Latest": "Azkena",
"LabelRunningTimeValue": "Iraupena: {0}",
"LabelIpAddressValue": "IP helbidea: {0}",
"ItemRemovedWithName": "{0} liburutegitik kendu da",
"ItemAddedWithName": "{0} liburutegira gehitu da",
"HomeVideos": "Etxeko bideoak",
"HeaderNextUp": "Hurrengoa",
"HeaderLiveTV": "Zuzeneko TB",
"HeaderFavoriteSongs": "Gogoko abestiak",
"HeaderFavoriteShows": "Gogoko serieak",
"HeaderFavoriteEpisodes": "Gogoko atalak",
"HeaderFavoriteArtists": "Gogoko artistak",
"HeaderFavoriteAlbums": "Gogoko albumak",
"Forced": "Behartuta",
"FailedLoginAttemptWithUserName": "{0}-tik saioa hasteak huts egin du",
"External": "Kanpokoa",
"DeviceOnlineWithName": "{0} konektatu da",
"DeviceOfflineWithName": "{0} deskonektatu da",
"ChapterNameValue": "{0} Kapitulua",
"CameraImageUploadedFrom": "{0}-tik kamera irudi berri bat igo da",
"AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da",
"Application": "Aplikazioa",
"AppDeviceValues": "App: {0}, Gailua: {1}",
"HearingImpaired": "Entzumen urritasuna",
"ProviderValue": "Hornitzailea: {0}",
"TaskKeyframeExtractorDescription": "Bideo fitxategietako fotograma gakoak ateratzen ditu HLS erreprodukzio-zerrenda zehatzagoak sortzeko. Zeregin honek denbora asko iraun dezake.",
"HeaderRecordingGroups": "Grabaketa taldeak",
"Inherit": "Oinordetu",
"TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.",
"TaskKeyframeExtractor": "Fotograma gakoen erauzgailua",
@@ -101,6 +130,8 @@
"TaskDownloadMissingLyrics": "Deskargatu falta diren letrak",
"TaskDownloadMissingLyricsDescription": "Deskargatu abestientzako letrak",
"TaskExtractMediaSegments": "Multimedia segmentuen eskaneoa",
"TaskCleanCollectionsAndPlaylistsDescription": "Jada existitzen ez diren bildumak eta erreprodukzio-zerrendak kentzen ditu.",
"TaskCleanCollectionsAndPlaylists": "Garbitu bildumak eta erreprodukzio-zerrendak",
"TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
"TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",

View File

@@ -1,24 +1,41 @@
{
"Albums": "آلبوم‌ها",
"AppDeviceValues": "برنامه: {0} ، دستگاه: {1}",
"Application": "برنامه",
"Artists": "هنرمندان",
"AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد",
"Books": "کتاب‌ها",
"CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}",
"Channels": "کانالها",
"ChapterNameValue": "قسمت {0}",
"Collections": "مجموعه‌ها",
"DeviceOfflineWithName": "ارتباط {0} قطع شد",
"DeviceOnlineWithName": "{0} متصل شد",
"FailedLoginAttemptWithUserName": "تلاش برای ورود از {0} ناموفق بود",
"Favorites": "مورد علاقه‌ها",
"Folders": "پوشه‌ها",
"Genres": "ژانرها",
"HeaderAlbumArtists": "هنرمندان آلبوم",
"HeaderContinueWatching": "ادامه تماشا",
"HeaderFavoriteAlbums": "آلبوم‌های مورد علاقه",
"HeaderFavoriteArtists": "هنرمندان مورد علاقه",
"HeaderFavoriteEpisodes": "قسمت‌های مورد علاقه",
"HeaderFavoriteShows": "سریال‌های مورد علاقه",
"HeaderFavoriteSongs": "آهنگ‌های مورد علاقه",
"HeaderLiveTV": "پخش زنده",
"HeaderNextUp": "قسمت بعدی",
"HeaderRecordingGroups": "گروه‌های ضبط",
"HomeVideos": "ویدیوهای خانگی",
"Inherit": "به ارث برده",
"ItemAddedWithName": "{0} به کتابخانه افزوده شد",
"ItemRemovedWithName": "{0} از کتابخانه حذف شد",
"LabelIpAddressValue": "آدرس آی پی: {0}",
"LabelRunningTimeValue": "زمان اجرا: {0}",
"Latest": "جدیدترین‌ها",
"MessageApplicationUpdated": "سرور Jellyfin بروزرسانی شد",
"MessageApplicationUpdatedTo": "سرور Jellyfin به نسخه {0} بروزرسانی شد",
"MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
"MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد",
"MixedContent": "محتوای مخلوط",
"Movies": "فیلم ها",
"Music": "موسیقی",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "پخش ویدیو آغاز شد",
"NotificationOptionVideoPlaybackStopped": "پخش ویدیو متوقف شد",
"Photos": "عکس‌ها",
"Playlists": "لیست‌های پخش",
"Plugin": "افزونه",
"PluginInstalledWithName": "{0} نصب شد",
"PluginUninstalledWithName": "{0} حذف شد",
"PluginUpdatedWithName": "{0} آپدیت شد",
"ProviderValue": "ارائه دهنده: {0}",
"ScheduledTaskFailedWithName": "{0} شکست خورد",
"ScheduledTaskStartedWithName": "{0} شروع شد",
"ServerNameNeedsToBeRestarted": "{0} نیاز به راه اندازی مجدد دارد",
"Shows": "سریال‌ها",
"Songs": "موسیقی‌ها",
"StartupEmbyServerIsLoading": "سرور Jellyfin در حال بارگیری است. لطفا کمی بعد دوباره تلاش کنید.",
"SubtitleDownloadFailureFromForItem": "بارگیری زیرنویس برای {1} از {0} شکست خورد",
"Sync": "همگام‌سازی",
"System": "سیستم",
"TvShows": "سریال‌های تلویزیونی",
"User": "کاربر",
"UserCreatedWithName": "کاربر {0} ایجاد شد",
"UserDeletedWithName": "کاربر {0} حذف شد",
"UserDownloadingItemWithValues": "{0} در حال بارگیری {1} می‌باشد",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "ارتباط {0} از {1} قطع شد",
"UserOnlineFromDevice": "{0} از {1} آنلاین می‌باشد",
"UserPasswordChangedWithName": "گذرواژه برای کاربر {0} تغییر کرد",
"UserPolicyUpdatedWithName": "سیاست کاربری برای {0} بروزرسانی شد",
"UserStartedPlayingItemWithValues": "{0} در حال پخش {1} بر روی {2} است",
"UserStoppedPlayingItemWithValues": "{0} پخش {1} را بر روی {2} به پایان رساند",
"ValueHasBeenAddedToLibrary": "{0} به کتابخانه‌ی رسانه‌ی شما افزوده شد",
"ValueSpecialEpisodeName": "ویژه - {0}",
"VersionNumber": "نسخه {0}",
"TaskCleanTranscodeDescription": "فایل‌های کدگذاری که قدیمی‌تر از یک روز هستند را حذف می‌کند.",
"TaskCleanTranscode": "پاکسازی مسیر کد گذاری",
@@ -97,6 +126,8 @@
"HearingImpaired": "مشکل شنوایی",
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
"TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه.",
"TaskCleanCollectionsAndPlaylists": "پاکسازی مجموعه ها و لیست پخش",
"TaskCleanCollectionsAndPlaylistsDescription": "موارد را از مجموعه ها و لیست پخش هایی که دیگر وجود ندارند حذف میکند.",
"TaskAudioNormalizationDescription": "بررسی فایل برای داده‌های نرمال کردن صدا.",
"TaskDownloadMissingLyrics": "دانلود متن‌های ناموجود",
"TaskDownloadMissingLyricsDescription": "دانلود متن شعر‌ها",

View File

@@ -8,33 +8,57 @@
"Music": "Musiikki",
"Movies": "Elokuvat",
"MixedContent": "Sekalainen sisältö",
"MessageServerConfigurationUpdated": "Palvelimen asetukset on päivitetty",
"MessageNamedServerConfigurationUpdatedWithValue": "Palvelimen asetusten osio {0} on päivitetty",
"MessageApplicationUpdatedTo": "Jellyfin-palvelin on päivitetty versioon {0}",
"MessageApplicationUpdated": "Jellyfin-palvelin on päivitetty",
"Latest": "Viimeisimmät",
"LabelRunningTimeValue": "Kesto: {0}",
"LabelIpAddressValue": "IP-osoite: {0}",
"ItemRemovedWithName": "{0} poistettiin kirjastosta",
"ItemAddedWithName": "{0} lisättiin kirjastoon",
"Inherit": "Peri",
"HomeVideos": "Kotivideot",
"HeaderRecordingGroups": "Tallennusryhmät",
"HeaderNextUp": "Seuraavaksi",
"HeaderFavoriteSongs": "Suosikkikappaleet",
"HeaderFavoriteShows": "Suosikkisarjat",
"HeaderFavoriteEpisodes": "Suosikkijaksot",
"HeaderFavoriteArtists": "Suosikkiesittäjät",
"HeaderFavoriteAlbums": "Suosikkialbumit",
"HeaderContinueWatching": "Jatka katselua",
"HeaderAlbumArtists": "Albumin esittäjät",
"Genres": "Tyylilajit",
"Folders": "Kansiot",
"Favorites": "Suosikit",
"FailedLoginAttemptWithUserName": "Epäonnistunut kirjautumisyritys lähteestä \"{0}\"",
"DeviceOnlineWithName": "{0} on yhdistetty",
"DeviceOfflineWithName": "{0} on katkaissut yhteyden",
"Collections": "Kokoelmat",
"ChapterNameValue": "Kappale {0}",
"Channels": "Kanavat",
"CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}",
"Books": "Kirjat",
"AuthenticationSucceededWithUserName": "{0} todennus onnistunut",
"Artists": "Artistit",
"AuthenticationSucceededWithUserName": "{0} on todennettu",
"Artists": "Esittäjät",
"Application": "Sovellus",
"AppDeviceValues": "Sovellus: {0}, Laite: {1}",
"Albums": "Albumit",
"User": "Käyttäjä",
"System": "Järjestelmä",
"ScheduledTaskFailedWithName": "{0} epäonnistui",
"PluginUpdatedWithName": "{0} päivitettiin",
"PluginInstalledWithName": "{0} asennettiin",
"Photos": "Valokuvat",
"ScheduledTaskStartedWithName": "\"{0}\" käynnistetty",
"PluginUninstalledWithName": "{0} poistettiin",
"Playlists": "Soittolistat",
"VersionNumber": "Versio {0}",
"ValueSpecialEpisodeName": "Erikoisjakso - {0}",
"ValueHasBeenAddedToLibrary": "\"{0}\" on lisätty mediakirjastoon",
"UserStoppedPlayingItemWithValues": "{0} lopetti kohteen \"{1}\" toiston sijainnissa \"{2}\"",
"UserStartedPlayingItemWithValues": "{0} toistaa kohdetta \"{1}\" sijainnissa \"{2}\"",
"UserPolicyUpdatedWithName": "Käyttäjän {0} käyttöoikeudet on päivitetty",
"UserPasswordChangedWithName": "Käyttäjän {0} salasana on vaihdettu",
"UserOnlineFromDevice": "{0} on yhdistänyt sijainnista \"{1}\"",
"UserOfflineFromDevice": "{0} on katkaissut yhteyden sijainnista \"{1}\"",
@@ -43,9 +67,14 @@
"UserDeletedWithName": "Käyttäjä {0} on poistettu",
"UserCreatedWithName": "Käyttäjä {0} on luotu",
"TvShows": "Sarjat",
"Sync": "Synkronointi",
"SubtitleDownloadFailureFromForItem": "Tekstityksen lataus lähteestä \"{0}\" kohteelle \"{1}\" epäonnistui",
"StartupEmbyServerIsLoading": "Jellyfin-palvelin on latautumassa. Yritä hetken kuluttua uudelleen.",
"Songs": "Kappaleet",
"Shows": "Sarjat",
"ServerNameNeedsToBeRestarted": "\"{0}\" on käynnistettävä uudelleen",
"ProviderValue": "Lähde: {0}",
"Plugin": "Lisäosa",
"NotificationOptionVideoPlaybackStopped": "Videon toisto lopetettu",
"NotificationOptionVideoPlayback": "Videon toisto aloitettu",
"NotificationOptionUserLockedOut": "Käyttäjä on lukittu",
@@ -97,6 +126,8 @@
"HearingImpaired": "Kuulorajoitteinen",
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja.",
"TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka",

View File

@@ -1,7 +1,10 @@
{
"VersionNumber": "Bersyon {0}",
"ValueSpecialEpisodeName": "Espesyal - {0}",
"ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong librerya ng medya",
"UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
"UserStartedPlayingItemWithValues": "Si {0} ay nagpla-play ng {1} sa {2}",
"UserPolicyUpdatedWithName": "Ang user policy ay nai-update para kay {0}",
"UserPasswordChangedWithName": "Napalitan na ang password ni {0}",
"UserOnlineFromDevice": "Si {0} ay naka-konekta galing sa {1}",
"UserOfflineFromDevice": "Si {0} ay na-diskonekta galing sa {1}",
@@ -9,14 +12,23 @@
"UserDownloadingItemWithValues": "Nagdadownload si {0} ng {1}",
"UserDeletedWithName": "Natanggal na is user {0}",
"UserCreatedWithName": "Nagawa na si user {0}",
"User": "User",
"TvShows": "Mga Palabas sa Telebisyon",
"System": "Sistema",
"Sync": "Pag-sync",
"SubtitleDownloadFailureFromForItem": "Hindi nai-download ang subtitles {0} para sa {1}",
"StartupEmbyServerIsLoading": "Naglo-load ang Jellyfin Server. Mangyaring subukan ulit sandali.",
"Songs": "Mga Kanta",
"Shows": "Mga Pelikula",
"ServerNameNeedsToBeRestarted": "Kailangan irestart ang {0}",
"ScheduledTaskStartedWithName": "Nagsimula na ang {0}",
"ScheduledTaskFailedWithName": "Hindi gumana ang {0}",
"ProviderValue": "Tagapagtustos: {0}",
"PluginUpdatedWithName": "Naiupdate na ang {0}",
"PluginUninstalledWithName": "Naiuninstall na ang {0}",
"PluginInstalledWithName": "Nainstall na ang {0}",
"Plugin": "Plugin",
"Playlists": "Mga Playlist",
"Photos": "Mga Larawan",
"NotificationOptionVideoPlaybackStopped": "Huminto na ang pelikula",
"NotificationOptionVideoPlayback": "Nagsimula na ang pelikula",
@@ -42,25 +54,42 @@
"Music": "Mga Kanta",
"Movies": "Mga Pelikula",
"MixedContent": "Halo-halong content",
"MessageServerConfigurationUpdated": "Naiupdate na ang server configuration",
"MessageNamedServerConfigurationUpdatedWithValue": "Naiupdate na ang server configuration section {0}",
"MessageApplicationUpdatedTo": "Ang bersyon ng Jellyfin Server ay naiupdate sa {0}",
"MessageApplicationUpdated": "Naiupdate na ang Jellyfin Server",
"Latest": "Pinakabago",
"LabelRunningTimeValue": "Oras: {0}",
"LabelIpAddressValue": "IP address: {0}",
"ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
"ItemAddedWithName": "Naidagdag ang {0} sa librerya",
"Inherit": "Manahin",
"HeaderRecordingGroups": "Pagtatalang Grupo",
"HeaderNextUp": "Susunod",
"HeaderLiveTV": "Live TV",
"HeaderFavoriteSongs": "Mga Paboritong Kanta",
"HeaderFavoriteShows": "Mga Paboritong Pelikula",
"HeaderFavoriteEpisodes": "Mga Paboritong Yugto",
"HeaderFavoriteArtists": "Mga Paboritong Artista",
"HeaderFavoriteAlbums": "Mga Paboritong Album",
"HeaderContinueWatching": "Magpatuloy sa Panonood",
"HeaderAlbumArtists": "Mga Artista ng Album",
"Genres": "Mga Kategorya",
"Folders": "Mga Folder",
"Favorites": "Mga Paborito",
"FailedLoginAttemptWithUserName": "Maling login galing kay/sa {0}",
"DeviceOnlineWithName": "Nakakonekta si/ang {0}",
"DeviceOfflineWithName": "Nadiskonekta si/ang {0}",
"Collections": "Mga Koleksyon",
"ChapterNameValue": "Kabanata {0}",
"Channels": "Mga Channel",
"CameraImageUploadedFrom": "May bagong larawan na naupload galing sa/kay {0}",
"Books": "Mga Libro",
"AuthenticationSucceededWithUserName": "Napatunayan si/ang {0}",
"Artists": "Mga Artista",
"Application": "Aplikasyon",
"AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
"Albums": "Mga Album",
"TaskRefreshLibrary": "Suriin and Librerya ng Medya",
"TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
"TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",

View File

@@ -2,15 +2,17 @@
"Artists": "Listafólk",
"Collections": "Søvn",
"Default": "Sjálvgildi",
"DeviceOfflineWithName": "{0} hevur slitið sambandið",
"External": "Ytri",
"Genres": "Greinar",
"Albums": "Album",
"AppDeviceValues": "App: {0}, Eind: {1}",
"Application": "Nýtsluskipan",
"Books": "Bøkur",
"Channels": "Rásir",
"ChapterNameValue": "Kapittul {0}",
"DeviceOnlineWithName": "{0} er sambundið",
"Favorites": "Yndis",
"Folders": "Mappur",
"Forced": "Kravt",
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
"HeaderFavoriteEpisodes": "Yndispartar",
"LabelIpAddressValue": "IP atsetur: {0}"
"Forced": "Kravt"
}

View File

@@ -1,24 +1,41 @@
{
"Albums": "Albums",
"AppDeviceValues": "App : {0}, Appareil : {1}",
"Application": "Application",
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
"CameraImageUploadedFrom": "Une nouvelle photo a été téléversée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
"DeviceOfflineWithName": "{0} s'est déconnecté",
"DeviceOnlineWithName": "{0} est connecté",
"FailedLoginAttemptWithUserName": "Tentative de connexion échouée par {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes de l'album",
"HeaderContinueWatching": "Reprendre le visionnement",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes favoris",
"HeaderFavoriteEpisodes": "Épisodes favoris",
"HeaderFavoriteShows": "Séries favorites",
"HeaderFavoriteSongs": "Chansons favorites",
"HeaderLiveTV": "TV en direct",
"HeaderNextUp": "À Suivre",
"HeaderRecordingGroups": "Groupes d'enregistrements",
"HomeVideos": "Vidéos personnelles",
"Inherit": "Hérite",
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
"LabelIpAddressValue": "Adresse IP : {0}",
"LabelRunningTimeValue": "Durée : {0}",
"Latest": "Plus récent",
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
"MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
"MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour",
"MixedContent": "Contenu mixte",
"Movies": "Films",
"Music": "Musique",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Lecture vidéo démarrée",
"NotificationOptionVideoPlaybackStopped": "Lecture vidéo arrêtée",
"Photos": "Photos",
"Playlists": "Listes de lecture",
"Plugin": "Extension",
"PluginInstalledWithName": "{0} a été installé",
"PluginUninstalledWithName": "{0} a été désinstallé",
"PluginUpdatedWithName": "{0} a été mis à jour",
"ProviderValue": "Fournisseur : {0}",
"ScheduledTaskFailedWithName": "{0} a échoué",
"ScheduledTaskStartedWithName": "{0} a commencé",
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
"Shows": "Séries",
"Songs": "Chansons",
"StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
"Sync": "Synchroniser",
"System": "Système",
"TvShows": "Séries Télé",
"User": "Utilisateur",
"UserCreatedWithName": "L'utilisateur {0} a été créé",
"UserDeletedWithName": "L'utilisateur {0} supprimé",
"UserDownloadingItemWithValues": "{0} télécharge {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} s'est déconnecté de {1}",
"UserOnlineFromDevice": "{0} s'est connecté de {1}",
"UserPasswordChangedWithName": "Le mot de passe de utilisateur {0} a été modifié",
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
"UserStartedPlayingItemWithValues": "{0} joue {1} sur {2}",
"UserStoppedPlayingItemWithValues": "{0} a terminé la lecture de {1} sur {2}",
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
"ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}",
"TasksLibraryCategory": "Médiathèque",
"TasksMaintenanceCategory": "Entretien",
@@ -97,6 +126,8 @@
"HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
"TaskAudioNormalization": "Normalisation audio",
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
"TaskExtractMediaSegments": "Analyse des segments de média",
@@ -106,7 +137,5 @@
"TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
"LyricDownloadFailureFromForItem": "Le téléchargement des paroles a échoué de {0} pour {1}",
"Original": "Original"
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
}

View File

@@ -1,24 +1,41 @@
{
"Albums": "Albums",
"AppDeviceValues": "Application : {0}, Appareil : {1}",
"Application": "Application",
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
"CameraImageUploadedFrom": "Une photo a été téléchargée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
"DeviceOfflineWithName": "{0} s'est déconnecté",
"DeviceOnlineWithName": "{0} est connecté",
"FailedLoginAttemptWithUserName": "Échec de connexion depuis {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes d'albums",
"HeaderContinueWatching": "Continuer de regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
"HeaderFavoriteEpisodes": "Épisodes favoris",
"HeaderFavoriteShows": "Séries favorites",
"HeaderFavoriteSongs": "Chansons préférées",
"HeaderLiveTV": "TV en direct",
"HeaderNextUp": "Prochain à venir",
"HeaderRecordingGroups": "Groupes d'enregistrements",
"HomeVideos": "Vidéos personnelles",
"Inherit": "Hériter",
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
"LabelIpAddressValue": "Adresse IP : {0}",
"LabelRunningTimeValue": "Durée : {0}",
"Latest": "Derniers",
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
"MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour en version {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
"MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour",
"MixedContent": "Contenu mixte",
"Movies": "Films",
"Music": "Musique",
@@ -44,14 +61,23 @@
"NotificationOptionVideoPlayback": "Lecture vidéo démarrée",
"NotificationOptionVideoPlaybackStopped": "Lecture vidéo arrêtée",
"Photos": "Photos",
"Playlists": "Listes de lecture",
"Plugin": "Extension",
"PluginInstalledWithName": "{0} a été installé",
"PluginUninstalledWithName": "{0} a été désinstallé",
"PluginUpdatedWithName": "{0} a été mis à jour",
"ProviderValue": "Fournisseur : {0}",
"ScheduledTaskFailedWithName": "{0} a échoué",
"ScheduledTaskStartedWithName": "{0} a démarré",
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
"Shows": "Séries",
"Songs": "Chansons",
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
"Sync": "Synchroniser",
"System": "Système",
"TvShows": "Séries TV",
"User": "Utilisateur",
"UserCreatedWithName": "L'utilisateur {0} a été créé",
"UserDeletedWithName": "L'utilisateur {0} a été supprimé",
"UserDownloadingItemWithValues": "{0} est en train de télécharger {1}",
@@ -59,8 +85,11 @@
"UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}",
"UserOnlineFromDevice": "{0} s'est connecté depuis {1}",
"UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié",
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
"UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}",
"UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
"ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}",
"TasksChannelsCategory": "Chaînes en ligne",
"TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquants sur Internet en se basant sur la configuration des métadonnées.",
@@ -97,6 +126,8 @@
"HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
"TaskAudioNormalization": "Normalisation audio",
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
"TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons",
@@ -106,7 +137,5 @@
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
"LyricDownloadFailureFromForItem": "Le téléchargement des paroles à échoué de {0} pour {1}",
"Original": "Original"
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
}

View File

@@ -1,10 +1,15 @@
{
"Albums": "Albaim",
"Artists": "Ealaíontóirí",
"AuthenticationSucceededWithUserName": "D'éirigh le fíordheimhniú {0}",
"Books": "Leabhair",
"CameraImageUploadedFrom": "Uaslódáladh íomhá ceamara nua ó {0}",
"Channels": "Cainéil",
"ChapterNameValue": "Caibidil {0}",
"Collections": "Bailiúcháin",
"Default": "Réamhshocrú",
"DeviceOfflineWithName": "Tá {0} dícheangailte",
"DeviceOnlineWithName": "Tá {0} nasctha",
"External": "Seachtrach",
"FailedLoginAttemptWithUserName": "Theip ar iarracht logáil isteach ó {0}",
"Favorites": "Ceanáin",
@@ -24,27 +29,41 @@
"TaskRefreshChannelsDescription": "Athnuachan eolas faoi chainéil idirlín.",
"TaskOptimizeDatabase": "Bunachar sonraí a bharrfheabhsú",
"TaskKeyframeExtractorDescription": "Baintear eochairfhrámaí as comhaid físe chun seinmliostaí HLS níos cruinne a chruthú. Féadfaidh an tasc seo a bheith ar siúl ar feadh i bhfad.",
"TaskCleanCollectionsAndPlaylistsDescription": "Baintear míreanna as bailiúcháin agus seinmliostaí nach ann dóibh a thuilleadh.",
"TaskDownloadMissingLyricsDescription": "Íosluchtaigh liricí do na hamhráin",
"TaskUpdatePluginsDescription": "Íoslódálann agus suiteálann nuashonruithe do bhreiseáin atá cumraithe le nuashonrú go huathoibríoch.",
"TaskDownloadMissingSubtitlesDescription": "Déanann sé cuardach ar an idirlíon le haghaidh fotheidil atá ar iarraidh bunaithe ar chumraíocht meiteashonraí.",
"TaskExtractMediaSegmentsDescription": "Sliocht nó faigheann codanna meán ó bhreiseáin chumasaithe MediaSegment.",
"TaskCleanCollectionsAndPlaylists": "Glan suas bailiúcháin agus seinmliostaí",
"TaskOptimizeDatabaseDescription": "Comhdhlúthaíonn bunachar sonraí agus gearrtar spás saor in aisce. Má ritheann tú an tasc seo tar éis scanadh a dhéanamh ar an leabharlann nó athruithe eile a dhéanamh a thugann le tuiscint gur cheart go bhfeabhsófaí an fheidhmíocht.",
"TaskMoveTrickplayImagesDescription": "Bogtar comhaid trickplay atá ann cheana de réir socruithe na leabharlainne.",
"AppDeviceValues": "Aip: {0}, Gléas: {1}",
"Application": "Feidhmchlár",
"Folders": "Fillteáin",
"Forced": "Éigean",
"Genres": "Seánraí",
"HeaderAlbumArtists": "Ealaíontóirí albam",
"HeaderContinueWatching": "Leanúint ar aghaidh ag Breathnú",
"HeaderFavoriteAlbums": "Albam is fearr leat",
"HeaderFavoriteArtists": "Ealaíontóirí is Fearr",
"HeaderFavoriteEpisodes": "Eipeasóid is fearr leat",
"HeaderFavoriteShows": "Seónna is Fearr",
"HeaderFavoriteSongs": "Amhráin is fearr leat",
"HeaderLiveTV": "Teilifís beo",
"HeaderNextUp": "Ar Aghaidh Suas",
"HeaderRecordingGroups": "Grúpaí Taifeadta",
"HearingImpaired": "Lag éisteachta",
"HomeVideos": "Físeáin Baile",
"Inherit": "Oidhreacht",
"ItemAddedWithName": "Cuireadh {0} leis an leabharlann",
"ItemRemovedWithName": "Baineadh {0} den leabharlann",
"LabelIpAddressValue": "Seoladh IP: {0}",
"LabelRunningTimeValue": "Am rite: {0}",
"Latest": "Is déanaí",
"MessageApplicationUpdated": "Tá Freastalaí Jellyfin nuashonraithe",
"MessageApplicationUpdatedTo": "Nuashonraíodh Freastalaí Jellyfin go {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Nuashonraíodh an chuid cumraíochta freastalaí {0}",
"MessageServerConfigurationUpdated": "Nuashonraíodh cumraíocht an fhreastalaí",
"MixedContent": "Ábhar measctha",
"Movies": "Scannáin",
"Music": "Ceol",
@@ -70,15 +89,24 @@
"NotificationOptionVideoPlayback": "Cuireadh tús le hathsheinm físe",
"NotificationOptionVideoPlaybackStopped": "Cuireadh deireadh le hathsheinm físe",
"Photos": "Grianghraif",
"Playlists": "Seinmliostaí",
"Plugin": "Breiseán",
"PluginInstalledWithName": "Suiteáladh {0}",
"PluginUninstalledWithName": "Díshuiteáladh {0}",
"PluginUpdatedWithName": "Nuashonraíodh {0}",
"ProviderValue": "Soláthraí: {0}",
"ScheduledTaskFailedWithName": "Theip ar {0}",
"ScheduledTaskStartedWithName": "Thosaigh {0}",
"ServerNameNeedsToBeRestarted": "Ní mór {0} a atosú",
"Shows": "Seónna",
"Songs": "Amhráin",
"StartupEmbyServerIsLoading": "Tá freastalaí Jellyfin á luchtú. Bain triail eile as gan mhoill.",
"SubtitleDownloadFailureFromForItem": "Theip ar fhotheidil a íoslódáil ó {0} le haghaidh {1}",
"Sync": "Sioncrónaigh",
"System": "Córas",
"TvShows": "Seónna Teilifíse",
"Undefined": "Neamhshainithe",
"User": "Úsáideoir",
"UserCreatedWithName": "Cruthaíodh úsáideoir {0}",
"UserDeletedWithName": "Scriosadh úsáideoir {0}",
"UserDownloadingItemWithValues": "Tá {0} á íoslódáil {1}",
@@ -86,8 +114,11 @@
"UserOfflineFromDevice": "Tá {0} dícheangailte ó {1}",
"UserOnlineFromDevice": "Tá {0} ar líne ó {1}",
"UserPasswordChangedWithName": "Athraíodh pasfhocal don úsáideoir {0}",
"UserPolicyUpdatedWithName": "Nuashonraíodh polasaí úsáideora le haghaidh {0}",
"UserStartedPlayingItemWithValues": "Tá {0} ag seinnt {1} ar {2}",
"UserStoppedPlayingItemWithValues": "Chríochnaigh {0} ag imirt {1} ar {2}",
"ValueHasBeenAddedToLibrary": "Cuireadh {0} le do leabharlann meán",
"ValueSpecialEpisodeName": "Speisialta - {0}",
"VersionNumber": "Leagan {0}",
"TasksMaintenanceCategory": "Cothabháil",
"TasksLibraryCategory": "Leabharlann",
@@ -106,6 +137,5 @@
"TaskCleanTranscode": "Eolaire Transcode Glan",
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh",
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
"CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad.",
"Original": "Bunaidh"
"CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad."
}

View File

@@ -1,9 +1,13 @@
{
"Albums": "Álbums",
"Collections": "Coleccións",
"ChapterNameValue": "Capítulo {0}",
"Channels": "Canles",
"CameraImageUploadedFrom": "Cargouse unha nova imaxe de cámara dende {0}",
"Books": "Libros",
"AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
"Artists": "Artistas",
"Application": "Aplicación",
"NotificationOptionServerRestartRequired": "Necesario o reinicio do servidor",
"NotificationOptionPluginUpdateInstalled": "Actualización do plugin instalada",
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
@@ -24,41 +28,63 @@
"Music": "Música",
"Movies": "Películas",
"MixedContent": "Contido mixto",
"MessageServerConfigurationUpdated": "Actualizouse a configuración do servidor",
"MessageNamedServerConfigurationUpdatedWithValue": "Actualizouse a sección de configuración {0} do servidor",
"MessageApplicationUpdatedTo": "O servidor Jellyfin actualizouse a {0}",
"MessageApplicationUpdated": "O servidor Jellyfin actualizouse",
"Latest": "Último",
"LabelRunningTimeValue": "Tempo en execución: {0}",
"LabelIpAddressValue": "Enderezo IP: {0}",
"ItemRemovedWithName": "{0} eliminouse da biblioteca",
"ItemAddedWithName": "{0} engadiuse á biblioteca",
"Inherit": "Herdar",
"HomeVideos": "Videos caseiros",
"HeaderRecordingGroups": "Grupos de grabación",
"HeaderNextUp": "De seguido",
"HeaderLiveTV": "TV en directo",
"HeaderFavoriteSongs": "Cancións favoritas",
"HeaderFavoriteShows": "Series de TV favoritas",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteAlbums": "Álbums favoritos",
"HeaderContinueWatching": "Seguir vendo",
"HeaderAlbumArtists": "Artistas do álbum",
"Genres": "Xéneros",
"Forced": "Forzado",
"Folders": "Cartafoles",
"Favorites": "Favoritos",
"FailedLoginAttemptWithUserName": "Fallo de intento de inicio de sesión dende {0}",
"DeviceOnlineWithName": "{0} conectouse",
"DeviceOfflineWithName": "{0} desconectouse",
"Default": "Por defecto",
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"TaskCleanLogs": "Limpar directorio de rexistros",
"TaskCleanActivityLog": "Limpar rexistro de actividade",
"TasksChannelsCategory": "Canles da Internet",
"TaskUpdatePlugins": "Actualizar plugins",
"User": "Usuario",
"Undefined": "Sen definir",
"TvShows": "Programas de TV",
"System": "Sistema",
"Sync": "Sincronizar",
"SubtitleDownloadFailureFromForItem": "Fallou a descarga de subtítulos para {1} dende {0}",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está cargando. Por favor, ténteo axiña outra vez.",
"Songs": "Cancións",
"Shows": "Programas",
"ServerNameNeedsToBeRestarted": "{0} precisa ser reiniciado",
"ScheduledTaskStartedWithName": "{0} comezou",
"ScheduledTaskFailedWithName": "{0} fallou",
"ProviderValue": "Provedor: {0}",
"PluginUpdatedWithName": "{0} foi actualizado",
"PluginUninstalledWithName": "{0} foi desinstalado",
"PluginInstalledWithName": "{0} foi instalado",
"Playlists": "Listas de reproducción",
"Photos": "Fotos",
"UserLockedOutWithName": "O usuario {0} foi bloqueado",
"UserDownloadingItemWithValues": "{0} está a ser transferido {1}",
"UserDeletedWithName": "O usuario {0} foi borrado",
"UserCreatedWithName": "O usuario {0} foi creado",
"Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo detida",
"NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada",
"NotificationOptionUserLockedOut": "Usuario bloqueado",
@@ -83,9 +109,12 @@
"TaskCleanCache": "Limpar directorio de caché",
"TaskCleanActivityLogDescription": "Borra do rexistro de actividade as entradas anteriores á data configurada.",
"TasksApplicationCategory": "Aplicación",
"ValueSpecialEpisodeName": "Especial - {0}",
"ValueHasBeenAddedToLibrary": "{0} engadiuse á túa biblioteca de medios",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Mantemento",
"VersionNumber": "Versión {0}",
"UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
"UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
"UserOnlineFromDevice": "{0} está en liña desde {1}",
"UserOfflineFromDevice": "{0} desconectouse dende {1}",
@@ -99,6 +128,8 @@
"TaskRefreshTrickplayImagesDescription": "Crea miniaturas de previsualización para os vídeos nas bibliotecas habilitadas.",
"TaskDownloadMissingLyrics": "Descargar letras que faltan",
"TaskDownloadMissingLyricsDescription": "Descarga as letras das cancións",
"TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución",
"TaskCleanCollectionsAndPlaylistsDescription": "Quita ítems que xa non existen das coleccións e listas de reprodución.",
"TaskExtractMediaSegmentsDescription": "Procura segmentos de medios cos plugins habilitados.",
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
"TaskMoveTrickplayImages": "Migrar as miniaturas de previsualización a outra ubicación",

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