Compare commits

..

2 Commits

Author SHA1 Message Date
Andrew Rabert
4cf53d9f88 Mitigate pull_request_target privilege escalation
Hotfix — replaces pull_request_target with pull_request to stop
granting write permissions and secrets to fork PRs. Some workflows
will break; can be fixed properly later.
2026-02-20 19:10:40 -05:00
crobibero
641a097707 Implement caching for OpenAPI document 2025-11-30 09:04:40 -07:00
612 changed files with 8352 additions and 43786 deletions

View File

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

View File

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

View File

@@ -379,9 +379,6 @@ dotnet_diagnostic.CA1720.severity = suggestion
# disable warning CA1724: Type names should not match namespaces # disable warning CA1724: Type names should not match namespaces
dotnet_diagnostic.CA1724.severity = suggestion dotnet_diagnostic.CA1724.severity = suggestion
# disable warning CA1873: Avoid potentially expensive logging
dotnet_diagnostic.CA1873.severity = suggestion
# disable warning CA1805: Do not initialize unnecessarily # disable warning CA1805: Do not initialize unnecessarily
dotnet_diagnostic.CA1805.severity = suggestion dotnet_diagnostic.CA1805.severity = suggestion
@@ -403,10 +400,6 @@ dotnet_diagnostic.CA1861.severity = suggestion
# disable warning CA2000: Dispose objects before losing scope # disable warning CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = suggestion dotnet_diagnostic.CA2000.severity = suggestion
# TODO: Reevaluate when false positives are fixed: https://github.com/dotnet/roslyn-analyzers/issues/7699
# disable warning CA2025: Do not pass 'IDisposable' instances into unawaited tasks
dotnet_diagnostic.CA2025.severity = suggestion
# disable warning CA2253: Named placeholders should not be numeric values # disable warning CA2253: Named placeholders should not be numeric values
dotnet_diagnostic.CA2253.severity = suggestion dotnet_diagnostic.CA2253.severity = suggestion

View File

@@ -87,9 +87,10 @@ body:
label: Jellyfin Server version label: Jellyfin Server version
description: What version of Jellyfin are you using? description: What version of Jellyfin are you using?
options: options:
- 10.11.8 - 10.11.3
- 10.11.7 - 10.11.2
- 10.11.6 - 10.11.1
- 10.11.0
- Master - Master
- Unstable - Unstable
- Older* - Older*
@@ -138,14 +139,13 @@ body:
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin] - **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode] - **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.] - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
- **CPU Model**: [e.g. AMD Ryzen 5 9600X, Intel Core i7-8565U, etc.]
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.] - **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.] - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.] - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example] - **Base URL**: [e.g. none, yes: /example]
- **Networking**: [e.g. Host, Bridge/NAT] - **Networking**: [e.g. Host, Bridge/NAT]
- **Jellyfin Data Storage & Filesystem**: [e.g. local SATA SSD - ext4, local HDD - NTFS] - **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD]
- **Media Storage & Filesystem**: [e.g. Local HDD - ext4, SMB Share] - **Media Storage**: [e.g. Local HDD, SMB Share]
- **External Integrations**: [e.g. Jellystat, Jellyseerr] - **External Integrations**: [e.g. Jellystat, Jellyseerr]
value: | value: |
- OS: - OS:
@@ -156,14 +156,13 @@ body:
- FFmpeg Version: - FFmpeg Version:
- Playback Method: - Playback Method:
- Hardware Acceleration: - Hardware Acceleration:
- CPU Model:
- GPU Model: - GPU Model:
- Plugins: - Plugins:
- Reverse Proxy: - Reverse Proxy:
- Base URL: - Base URL:
- Networking: - Networking:
- Jellyfin Data Storage & Filesystem: - Jellyfin Data Storage:
- Media Storage & Filesystem: - Media Storage:
- External Integrations: - External Integrations:
render: markdown render: markdown
validations: validations:

View File

@@ -8,10 +8,6 @@ on:
schedule: schedule:
- cron: '24 2 * * 4' - cron: '24 2 * * 4'
permissions:
contents: read
security-events: write
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
@@ -24,21 +20,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '10.0.x' dotnet-version: '9.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5

View File

@@ -11,22 +11,22 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '10.0.x' dotnet-version: '9.0.x'
- name: Build - name: Build
run: | run: |
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: abi-head name: abi-head
retention-days: 14 retention-days: 14
@@ -40,16 +40,16 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0 fetch-depth: 0
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: '10.0.x' dotnet-version: '9.0.x'
- name: Checkout common ancestor - name: Checkout common ancestor
env: env:
@@ -65,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: abi-base name: abi-base
retention-days: 14 retention-days: 14
@@ -85,13 +85,13 @@ jobs:
steps: steps:
- name: Download abi-head - name: Download abi-head
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: abi-head name: abi-head
path: abi-head path: abi-head
- name: Download abi-base - name: Download abi-base
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: abi-base name: abi-base
path: abi-base path: abi-base
@@ -106,7 +106,7 @@ jobs:
{ {
echo 'body<<EOF' echo 'body<<EOF'
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 || true )" COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
printf "\n${file}\n${COMPAT_OUTPUT}\n" printf "\n${file}\n${COMPAT_OUTPUT}\n"
fi fi

271
.github/workflows/ci-openapi.yml vendored Normal file
View File

@@ -0,0 +1,271 @@
name: OpenAPI
on:
push:
branches:
- master
tags:
- 'v*'
pull_request:
permissions: {}
jobs:
openapi-head:
name: OpenAPI - HEAD
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with:
dotnet-version: '9.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: openapi-head
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
openapi-base:
name: OpenAPI - BASE
if: ${{ github.base_ref != '' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with:
dotnet-version: '9.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
openapi-diff:
permissions:
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
name: OpenAPI - Difference
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
needs:
- openapi-head
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: openapi-base
path: openapi-base
- name: Workaround openapi-diff issue
run: |
sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
- name: Calculate OpenAPI difference
uses: docker://openapitools/openapi-diff
continue-on-error: true
with:
args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
- id: read-diff
name: Read openapi-diff output
run: |
# Read and fix markdown
body=$(cat openapi-changes.md)
# Write to workflow summary
echo "$body" >> $GITHUB_STEP_SUMMARY
# Set ApiChanged var
if [ "$body" != '' ]; then
echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
else
echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
fi
# Add header/footer for diff comment
echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md
echo "<details>" >> openapi-changes-reply.md
echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md
echo "" >> openapi-changes-reply.md
echo "$body" >> openapi-changes-reply.md
echo "" >> openapi-changes-reply.md
echo "</details>" >> openapi-changes-reply.md
- name: Find difference comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body-path: openapi-changes-reply.md
- name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
<!--openapi-diff-workflow-comment-->
No changes to OpenAPI specification found. See history of this comment for previous changes.
publish-unstable:
name: OpenAPI - Publish Unstable Spec
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- 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@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (unstable) to repository server
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
source: openapi-head/openapi.json
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
debug: false
script_stop: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/unstable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
if diff /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/${LAST_SPEC} &>/dev/null; then
rm -r /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
exit 0
fi
# Move new spec into place
sudo mv /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
# Delete previous jellyfin-openapi-unstable_previous.json
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
# Move current jellyfin-openapi-unstable.json symlink to jellyfin-openapi-unstable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-unstable.json ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
# Create new jellyfin-openapi-unstable.json symlink
sudo ln -s unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-unstable.json
# Check that the previous openapi unstable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-unstable_previous.json )" != "unstable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
fi
) 200>/run/workflows/openapi-unstable.lock
publish-stable:
name: OpenAPI - Publish Stable Spec
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- 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@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (stable) to repository server
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
source: openapi-head/openapi.json
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
debug: false
script_stop: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
exit 0
fi
# Move new spec into place
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
# Delete previous jellyfin-openapi-stable_previous.json
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Create new jellyfin-openapi-stable.json symlink
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
# Check that the previous openapi stable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
fi
) 200>/run/workflows/openapi-stable.lock

View File

@@ -9,7 +9,7 @@ on:
pull_request: pull_request:
env: env:
SDK_VERSION: "10.0.x" SDK_VERSION: "9.0.x"
jobs: jobs:
run-tests: run-tests:
@@ -20,9 +20,9 @@ jobs:
runs-on: "${{ matrix.os }}" runs-on: "${{ matrix.os }}"
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with: with:
dotnet-version: ${{ env.SDK_VERSION }} dotnet-version: ${{ env.SDK_VERSION }}
@@ -35,7 +35,7 @@ jobs:
--verbosity minimal --verbosity minimal
- name: Merge code coverage results - name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10 uses: danielpalme/ReportGenerator-GitHub-Action@dcdfb6e704e87df6b2ed0cf123a6c9f69e364869 # v5.5.0
with: with:
reports: "**/coverage.cobertura.xml" reports: "**/coverage.cobertura.xml"
targetdir: "merged/" targetdir: "merged/"

View File

@@ -24,7 +24,7 @@ jobs:
reactions: '+1' reactions: '+1'
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@@ -36,23 +36,20 @@ jobs:
rename: rename:
name: 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 runs-on: ubuntu-latest
steps: steps:
- name: pull in script - name: pull in script
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
repository: jellyfin/jellyfin-triage-script repository: jellyfin/jellyfin-triage-script
- name: install python - name: install python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: '3.14' python-version: '3.14'
cache: 'pip' cache: 'pip'
- name: install python packages - name: install python packages
run: pip install -r rename/requirements.txt run: pip install -r rename/requirements.txt
- name: run rename script - name: run rename script
run: python3 rename.py run: python3 rename.py
working-directory: ./rename working-directory: ./rename

View File

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

View File

@@ -10,19 +10,16 @@ jobs:
issues: write issues: write
steps: steps:
- name: pull in script - name: pull in script
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
repository: jellyfin/jellyfin-triage-script repository: jellyfin/jellyfin-triage-script
- name: install python - name: install python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: '3.14' python-version: '3.14'
cache: 'pip' cache: 'pip'
- name: install python packages - name: install python packages
run: pip install -r main-repo-triage/requirements.txt run: pip install -r main-repo-triage/requirements.txt
- name: check and comment issue - name: check and comment issue
working-directory: ./main-repo-triage working-directory: ./main-repo-triage
run: python3 single_issue_gha.py run: python3 single_issue_gha.py

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,140 +0,0 @@
name: OpenAPI Publish
on:
push:
branches:
- master
tags:
- 'v*'
jobs:
publish-openapi:
name: OpenAPI - Publish Artifact
uses: ./.github/workflows/openapi-generate.yml
permissions:
contents: read
with:
ref: ${{ github.sha }}
repository: ${{ github.repository }}
artifact: openapi-head
publish-unstable:
name: OpenAPI - Publish Unstable Spec
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- publish-openapi
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
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (unstable) to repository server
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
source: openapi-head/openapi.json
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
debug: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/unstable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
if diff /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/${LAST_SPEC} &>/dev/null; then
rm -r /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
exit 0
fi
# Move new spec into place
sudo mv /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
# Delete previous jellyfin-openapi-unstable_previous.json
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
# Move current jellyfin-openapi-unstable.json symlink to jellyfin-openapi-unstable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-unstable.json ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
# Create new jellyfin-openapi-unstable.json symlink
sudo ln -s unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-unstable.json
# Check that the previous openapi unstable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-unstable_previous.json )" != "unstable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
fi
) 200>/run/workflows/openapi-unstable.lock
publish-stable:
name: OpenAPI - Publish Stable Spec
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- publish-openapi
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
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (stable) to repository server
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
source: openapi-head/openapi.json
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
debug: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
exit 0
fi
# Move new spec into place
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
# Delete previous jellyfin-openapi-stable_previous.json
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Create new jellyfin-openapi-stable.json symlink
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
# Check that the previous openapi stable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
fi
) 200>/run/workflows/openapi-stable.lock

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

@@ -21,7 +21,6 @@ jobs:
with: with:
project: Current Release project: Current Release
action: delete action: delete
column: In progress
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project - name: Add to 'Release Next' project

View File

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

View File

@@ -28,12 +28,12 @@ jobs:
timeoutSeconds: 3600 timeoutSeconds: 3600
- name: Setup YQ - name: Setup YQ
uses: chrisdickinson/setup-yq@fa3192edd79d6eb0e4e12de8dde3a0c26f2b853b # latest uses: chrisdickinson/setup-yq@latest
with: with:
yq-version: v4.9.8 yq-version: v4.9.8
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}

6
.vscode/launch.json vendored
View File

@@ -6,7 +6,7 @@
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll", "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": [], "args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll", "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": ["--nowebclient"], "args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",
@@ -34,7 +34,7 @@
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll", "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"], "args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",

View File

@@ -1,6 +1,5 @@
# Jellyfin Contributors # Jellyfin Contributors
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
- [1337joe](https://github.com/1337joe) - [1337joe](https://github.com/1337joe)
- [97carmine](https://github.com/97carmine) - [97carmine](https://github.com/97carmine)
- [Abbe98](https://github.com/Abbe98) - [Abbe98](https://github.com/Abbe98)
@@ -15,7 +14,7 @@
- [bilde2910](https://github.com/bilde2910) - [bilde2910](https://github.com/bilde2910)
- [bfayers](https://github.com/bfayers) - [bfayers](https://github.com/bfayers)
- [BnMcG](https://github.com/BnMcG) - [BnMcG](https://github.com/BnMcG)
- [Bond_009](https://github.com/Bond-009) - [Bond-009](https://github.com/Bond-009)
- [brianjmurrell](https://github.com/brianjmurrell) - [brianjmurrell](https://github.com/brianjmurrell)
- [bugfixin](https://github.com/bugfixin) - [bugfixin](https://github.com/bugfixin)
- [chaosinnovator](https://github.com/chaosinnovator) - [chaosinnovator](https://github.com/chaosinnovator)
@@ -32,7 +31,6 @@
- [DaveChild](https://github.com/DaveChild) - [DaveChild](https://github.com/DaveChild)
- [DavidFair](https://github.com/DavidFair) - [DavidFair](https://github.com/DavidFair)
- [Delgan](https://github.com/Delgan) - [Delgan](https://github.com/Delgan)
- [DerMaddis](https://github.com/dermaddis)
- [Derpipose](https://github.com/Derpipose) - [Derpipose](https://github.com/Derpipose)
- [dcrdev](https://github.com/dcrdev) - [dcrdev](https://github.com/dcrdev)
- [dhartung](https://github.com/dhartung) - [dhartung](https://github.com/dhartung)
@@ -56,7 +54,6 @@
- [geilername](https://github.com/geilername) - [geilername](https://github.com/geilername)
- [GermanCoding](https://github.com/GermanCoding) - [GermanCoding](https://github.com/GermanCoding)
- [gnattu](https://github.com/gnattu) - [gnattu](https://github.com/gnattu)
- [gnuyent](https://github.com/gnuyent)
- [GodTamIt](https://github.com/GodTamIt) - [GodTamIt](https://github.com/GodTamIt)
- [grafixeyehero](https://github.com/grafixeyehero) - [grafixeyehero](https://github.com/grafixeyehero)
- [h1nk](https://github.com/h1nk) - [h1nk](https://github.com/h1nk)
@@ -64,7 +61,6 @@
- [HelloWorld017](https://github.com/HelloWorld017) - [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog) - [ikomhoog](https://github.com/ikomhoog)
- [iwalton3](https://github.com/iwalton3) - [iwalton3](https://github.com/iwalton3)
- [Jakob Kukla](https://github.com/jakobkukla)
- [jftuga](https://github.com/jftuga) - [jftuga](https://github.com/jftuga)
- [jkhsjdhjs](https://github.com/jkhsjdhjs) - [jkhsjdhjs](https://github.com/jkhsjdhjs)
- [jmshrv](https://github.com/jmshrv) - [jmshrv](https://github.com/jmshrv)
@@ -73,10 +69,8 @@
- [JustAMan](https://github.com/JustAMan) - [JustAMan](https://github.com/JustAMan)
- [justinfenn](https://github.com/justinfenn) - [justinfenn](https://github.com/justinfenn)
- [JPVenson](https://github.com/JPVenson) - [JPVenson](https://github.com/JPVenson)
- [JPUC1143](https://github.com/Jpuc1143/)
- [KerryRJ](https://github.com/KerryRJ) - [KerryRJ](https://github.com/KerryRJ)
- [Larvitar](https://github.com/Larvitar) - [Larvitar](https://github.com/Larvitar)
- [lbenini](https://github.com/lbenini)
- [LeoVerto](https://github.com/LeoVerto) - [LeoVerto](https://github.com/LeoVerto)
- [Liggy](https://github.com/Liggy) - [Liggy](https://github.com/Liggy)
- [lmaonator](https://github.com/lmaonator) - [lmaonator](https://github.com/lmaonator)
@@ -89,19 +83,15 @@
- [marius-luca-87](https://github.com/marius-luca-87) - [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro) - [mark-monteiro](https://github.com/mark-monteiro)
- [MarkCiliaVincenti](https://github.com/MarkCiliaVincenti) - [MarkCiliaVincenti](https://github.com/MarkCiliaVincenti)
- [Martin Reuter](https://github.com/reuterma24)
- [Matt07211](https://github.com/Matt07211) - [Matt07211](https://github.com/Matt07211)
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [Maxr1998](https://github.com/Maxr1998) - [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00) - [mcarlton00](https://github.com/mcarlton00)
- [Michael McElroy](https://github.com/mcmcelro)
- [mitchfizz05](https://github.com/mitchfizz05) - [mitchfizz05](https://github.com/mitchfizz05)
- [mohd-akram](https://github.com/mohd-akram) - [mohd-akram](https://github.com/mohd-akram)
- [MrTimscampi](https://github.com/MrTimscampi) - [MrTimscampi](https://github.com/MrTimscampi)
- [n8225](https://github.com/n8225) - [n8225](https://github.com/n8225)
- [Nalsai](https://github.com/Nalsai) - [Nalsai](https://github.com/Nalsai)
- [Narfinger](https://github.com/Narfinger) - [Narfinger](https://github.com/Narfinger)
- [Nathan McCrina](https://github.com/nfmccrina)
- [NathanPickard](https://github.com/NathanPickard) - [NathanPickard](https://github.com/NathanPickard)
- [neilsb](https://github.com/neilsb) - [neilsb](https://github.com/neilsb)
- [nevado](https://github.com/nevado) - [nevado](https://github.com/nevado)
@@ -112,25 +102,21 @@
- [OancaAndrei](https://github.com/OancaAndrei) - [OancaAndrei](https://github.com/OancaAndrei)
- [obradovichv](https://github.com/obradovichv) - [obradovichv](https://github.com/obradovichv)
- [oddstr13](https://github.com/oddstr13) - [oddstr13](https://github.com/oddstr13)
- [olsh](https://github.com/olsh)
- [orryverducci](https://github.com/orryverducci) - [orryverducci](https://github.com/orryverducci)
- [petermcneil](https://github.com/petermcneil) - [petermcneil](https://github.com/petermcneil)
- [Phlogi](https://github.com/Phlogi) - [Phlogi](https://github.com/Phlogi)
- [pjeanjean](https://github.com/pjeanjean) - [pjeanjean](https://github.com/pjeanjean)
- [ploughpuff](https://github.com/ploughpuff) - [ploughpuff](https://github.com/ploughpuff)
- [poytiis](https://github.com/poytiis)
- [pR0Ps](https://github.com/pR0Ps) - [pR0Ps](https://github.com/pR0Ps)
- [PrplHaz4](https://github.com/PrplHaz4) - [PrplHaz4](https://github.com/PrplHaz4)
- [RazeLighter777](https://github.com/RazeLighter777) - [RazeLighter777](https://github.com/RazeLighter777)
- [redSpoutnik](https://github.com/redSpoutnik) - [redSpoutnik](https://github.com/redSpoutnik)
- [ringmatter](https://github.com/ringmatter) - [ringmatter](https://github.com/ringmatter)
- [Robert Lützner](https://github.com/rluetzner)
- [ryan-hartzell](https://github.com/ryan-hartzell) - [ryan-hartzell](https://github.com/ryan-hartzell)
- [s0urcelab](https://github.com/s0urcelab) - [s0urcelab](https://github.com/s0urcelab)
- [sachk](https://github.com/sachk) - [sachk](https://github.com/sachk)
- [sammyrc34](https://github.com/sammyrc34) - [sammyrc34](https://github.com/sammyrc34)
- [samuel9554](https://github.com/samuel9554) - [samuel9554](https://github.com/samuel9554)
- [SapientGuardian](https://github.com/SapientGuardian)
- [scheidleon](https://github.com/scheidleon) - [scheidleon](https://github.com/scheidleon)
- [sebPomme](https://github.com/sebPomme) - [sebPomme](https://github.com/sebPomme)
- [SegiH](https://github.com/SegiH) - [SegiH](https://github.com/SegiH)
@@ -140,7 +126,6 @@
- [sl1288](https://github.com/sl1288) - [sl1288](https://github.com/sl1288)
- [Smith00101010](https://github.com/Smith00101010) - [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004) - [sorinyo2004](https://github.com/sorinyo2004)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
- [sparky8251](https://github.com/sparky8251) - [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits) - [spookbits](https://github.com/spookbits)
- [ssenart](https://github.com/ssenart) - [ssenart](https://github.com/ssenart)
@@ -163,7 +148,6 @@
- [twinkybot](https://github.com/twinkybot) - [twinkybot](https://github.com/twinkybot)
- [Ullmie02](https://github.com/Ullmie02) - [Ullmie02](https://github.com/Ullmie02)
- [Unhelpful](https://github.com/Unhelpful) - [Unhelpful](https://github.com/Unhelpful)
- [Utku Özdemir](https://github.com/utkuozdemir)
- [viaregio](https://github.com/viaregio) - [viaregio](https://github.com/viaregio)
- [vitorsemeano](https://github.com/vitorsemeano) - [vitorsemeano](https://github.com/vitorsemeano)
- [voodoos](https://github.com/voodoos) - [voodoos](https://github.com/voodoos)
@@ -179,7 +163,6 @@
- [XVicarious](https://github.com/XVicarious) - [XVicarious](https://github.com/XVicarious)
- [YouKnowBlom](https://github.com/YouKnowBlom) - [YouKnowBlom](https://github.com/YouKnowBlom)
- [ZachPhelan](https://github.com/ZachPhelan) - [ZachPhelan](https://github.com/ZachPhelan)
- [ZeusCraft10](https://github.com/ZeusCraft10)
- [KristupasSavickas](https://github.com/KristupasSavickas) - [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta) - [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen) - [nielsvanvelzen](https://github.com/nielsvanvelzen)
@@ -222,15 +205,6 @@
- [theshoeshiner](https://github.com/theshoeshiner) - [theshoeshiner](https://github.com/theshoeshiner)
- [TokerX](https://github.com/TokerX) - [TokerX](https://github.com/TokerX)
- [GeneMarks](https://github.com/GeneMarks) - [GeneMarks](https://github.com/GeneMarks)
- [Kirill Nikiforov](https://github.com/allmazz)
- [bjorntp](https://github.com/bjorntp)
- [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 # Emby Contributors
@@ -294,3 +268,16 @@
- [tikuf](https://github.com/tikuf/) - [tikuf](https://github.com/tikuf/)
- [Tim Hobbs](https://github.com/timhobbs) - [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande) - [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,57 @@
</PropertyGroup> </PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies"> <ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="8.0.2" /> <PackageVersion Include="AsyncKeyedLock" Version="7.1.7" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> <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="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" /> <PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.5.4" /> <PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="10.0.0" /> <PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Diacritics" Version="4.1.8" /> <PackageVersion Include="Diacritics" Version="4.0.17" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <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="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" /> <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Ignore" Version="0.2.1" /> <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="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.7" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.7" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.7" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.7" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.7" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.11" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.11" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.11" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" /> <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="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="1.1.0.5" /> <PackageVersion Include="NEbml" Version="1.1.0.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
@@ -57,11 +62,11 @@
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" /> <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageVersion Include="prometheus-net" Version="8.2.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.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" /> <PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" /> <PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
@@ -69,21 +74,26 @@
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.2.0" /> <PackageVersion Include="SharpFuzz" Version="2.2.0" />
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 --> <!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
<PackageVersion Include="SkiaSharp" Version="[3.116.1]" /> <PackageVersion Include="SkiaSharp" Version="3.116.1" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="[3.116.1]" /> <PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.4.1" /> <PackageVersion Include="Svg.Skia" Version="3.2.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Text.Json" Version="10.0.7" /> <PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" />
<PackageVersion Include="System.Text.Json" Version="9.0.11" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.13.0" /> <PackageVersion Include="z440.atl.core" Version="7.9.0" />
<PackageVersion Include="TMDbLib" Version="3.0.0" /> <PackageVersion Include="TMDbLib" Version="2.3.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.v3" Version="3.2.2" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.v3.Priority" Version="1.1.18" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xunit" Version="2.9.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

@@ -1,75 +0,0 @@
using System.Text.RegularExpressions;
namespace Emby.Naming.Book
{
/// <summary>
/// Helper class to retrieve basic metadata from a book filename.
/// </summary>
public static class BookFileNameParser
{
private const string NameMatchGroup = "name";
private const string IndexMatchGroup = "index";
private const string YearMatchGroup = "year";
private const string SeriesNameMatchGroup = "seriesName";
private static readonly Regex[] _nameMatches =
[
// seriesName (seriesYear) #index (of count) (year) where only seriesName and index are required
new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>[0-9]{4})\))?)\s#(?<index>[0-9]+)((\s\(of\s(?<count>[0-9]+)\))?)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>[0-9]+)\)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"^(?<index>[0-9]+)\s\-\s(?<name>.+?)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"(?<name>.*)\((?<year>[0-9]{4})\)"),
// last resort matches the whole string as the name
new Regex(@"(?<name>.*)")
];
/// <summary>
/// Parse a filename name to retrieve the book name, series name, index, and year.
/// </summary>
/// <param name="name">Book filename to parse for information.</param>
/// <returns>Returns <see cref="BookFileNameParserResult"/> object.</returns>
public static BookFileNameParserResult Parse(string? name)
{
var result = new BookFileNameParserResult();
if (name == null)
{
return result;
}
foreach (var regex in _nameMatches)
{
var match = regex.Match(name);
if (!match.Success)
{
continue;
}
if (match.Groups.TryGetValue(NameMatchGroup, out Group? nameGroup) && nameGroup.Success)
{
result.Name = nameGroup.Value.Trim();
}
if (match.Groups.TryGetValue(IndexMatchGroup, out Group? indexGroup) && indexGroup.Success && int.TryParse(indexGroup.Value, out var index))
{
result.Index = index;
}
if (match.Groups.TryGetValue(YearMatchGroup, out Group? yearGroup) && yearGroup.Success && int.TryParse(yearGroup.Value, out var year))
{
result.Year = year;
}
if (match.Groups.TryGetValue(SeriesNameMatchGroup, out Group? seriesGroup) && seriesGroup.Success)
{
result.SeriesName = seriesGroup.Value.Trim();
}
break;
}
return result;
}
}
}

View File

@@ -1,41 +0,0 @@
using System;
namespace Emby.Naming.Book
{
/// <summary>
/// Data object used to pass metadata parsed from a book filename.
/// </summary>
public class BookFileNameParserResult
{
/// <summary>
/// Initializes a new instance of the <see cref="BookFileNameParserResult"/> class.
/// </summary>
public BookFileNameParserResult()
{
Name = null;
Index = null;
Year = null;
SeriesName = null;
}
/// <summary>
/// Gets or sets the name of the book.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the book index.
/// </summary>
public int? Index { get; set; }
/// <summary>
/// Gets or sets the publication year.
/// </summary>
public int? Year { get; set; }
/// <summary>
/// Gets or sets the series name.
/// </summary>
public string? SeriesName { get; set; }
}
}

View File

@@ -152,8 +152,8 @@ namespace Emby.Naming.Common
CleanStrings = 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>.+?)[ _\,\.\(\)\[\]\-](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]+)?$", @"^(?<cleaned>.+?)(\[.*\])",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)", @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)", @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$", @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
@@ -225,7 +225,6 @@ namespace Emby.Naming.Common
".afc", ".afc",
".amf", ".amf",
".aif", ".aif",
".aifc",
".aiff", ".aiff",
".alac", ".alac",
".amr", ".amr",
@@ -379,14 +378,6 @@ namespace Emby.Naming.Common
IsNamed = true 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/anything_102.mp4
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv // /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
// /server/anything_1996.11.14.mp4 // /server/anything_1996.11.14.mp4

View File

@@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -36,7 +36,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>12.0.0</VersionPrefix> <VersionPrefix>10.12.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

@@ -12,10 +12,10 @@ namespace Emby.Naming.TV
{ {
private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled); 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(); 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(); private static partial Regex ProcessPost();
[GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)] [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="status">The status string.</param>
/// <param name="enumValue">The <see cref="SeriesStatus"/>.</param> /// <param name="enumValue">The <see cref="SeriesStatus"/>.</param>
/// <returns>Returns true if parsing was successful.</returns> /// <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)) if (Enum.TryParse(status, true, out SeriesStatus seriesStatus))
{ {

View File

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

View File

@@ -44,7 +44,7 @@ namespace Emby.Naming.Video
var match = expression.Match(name); var match = expression.Match(name);
if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned)) if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
{ {
newName = cleaned.Value.Trim(); newName = cleaned.Value;
return true; return true;
} }

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -137,27 +136,19 @@ namespace Emby.Naming.Video
if (videos.Count > 1) if (videos.Count > 1)
{ {
var groups = videos var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
.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.Clear(); videos.Clear();
StringComparer comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
foreach (var group in groups) foreach (var group in groups)
{ {
if (group.Key) if (group.Key)
{ {
videos.InsertRange(0, group videos.InsertRange(0, group
.OrderByDescending(x => x.resolutionMatch.Value, comparer) .OrderByDescending(x => ResolutionRegex().Match(x.Files[0].FileNameWithoutExtension.ToString()).Value, new AlphanumericComparator())
.ThenBy(x => x.filename, comparer) .ThenBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
.Select(x => x.value));
} }
else else
{ {
videos.AddRange(group.OrderBy(x => x.filename, comparer).Select(x => x.value)); videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
} }
} }
} }
@@ -217,8 +208,6 @@ namespace Emby.Naming.Video
// The CleanStringParser should have removed common keywords etc. // The CleanStringParser should have removed common keywords etc.
return testFilename.IsEmpty return testFilename.IsEmpty
|| testFilename[0] == '-' || testFilename[0] == '-'
|| testFilename[0] == '_'
|| testFilename[0] == '.'
|| CheckMultiVersionRegex().IsMatch(testFilename); || CheckMultiVersionRegex().IsMatch(testFilename);
} }
} }

View File

@@ -19,7 +19,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>

View File

@@ -90,7 +90,6 @@ namespace Emby.Server.Implementations.AppBase
CreateAndCheckMarker(ProgramDataPath, "data"); CreateAndCheckMarker(ProgramDataPath, "data");
CreateAndCheckMarker(CachePath, "cache"); CreateAndCheckMarker(CachePath, "cache");
CreateAndCheckMarker(DataPath, "data"); CreateAndCheckMarker(DataPath, "data");
CreateCacheDirTag(CachePath);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -101,26 +100,6 @@ namespace Emby.Server.Implementations.AppBase
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive); 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) private IEnumerable<string> GetMarkers(string path, bool recursive = false)
{ {
return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); 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); Logger.LogInformation("Setting cache path: {Path}", cachePath);
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath; ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache"); CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
BaseApplicationPaths.CreateCacheDirTag(cachePath);
} }
/// <summary> /// <summary>

View File

@@ -166,6 +166,8 @@ namespace Emby.Server.Implementations
ConfigurationManager.Configuration, ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath, ApplicationPaths.PluginsPath,
ApplicationVersion); ApplicationVersion);
_disposableParts.Add(_pluginManager);
} }
/// <summary> /// <summary>
@@ -505,13 +507,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<BaseItemRepository>(); serviceCollection.AddSingleton<IItemRepository, 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<IPeopleRepository, PeopleRepository>(); serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>(); serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>(); serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
@@ -534,7 +530,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IMusicManager, MusicManager>(); serviceCollection.AddSingleton<IMusicManager, MusicManager>();
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>(); serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
serviceCollection.AddSingleton<DotIgnoreIgnoreRule>();
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>(); serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
@@ -646,7 +641,6 @@ namespace Emby.Server.Implementations
BaseItem.ConfigurationManager = ConfigurationManager; BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.FileSystem = Resolve<IFileSystem>(); BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.ItemRepository = Resolve<IItemRepository>(); BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.ItemCountService = Resolve<IItemCountService>();
BaseItem.LibraryManager = Resolve<ILibraryManager>(); BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.Logger = Resolve<ILogger<BaseItem>>(); BaseItem.Logger = Resolve<ILogger<BaseItem>>();
@@ -1012,8 +1006,6 @@ namespace Emby.Server.Implementations
} }
_disposableParts.Clear(); _disposableParts.Clear();
_pluginManager?.Dispose();
} }
_disposed = true; _disposed = true;

View File

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

View File

@@ -272,7 +272,7 @@ namespace Emby.Server.Implementations.Collections
{ {
var childItem = _libraryManager.GetItemById(guidId); 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) 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? // 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) if (item is Video video)
{ {
foreach (var childId in _libraryManager.GetLocalAlternateVersionIds(video)) foreach (var childId in video.GetLocalAlternateVersionIds())
{ {
if (!results.ContainsKey(childId)) if (!results.ContainsKey(childId))
{ {

View File

@@ -39,24 +39,22 @@ namespace Emby.Server.Implementations.Cryptography
{ {
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal)) if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
{ {
var iterations = GetIterationsParameter(hash);
return hash.Hash.SequenceEqual( return hash.Hash.SequenceEqual(
Rfc2898DeriveBytes.Pbkdf2( Rfc2898DeriveBytes.Pbkdf2(
password, password,
hash.Salt, hash.Salt,
iterations, int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
HashAlgorithmName.SHA1, HashAlgorithmName.SHA1,
32)); 32));
} }
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal)) if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
{ {
var iterations = GetIterationsParameter(hash);
return hash.Hash.SequenceEqual( return hash.Hash.SequenceEqual(
Rfc2898DeriveBytes.Pbkdf2( Rfc2898DeriveBytes.Pbkdf2(
password, password,
hash.Salt, hash.Salt,
iterations, int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
HashAlgorithmName.SHA512, HashAlgorithmName.SHA512,
DefaultOutputLength)); DefaultOutputLength));
} }
@@ -64,27 +62,6 @@ namespace Emby.Server.Implementations.Cryptography
throw new NotSupportedException($"Can't verify hash with id: {hash.Id}"); throw new NotSupportedException($"Can't verify hash with id: {hash.Id}");
} }
/// <summary>
/// Extracts and validates the iterations parameter from a password hash.
/// </summary>
/// <param name="hash">The password hash containing parameters.</param>
/// <returns>The number of iterations.</returns>
/// <exception cref="FormatException">Thrown when iterations parameter is missing or invalid.</exception>
private static int GetIterationsParameter(PasswordHash hash)
{
if (!hash.Parameters.TryGetValue("iterations", out var iterationsStr))
{
throw new FormatException($"Password hash with id '{hash.Id}' is missing required 'iterations' parameter.");
}
if (!int.TryParse(iterationsStr, CultureInfo.InvariantCulture, out var iterations))
{
throw new FormatException($"Password hash with id '{hash.Id}' has invalid 'iterations' parameter: '{iterationsStr}'.");
}
return iterations;
}
/// <inheritdoc /> /// <inheritdoc />
public byte[] GenerateSalt() public byte[] GenerateSalt()
=> GenerateSalt(DefaultSaltLength); => GenerateSalt(DefaultSaltLength);

View File

@@ -5,12 +5,10 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO; using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -37,11 +35,7 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{ {
var deadItemsProgress = new Progress<double>(val => progress.Report(val * 0.8)); await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
await CleanDeadItems(cancellationToken, deadItemsProgress).ConfigureAwait(false);
var playlistProgress = new Progress<double>(val => progress.Report(80 + (val * 0.2)));
await CleanOrphanedFilePlaylistsAsync(cancellationToken, playlistProgress).ConfigureAwait(false);
} }
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress) private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
@@ -122,32 +116,4 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
progress.Report(100); 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; private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos( public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null)
IReadOnlyList<BaseItem> items,
DtoOptions options,
User? user = null,
BaseItem? owner = null,
bool skipVisibilityCheck = false)
{ {
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]; var returnItems = new BaseItemDto[accessibleItems.Count];
List<(BaseItem, BaseItemDto)>? programTuples = null; List<(BaseItem, BaseItemDto)>? programTuples = null;
List<(BaseItemDto, LiveTvChannel)>? channelTuples = 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++) for (int index = 0; index < accessibleItems.Count; index++)
{ {
var item = accessibleItems[index]; var item = accessibleItems[index];
var dto = GetBaseItemDtoInternal( var dto = GetBaseItemDtoInternal(item, options, user, owner);
item,
options,
user,
owner,
userDataBatch?.GetValueOrDefault(item.Id),
allCollectionFolders,
childCountBatch,
playedCountBatch,
artistsBatch);
if (item is LiveTvChannel tvChannel) 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) 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) if (item is LiveTvChannel tvChannel)
{ {
LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user); LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
@@ -300,16 +215,7 @@ namespace Emby.Server.Implementations.Dto
return dto; return dto;
} }
private BaseItemDto GetBaseItemDtoInternal( private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
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)
{ {
var dto = new BaseItemDto var dto = new BaseItemDto
{ {
@@ -346,14 +252,7 @@ namespace Emby.Server.Implementations.Dto
if (user is not null) if (user is not null)
{ {
AttachUserSpecificInfo( AttachUserSpecificInfo(dto, item, user, options);
dto,
item,
user,
options,
userData,
childCountBatch,
playedCountBatch);
} }
if (item is IHasMediaSources if (item is IHasMediaSources
@@ -369,15 +268,13 @@ namespace Emby.Server.Implementations.Dto
AttachStudios(dto, item); AttachStudios(dto, item);
} }
AttachBasicFields(dto, item, owner, options, artistsBatch); AttachBasicFields(dto, item, owner, options);
if (options.ContainsField(ItemFields.CanDelete)) if (options.ContainsField(ItemFields.CanDelete))
{ {
dto.CanDelete = user is null dto.CanDelete = user is null
? item.CanDelete() ? item.CanDelete()
: allCollectionFolders is not null : item.CanDelete(user);
? item.CanDelete(user, allCollectionFolders)
: item.CanDelete(user);
} }
if (options.ContainsField(ItemFields.CanDownload)) if (options.ContainsField(ItemFields.CanDownload))
@@ -481,7 +378,37 @@ namespace Emby.Server.Implementations.Dto
return; 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.AlbumCount = counts.AlbumCount;
dto.ArtistCount = counts.ArtistCount; dto.ArtistCount = counts.ArtistCount;
@@ -531,14 +458,7 @@ namespace Emby.Server.Implementations.Dto
/// <summary> /// <summary>
/// Attaches the user specific info. /// Attaches the user specific info.
/// </summary> /// </summary>
private void AttachUserSpecificInfo( private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options)
BaseItemDto dto,
BaseItem item,
User user,
DtoOptions options,
UserItemData? userData = null,
Dictionary<Guid, int>? childCountBatch = null,
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
{ {
if (item.IsFolder) if (item.IsFolder)
{ {
@@ -546,19 +466,7 @@ namespace Emby.Server.Implementations.Dto
if (options.EnableUserData) if (options.EnableUserData)
{ {
if (userData is not null) dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
{
// 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);
}
} }
if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library) if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
@@ -577,7 +485,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.ChildCount)) 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 (options.EnableUserData)
{ {
if (userData is not null) dto.UserData = _userDataRepository.GetUserDataDto(item, user);
{
// 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);
}
} }
} }
@@ -615,25 +513,7 @@ namespace Emby.Server.Implementations.Dto
} }
} }
private static UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId) private static int GetChildCount(Folder folder, User user)
{
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)
{ {
// Right now this is too slow to calculate for top level folders on a per-user basis // 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 // 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); 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); return folder.GetChildCount(user);
} }
@@ -942,8 +815,7 @@ namespace Emby.Server.Implementations.Dto
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <param name="owner">The owner.</param> /// <param name="owner">The owner.</param>
/// <param name="options">The options.</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)
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
{ {
if (options.ContainsField(ItemFields.DateCreated)) if (options.ContainsField(ItemFields.DateCreated))
{ {
@@ -1067,8 +939,6 @@ namespace Emby.Server.Implementations.Dto
dto.OriginalTitle = item.OriginalTitle; dto.OriginalTitle = item.OriginalTitle;
} }
dto.OriginalLanguage = item.OriginalLanguage;
if (options.ContainsField(ItemFields.ParentId)) if (options.ContainsField(ItemFields.ParentId))
{ {
dto.ParentId = item.DisplayParentId; dto.ParentId = item.DisplayParentId;
@@ -1149,15 +1019,6 @@ namespace Emby.Server.Implementations.Dto
{ {
dto.AlbumId = albumParent.Id; dto.AlbumId = albumParent.Id;
dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary); 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)) // if (options.ContainsField(ItemFields.MediaSourceCount))
@@ -1190,17 +1051,16 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor // 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 foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
var artistsLookup = artistsBatch dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))])
?? _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]); .Where(e => e.Value.Length > 0)
.Select(i =>
dto.ArtistItems = hasArtist.Artists {
.Where(name => !string.IsNullOrWhiteSpace(name)) return new NameGuidPair
.Distinct() {
.Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0 Name = i.Key,
? new NameGuidPair { Name = name, Id = artists[0].Id } Id = i.Value.First().Id
: null) };
.Where(item => item is not null) }).Where(i => i is not null).ToArray();
.ToArray();
} }
if (item is IHasAlbumArtist hasAlbumArtist) if (item is IHasAlbumArtist hasAlbumArtist)
@@ -1225,17 +1085,31 @@ namespace Emby.Server.Implementations.Dto
// }) // })
// .ToList(); // .ToList();
var albumArtistsLookup = artistsBatch
?? _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
dto.AlbumArtists = hasAlbumArtist.AlbumArtists dto.AlbumArtists = hasAlbumArtist.AlbumArtists
.Where(name => !string.IsNullOrWhiteSpace(name)) // .Except(foundArtists, new DistinctNameComparer())
.Distinct() .Select(i =>
.Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0 {
? new NameGuidPair { Name = name, Id = albumArtists[0].Id } // This should not be necessary but we're seeing some cases of it
: null) if (string.IsNullOrEmpty(i))
.Where(item => item is not null) {
.ToArray(); return null;
}
var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
{
EnableImages = false
});
if (artist is not null)
{
return new NameGuidPair
{
Name = artist.Name,
Id = artist.Id
};
}
return null;
}).Where(i => i is not null).ToArray();
} }
// Add video info // Add video info
@@ -1264,6 +1138,11 @@ namespace Emby.Server.Implementations.Dto
} }
} }
if (options.ContainsField(ItemFields.Chapters))
{
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
}
if (options.ContainsField(ItemFields.Trickplay)) if (options.ContainsField(ItemFields.Trickplay))
{ {
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
@@ -1277,11 +1156,6 @@ namespace Emby.Server.Implementations.Dto
dto.ExtraType = video.ExtraType; dto.ExtraType = video.ExtraType;
} }
if (options.ContainsField(ItemFields.Chapters))
{
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
}
if (options.ContainsField(ItemFields.MediaStreams)) if (options.ContainsField(ItemFields.MediaStreams))
{ {
// Add VideoInfo // Add VideoInfo

View File

@@ -27,6 +27,7 @@
<PackageReference Include="Microsoft.Data.Sqlite" /> <PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="prometheus-net.DotNetRuntime" /> <PackageReference Include="prometheus-net.DotNetRuntime" />
@@ -38,7 +39,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>

View File

@@ -21,7 +21,6 @@ namespace Emby.Server.Implementations.IO
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
/// <summary> /// <summary>
/// The file system watchers. /// The file system watchers.
@@ -48,23 +47,19 @@ namespace Emby.Server.Implementations.IO
/// <param name="configurationManager">The configuration manager.</param> /// <param name="configurationManager">The configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param> /// <param name="fileSystem">The filesystem.</param>
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param> /// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
public LibraryMonitor( public LibraryMonitor(
ILogger<LibraryMonitor> logger, ILogger<LibraryMonitor> logger,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IServerConfigurationManager configurationManager, IServerConfigurationManager configurationManager,
IFileSystem fileSystem, IFileSystem fileSystem,
IHostApplicationLifetime appLifetime, IHostApplicationLifetime appLifetime)
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_logger = logger; _logger = logger;
_configurationManager = configurationManager; _configurationManager = configurationManager;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
appLifetime.ApplicationStarted.Register(Start); appLifetime.ApplicationStarted.Register(Start);
appLifetime.ApplicationStopping.Register(Stop);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -357,12 +352,6 @@ namespace Emby.Server.Implementations.IO
return; return;
} }
var fileInfo = _fileSystem.GetFileSystemInfo(path);
if (_dotIgnoreIgnoreRule.ShouldIgnore(fileInfo, null))
{
return;
}
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too // Ignore certain files, If the parent of an ignored path has a change event, ignore that too
foreach (var i in _tempIgnoredPaths.Keys) foreach (var i in _tempIgnoredPaths.Keys)
{ {

View File

@@ -497,17 +497,8 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc /> /// <inheritdoc />
public virtual bool AreEqual(string path1, string path2) public virtual bool AreEqual(string path1, string path2)
{ {
if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2)) return Path.TrimEndingDirectorySeparator(path1).Equals(
{ Path.TrimEndingDirectorySeparator(path2),
return false;
}
var normalized1 = Path.TrimEndingDirectorySeparator(path1);
var normalized2 = Path.TrimEndingDirectorySeparator(path2);
return string.Equals(
normalized1,
normalized2,
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
} }
@@ -586,12 +577,6 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc /> /// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) 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); var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and macOS the search pattern is case-sensitive // 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); 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) if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
{ {
return false; return false;
} }
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) if (!HasChangedByDate(item, image))
{ {
return false; return false;
} }
if (!HasChangedByDate(item, image))
{
return false;
} }
return true; return true;

View File

@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Images
includeItemTypes = new[] { BaseItemKind.Series }; includeItemTypes = new[] { BaseItemKind.Series };
break; break;
case CollectionType.music: case CollectionType.music:
includeItemTypes = new[] { BaseItemKind.MusicArtist }; // Music albums usually don't have dedicated backdrops, so use artist instead includeItemTypes = new[] { BaseItemKind.MusicAlbum };
break; break;
case CollectionType.musicvideos: case CollectionType.musicvideos:
includeItemTypes = new[] { BaseItemKind.MusicVideo }; includeItemTypes = new[] { BaseItemKind.MusicVideo };
@@ -98,11 +98,5 @@ namespace Emby.Server.Implementations.Images
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex); return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
} }
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
{
var age = DateTime.UtcNow - image.DateModified;
return age.TotalDays > 7;
}
} }
} }

View File

@@ -1,8 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.RegularExpressions;
using BitFaster.Caching.Lru;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO; using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
@@ -17,36 +14,22 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
{ {
private static readonly bool IsWindows = OperatingSystem.IsWindows(); private static readonly bool IsWindows = OperatingSystem.IsWindows();
private readonly FastConcurrentLru<string, IgnoreFileCacheEntry> _directoryCache; private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
private readonly FastConcurrentLru<string, ParsedIgnoreCacheEntry> _rulesCache;
/// <summary>
/// Initializes a new instance of the <see cref="DotIgnoreIgnoreRule"/> class.
/// </summary>
public DotIgnoreIgnoreRule()
{ {
var cacheSize = Math.Max(100, Environment.ProcessorCount * 100); for (var current = directory; current is not null; current = current.Parent)
_directoryCache = new FastConcurrentLru<string, IgnoreFileCacheEntry>( {
Environment.ProcessorCount, var ignorePath = Path.Join(current.FullName, ".ignore");
cacheSize, if (File.Exists(ignorePath))
StringComparer.Ordinal); {
_rulesCache = new FastConcurrentLru<string, ParsedIgnoreCacheEntry>( return new FileInfo(ignorePath);
Environment.ProcessorCount, }
Math.Max(32, cacheSize / 4), }
StringComparer.Ordinal);
return null;
} }
/// <inheritdoc /> /// <inheritdoc />
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnoredInternal(fileInfo, parent); public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(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();
}
/// <summary> /// <summary>
/// Checks whether or not the file is ignored. /// Checks whether or not the file is ignored.
@@ -54,86 +37,45 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
/// <param name="fileInfo">The file information.</param> /// <param name="fileInfo">The file information.</param>
/// <param name="parent">The parent BaseItem.</param> /// <param name="parent">The parent BaseItem.</param>
/// <returns>True if the file should be ignored.</returns> /// <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 var searchDirectory = fileInfo.IsDirectory
? fileInfo.FullName ? new DirectoryInfo(fileInfo.FullName)
: Path.GetDirectoryName(fileInfo.FullName); : new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
if (string.IsNullOrEmpty(searchDirectory)) if (string.IsNullOrEmpty(searchDirectory.FullName))
{ {
return false; return false;
} }
var ignoreFile = FindIgnoreFileCached(searchDirectory); var ignoreFile = FindIgnoreFile(searchDirectory);
if (ignoreFile is null) if (ignoreFile is null)
{ {
return false; return false;
} }
var parsedEntry = GetParsedRules(ignoreFile); // Fast path in case the ignore files isn't a symlink and is empty
if (parsedEntry is null) if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
{
// 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)
{ {
// Ignore directory if we just have the file
return true; 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);
} }
/// <summary> private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
/// Checks whether a path should be ignored based on an array of ignore rules.
/// </summary>
/// <param name="path">The path to check.</param>
/// <param name="rules">The array of ignore rules.</param>
/// <param name="isDirectory">Whether the path is a directory.</param>
/// <returns>True if the path should be ignored.</returns>
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory)
=> CheckIgnoreRules(path, rules, isDirectory, IsWindows);
/// <summary>
/// Checks whether a path should be ignored based on an array of ignore rules.
/// </summary>
/// <param name="path">The path to check.</param>
/// <param name="rules">The array of ignore rules.</param>
/// <param name="isDirectory">Whether the path is a directory.</param>
/// <param name="normalizePath">Whether to normalize backslashes to forward slashes (for Windows paths).</param>
/// <returns>True if the path should be ignored.</returns>
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath)
{ {
// If file has content, base ignoring off the content .gitignore-style rules
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var ignore = new Ignore.Ignore(); var ignore = new Ignore.Ignore();
ignore.Add(rules);
// Add each rule individually to catch and skip invalid patterns // Mitigate the problem of the Ignore library not handling Windows paths correctly.
var validRulesAdded = 0; // See https://github.com/jellyfin/jellyfin/issues/15484
foreach (var rule in rules) var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
{
try
{
ignore.Add(rule);
validRulesAdded++;
}
catch (RegexParseException)
{
// Ignore invalid patterns
}
}
// If no valid rules were added, fall back to ignoring everything (like an empty .ignore file)
if (validRulesAdded == 0)
{
return true;
}
// 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/" // Add trailing slash for directories to match "folder/"
if (isDirectory) if (isDirectory)
@@ -144,196 +86,11 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
return ignore.IsIgnored(pathToCheck); 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 ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
if (_directoryCache.TryGet(directory, out var cached)) return ignoreFile.Exists
{ ? File.ReadAllText(ignoreFile.FullName)
return cached.IgnoreFileDirectory is null : string.Empty;
? 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; }
} }
} }

View File

@@ -31,20 +31,6 @@ namespace Emby.Server.Implementations.Library
"**/*.sample.?????", "**/*.sample.?????",
"**/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 // Directories
"**/metadata/**", "**/metadata/**",
"**/metadata", "**/metadata",
@@ -64,10 +50,6 @@ namespace Emby.Server.Implementations.Library
"**/lost+found", "**/lost+found",
"**/subs/**", "**/subs/**",
"**/subs", "**/subs",
"**/.snapshots/**",
"**/.snapshots",
"**/.snapshot/**",
"**/.snapshot",
// Trickplay files // Trickplay files
"**/*.trickplay", "**/*.trickplay",

View File

@@ -30,17 +30,18 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.IO; using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@@ -76,17 +77,12 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository; 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 IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions; private readonly NamingOptions _namingOptions;
private readonly IPeopleRepository _peopleRepository; private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver; private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager; private readonly IPathManager _pathManager;
private readonly FastConcurrentLru<Guid, BaseItem> _cache; private readonly FastConcurrentLru<Guid, BaseItem> _cache;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
/// <summary> /// <summary>
/// The _root folder sync lock. /// The _root folder sync lock.
@@ -119,16 +115,11 @@ namespace Emby.Server.Implementations.Library
/// <param name="userViewManagerFactory">The user view manager.</param> /// <param name="userViewManagerFactory">The user view manager.</param>
/// <param name="mediaEncoder">The media encoder.</param> /// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</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="imageProcessor">The image processor.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param> /// <param name="directoryService">The directory service.</param>
/// <param name="peopleRepository">The people repository.</param> /// <param name="peopleRepository">The people repository.</param>
/// <param name="pathManager">The path manager.</param> /// <param name="pathManager">The path manager.</param>
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
public LibraryManager( public LibraryManager(
IServerApplicationHost appHost, IServerApplicationHost appHost,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
@@ -142,16 +133,11 @@ namespace Emby.Server.Implementations.Library
Lazy<IUserViewManager> userViewManagerFactory, Lazy<IUserViewManager> userViewManagerFactory,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IItemRepository itemRepository, IItemRepository itemRepository,
IItemPersistenceService persistenceService,
INextUpService nextUpService,
IItemCountService countService,
ILinkedChildrenService linkedChildrenService,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
NamingOptions namingOptions, NamingOptions namingOptions,
IDirectoryService directoryService, IDirectoryService directoryService,
IPeopleRepository peopleRepository, IPeopleRepository peopleRepository,
IPathManager pathManager, IPathManager pathManager)
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
{ {
_appHost = appHost; _appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>(); _logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -165,10 +151,6 @@ namespace Emby.Server.Implementations.Library
_userViewManagerFactory = userViewManagerFactory; _userViewManagerFactory = userViewManagerFactory;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_itemRepository = itemRepository; _itemRepository = itemRepository;
_persistenceService = persistenceService;
_nextUpService = nextUpService;
_countService = countService;
_linkedChildrenService = linkedChildrenService;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize); _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
@@ -176,7 +158,6 @@ namespace Emby.Server.Implementations.Library
_namingOptions = namingOptions; _namingOptions = namingOptions;
_peopleRepository = peopleRepository; _peopleRepository = peopleRepository;
_pathManager = pathManager; _pathManager = pathManager;
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService); _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated; _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -346,17 +327,9 @@ namespace Emby.Server.Implementations.Library
DeleteItem(item, options, parent, notifyParentItem); DeleteItem(item, options, parent, notifyParentItem);
} }
public void DeleteItemsUnsafeFast(IReadOnlyCollection<BaseItem> items, bool deleteSourceFiles = false) public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items)
{ {
if (items.Count == 0) var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray();
{
return;
}
var pathMaps = items.Select(e =>
(Item: e,
InternalPath: GetInternalMetadataPaths(e),
DeletePaths: deleteSourceFiles ? e.GetDeletePaths() : [])).ToArray();
foreach (var (item, internalPaths, pathsToDelete) in pathMaps) foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
{ {
@@ -390,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) public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
@@ -433,99 +406,6 @@ namespace Emby.Server.Implementations.Library
item.Id); 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 var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false) ? ((Folder)item).GetRecursiveChildren(false)
: []; : [];
@@ -570,7 +450,7 @@ namespace Emby.Server.Implementations.Library
item.SetParent(null); 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 _); _cache.TryRemove(item.Id, out _);
foreach (var child in children) foreach (var child in children)
{ {
@@ -696,9 +576,6 @@ namespace Emby.Server.Implementations.Library
// Trickplay // Trickplay
list.Add(_pathManager.GetTrickplayDirectory(video)); list.Add(_pathManager.GetTrickplayDirectory(video));
// Chapter Images
list.Add(_pathManager.GetChapterImageFolderPath(video));
// Subtitles and attachments // Subtitles and attachments
foreach (var mediaSource in item.GetMediaSources(false)) foreach (var mediaSource in item.GetMediaSources(false))
{ {
@@ -780,63 +657,8 @@ namespace Emby.Server.Implementations.Library
return key.GetMD5(); return key.GetMD5();
} }
public BaseItem? ResolvePath( public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null)
FileSystemMetadata fileInfo, => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
Folder? parent = null,
IDirectoryService? directoryService = null,
CollectionType? collectionType = null)
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType);
/// <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;
}
private BaseItem? ResolvePath( private BaseItem? ResolvePath(
FileSystemMetadata fileInfo, FileSystemMetadata fileInfo,
@@ -1219,7 +1041,7 @@ namespace Emby.Server.Implementations.Library
public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names) public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
{ {
return _linkedChildrenService.FindArtists(names); return _itemRepository.FindArtists(names);
} }
public MusicArtist GetArtist(string name, DtoOptions options) public MusicArtist GetArtist(string name, DtoOptions options)
@@ -1236,7 +1058,6 @@ namespace Emby.Server.Implementations.Library
{ {
IncludeItemTypes = [BaseItemKind.MusicArtist], IncludeItemTypes = [BaseItemKind.MusicArtist],
Name = name, Name = name,
UseRawName = true,
DtoOptions = options DtoOptions = options
}).Cast<MusicArtist>() }).Cast<MusicArtist>()
.OrderBy(i => i.IsAccessedByName ? 1 : 0) .OrderBy(i => i.IsAccessedByName ? 1 : 0)
@@ -1309,7 +1130,6 @@ namespace Emby.Server.Implementations.Library
public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken) public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
{ {
IsScanRunning = true; IsScanRunning = true;
ClearIgnoreRuleCache();
LibraryMonitor.Stop(); LibraryMonitor.Stop();
try try
@@ -1318,7 +1138,6 @@ namespace Emby.Server.Implementations.Library
} }
finally finally
{ {
ClearIgnoreRuleCache();
LibraryMonitor.Start(); LibraryMonitor.Start();
IsScanRunning = false; IsScanRunning = false;
} }
@@ -1326,7 +1145,6 @@ namespace Emby.Server.Implementations.Library
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false) public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
{ {
ClearIgnoreRuleCache();
RootFolder.Children = null; RootFolder.Children = null;
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
@@ -1367,16 +1185,8 @@ namespace Emby.Server.Implementations.Library
if (toDelete.Count > 0) 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) private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
@@ -1451,7 +1261,7 @@ namespace Emby.Server.Implementations.Library
progress.Report(percent * 100); progress.Report(percent * 100);
} }
_persistenceService.UpdateInheritedValues(); _itemRepository.UpdateInheritedValues();
progress.Report(100); progress.Report(100);
} }
@@ -1610,7 +1420,14 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User, allowExternalContent); 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) public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
@@ -1634,7 +1451,7 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User); AddUserToQuery(query, query.User);
} }
return _countService.GetCount(query); return _itemRepository.GetCount(query);
} }
public ItemCounts GetItemCounts(InternalItemsQuery query) public ItemCounts GetItemCounts(InternalItemsQuery query)
@@ -1653,30 +1470,7 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User); AddUserToQuery(query, query.User);
} }
return _countService.GetItemCounts(query); return _itemRepository.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);
} }
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents) public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
@@ -1721,17 +1515,7 @@ namespace Emby.Server.Implementations.Library
} }
} }
return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff); return _itemRepository.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);
} }
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query) public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
@@ -1898,25 +1682,6 @@ namespace Emby.Server.Implementations.Library
query.TopParentIds = [Guid.NewGuid()]; 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 else
{ {
// We need to be able to query from any arbitrary ancestor up the tree // We need to be able to query from any arbitrary ancestor up the tree
@@ -1934,11 +1699,6 @@ namespace Emby.Server.Implementations.Library
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true) private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
{ {
if (query.User is null)
{
query.SetUser(user);
}
if (query.AncestorIds.Length == 0 && if (query.AncestorIds.Length == 0 &&
query.ParentId.IsEmpty() && query.ParentId.IsEmpty() &&
query.ChannelIds.Count == 0 && query.ChannelIds.Count == 0 &&
@@ -1964,15 +1724,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) private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
{ {
if (item is UserView view) if (item is UserView view)
@@ -2137,44 +1888,6 @@ namespace Emby.Server.Implementations.Library
return video; 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 /> /// <inheritdoc />
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder) public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
{ {
@@ -2279,44 +1992,9 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc /> /// <inheritdoc />
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken) public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
{ {
// Resolve and add any local alternate version items that don't exist yet _itemRepository.SaveItems(items, cancellationToken);
// 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;
foreach (var item in items) 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);
allItems.Add(altVideo);
}
}
}
}
}
_persistenceService.SaveItems(allItems, cancellationToken);
foreach (var item in allItems)
{ {
RegisterItem(item); RegisterItem(item);
} }
@@ -2465,7 +2143,7 @@ namespace Emby.Server.Implementations.Library
item.ValidateImages(); item.ValidateImages();
await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false); _itemRepository.SaveImages(item);
RegisterItem(item); RegisterItem(item);
} }
@@ -2482,50 +2160,7 @@ namespace Emby.Server.Implementations.Library
item.DateLastSaved = DateTime.UtcNow; item.DateLastSaved = DateTime.UtcNow;
} }
// Resolve and add any local alternate version items that don't exist yet _itemRepository.SaveItems(items, cancellationToken);
// 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);
allItems.Add(altVideo);
}
}
}
}
}
_persistenceService.SaveItems(allItems, cancellationToken);
foreach (var item in allItems)
{
if (!items.Contains(item))
{
RegisterItem(item);
}
}
if (parent is Folder folder) if (parent is Folder folder)
{ {
@@ -2566,12 +2201,6 @@ namespace Emby.Server.Implementations.Library
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken); => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
/// <inheritdoc />
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
{
await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
}
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{ {
if (item.IsFileProtocol) if (item.IsFileProtocol)
@@ -2653,7 +2282,7 @@ namespace Emby.Server.Implementations.Library
if (item is null) if (item is null)
{ {
return []; return new List<Folder>();
} }
return GetCollectionFoldersInternal(item, allUserRootChildren); return GetCollectionFoldersInternal(item, allUserRootChildren);
@@ -3197,9 +2826,8 @@ namespace Emby.Server.Implementations.Library
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{ {
// Apply .ignore rules // Apply .ignore rules
var filtered = fileSystemChildren.Where(c => !_dotIgnoreIgnoreRule.ShouldIgnore(c, owner)).ToList(); var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(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, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
if (ownerVideoInfo is null) if (ownerVideoInfo is null)
{ {
yield break; yield break;
@@ -3261,16 +2889,10 @@ namespace Emby.Server.Implementations.Library
extra.ExtraType = extraType; extra.ExtraType = extraType;
} }
// Only return items that are actual extras (have ExtraType set) extra.ParentId = Guid.Empty;
// Note: OwnerId and ParentId are set by RefreshExtras, not here, extra.OwnerId = owner.Id;
// so that RefreshExtras can detect when they need updating and set ForceSave. extra.IsInMixedFolder = isInMixedFolder;
if (extra.ExtraType is not null) return extra;
{
extra.IsInMixedFolder = isInMixedFolder;
return extra;
}
return null;
} }
} }
@@ -3289,7 +2911,7 @@ namespace Emby.Server.Implementations.Library
public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query) public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
{ {
return _peopleRepository.GetPeople(query).Items; return _peopleRepository.GetPeople(query);
} }
public IReadOnlyList<PersonInfo> GetPeople(BaseItem item) public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
@@ -3310,33 +2932,24 @@ namespace Emby.Server.Implementations.Library
return []; return [];
} }
public QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query) public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
{ {
var queryResult = _peopleRepository.GetPeople(query); return _peopleRepository.GetPeopleNames(query)
var baseItems = queryResult.Items.Select(i => .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>
{ {
StartIndex = queryResult.StartIndex, try
TotalRecordCount = queryResult.TotalRecordCount, {
Items = baseItems, 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) public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
@@ -3581,7 +3194,19 @@ namespace Emby.Server.Implementations.Library
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
CreateShortcut(virtualFolderPath, pathInfo); var shortcutFilename = Path.GetFileNameWithoutExtension(path);
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
while (File.Exists(lnk))
{
shortcutFilename += "1";
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
}
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
RemoveContentTypeOverrides(path);
if (saveLibraryOptions) if (saveLibraryOptions)
{ {
@@ -3746,59 +3371,5 @@ namespace Emby.Server.Implementations.Library
return item is UserRootFolder || item.IsVisibleStandalone(user); return item is UserRootFolder || item.IsVisibleStandalone(user);
} }
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
{
var path = pathInfo.Path;
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
while (File.Exists(lnk))
{
shortcutFilename += "1";
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
}
_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);
}
} }
} }

View File

@@ -23,7 +23,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
@@ -424,7 +423,7 @@ namespace Emby.Server.Implementations.Library
MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLanguage); 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) 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)) var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
{
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);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
if (user.PlayDefaultAudioTrack) if (user.PlayDefaultAudioTrack)
@@ -498,19 +462,7 @@ namespace Emby.Server.Implementations.Library
var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections; var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections;
var originalLanguage = item?.OriginalLanguage ?? item switch SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
{
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);
SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection); SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
} }
else if (mediaType == MediaType.Audio) else if (mediaType == MediaType.Audio)

View File

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

View File

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

View File

@@ -5,12 +5,12 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Emby.Naming.Book;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers.Books namespace Emby.Server.Implementations.Library.Resolvers.Books
{ {
@@ -35,22 +35,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var extension = Path.GetExtension(args.Path.AsSpan()); var extension = Path.GetExtension(args.Path.AsSpan());
if (!_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{ {
return null; // It's a book
return new Book
{
Path = args.Path,
IsInMixedFolder = true
};
} }
var result = BookFileNameParser.Parse(Path.GetFileNameWithoutExtension(args.Path)); return null;
return new Book
{
Path = args.Path,
Name = result.Name ?? string.Empty,
IndexNumber = result.Index,
ProductionYear = result.Year,
SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)),
IsInMixedFolder = true,
};
} }
private Book GetBook(ItemResolveArgs args) private Book GetBook(ItemResolveArgs args)
@@ -64,22 +59,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
StringComparison.OrdinalIgnoreCase); StringComparison.OrdinalIgnoreCase);
}).ToList(); }).ToList();
// directory is only considered a book when it contains exactly one supported file // Don't return a Book if there is more (or less) than one document in the directory
// other library structures with multiple books to a directory will get picked up as individual files
if (bookFiles.Count != 1) if (bookFiles.Count != 1)
{ {
return null; return null;
} }
var result = BookFileNameParser.Parse(Path.GetFileName(args.Path));
return new Book return new Book
{ {
Path = bookFiles[0].FullName, Path = bookFiles[0].FullName
Name = result.Name ?? string.Empty,
IndexNumber = result.Index,
ProductionYear = result.Year,
SeriesName = result.SeriesName ?? string.Empty,
}; };
} }
} }

View File

@@ -1,10 +1,8 @@
#nullable disable #nullable disable
using System; using System;
using System.IO;
using System.Linq; using System.Linq;
using Emby.Naming.Common; using Emby.Naming.Common;
using Emby.Server.Implementations.Library;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@@ -83,34 +81,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
episode.ParentIndexNumber = 1; episode.ParentIndexNumber = 1;
} }
SetProviderIdFromPath(episode, args.Path);
return episode; return episode;
} }
return null; 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 #nullable disable
using System;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq;
using Emby.Naming.Common; using Emby.Naming.Common;
using Emby.Naming.TV; using Emby.Naming.TV;
using Emby.Server.Implementations.Library;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -82,14 +77,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null; 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)) if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name))
@@ -104,31 +91,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
args.LibraryOptions.PreferredMetadataLanguage); args.LibraryOptions.PreferredMetadataLanguage);
} }
SetProviderIdFromPath(season, path);
return season; return season;
} }
return null; 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

@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library
results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value); results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
} }
if (query.Limit.HasValue && query.Limit.Value > 0) if (query.Limit.HasValue)
{ {
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count)); results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
} }

View File

@@ -177,74 +177,53 @@ namespace Emby.Server.Implementations.Library
}; };
} }
/// <inheritdoc /> private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
public Dictionary<Guid, UserItemData> GetUserDataBatch(IReadOnlyList<BaseItem> items, User user)
{ {
var result = new Dictionary<Guid, UserItemData>(items.Count); var cacheKey = GetCacheKey(user.InternalId, itemId);
var itemsNeedingQuery = new List<(BaseItem Item, List<string> Keys)>();
foreach (var item in items) if (_cache.TryGet(cacheKey, out var data))
{ {
var cacheKey = GetCacheKey(user.InternalId, item.Id); return data;
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));
}
}
} }
if (itemsNeedingQuery.Count == 0) data = GetUserDataInternal(user.Id, itemId, keys);
{
return result;
}
// Build a single query for all missing items if (data is null)
var allItemIds = itemsNeedingQuery.Select(x => x.Item.Id).ToList();
var allKeys = itemsNeedingQuery.SelectMany(x => x.Keys).Distinct().ToList();
if (allKeys.Count > 0)
{ {
using var context = _repository.CreateDbContext(); return new UserItemData()
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)
{ {
UserItemData userData; Key = keys[0],
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);
}
} }
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> /// <summary>

View File

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

View File

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

View File

@@ -74,7 +74,7 @@ public class CollectionPostScanTask : ILibraryPostScanTask
foreach (var m in movies) 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)) if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -128,12 +128,12 @@
"TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling.", "TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling.",
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.", "TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
"TaskAudioNormalization": "Odio Normalisering", "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", "TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
"TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies", "TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
"TaskExtractMediaSegments": "Media Segment Skandeer", "TaskExtractMediaSegments": "Media Segment Skandeer",
"TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.", "TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
"TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging", "TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings.", "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."
} }

View File

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

View File

@@ -3,7 +3,7 @@
"Playlists": "Плэй-лісты", "Playlists": "Плэй-лісты",
"Latest": "Апошняе", "Latest": "Апошняе",
"LabelIpAddressValue": "IP-адрас: {0}", "LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} дададзены ў бібліятэку", "ItemAddedWithName": "{0} даданы ў бібліятэку",
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены", "MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана", "NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны", "PluginInstalledWithName": "{0} быў усталяваны",
@@ -14,9 +14,9 @@
"Channels": "Каналы", "Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}", "ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі", "Collections": "Калекцыі",
"Default": радвызначана", "Default": а змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі", "Folders": "Тэчкі",
"Favorites": "Абранае", "Favorites": "Абранае",
"External": "Знешні", "External": "Знешні",
"Genres": "Жанры", "Genres": "Жанры",
@@ -50,7 +50,7 @@
"User": "Карыстальнік", "User": "Карыстальнік",
"UserDeletedWithName": "Карыстальнік {0} быў выдалены", "UserDeletedWithName": "Карыстальнік {0} быў выдалены",
"UserDownloadingItemWithValues": "{0} спампоўваецца {1}", "UserDownloadingItemWithValues": "{0} спампоўваецца {1}",
"TaskOptimizeDatabase": "Аптымізацыя базы даных", "TaskOptimizeDatabase": "Аптымізаваць базу дадзеных",
"Artists": "Выканаўцы", "Artists": "Выканаўцы",
"UserOfflineFromDevice": "{0} адлучыўся ад {1}", "UserOfflineFromDevice": "{0} адлучыўся ад {1}",
"UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}", "UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
@@ -59,8 +59,8 @@
"TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.", "TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.",
"TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія сканфігураваныя на аўтаматычнае абнаўленне.", "TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія сканфігураваныя на аўтаматычнае абнаўленне.",
"TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.", "TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.",
"TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субцітры на аснове канфігурацыі метаданых.", "TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субцітры на аснове канфігурацыі метададзеных.",
"TaskOptimizeDatabaseDescription": "Сціскае базу даных і вызваляе вольную прастору. Выкананне гэтай задачы пасля сканіравання бібліятэкі або іншых змяненняў, якія мадыфікуюць базу даных, можа палепшыць прадукцыйнасць.", "TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых зменаў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць выдайнасць.",
"TaskKeyframeExtractor": "Экстрактар ключавых кадраў", "TaskKeyframeExtractor": "Экстрактар ключавых кадраў",
"TasksApplicationCategory": "Праграма", "TasksApplicationCategory": "Праграма",
"AppDeviceValues": "Праграма: {0}, Прылада: {1}", "AppDeviceValues": "Праграма: {0}, Прылада: {1}",
@@ -81,8 +81,8 @@
"NotificationOptionInstallationFailed": "Збой усталёўкі", "NotificationOptionInstallationFailed": "Збой усталёўкі",
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.", "NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана", "NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыя спынена", "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена",
"NotificationOptionAudioPlayback": "Прайграванне аўдыя пачалося", "NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося",
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт", "NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
"NotificationOptionPluginError": "Збой плагіна", "NotificationOptionPluginError": "Збой плагіна",
"NotificationOptionPluginUninstalled": "Плагін выдалены", "NotificationOptionPluginUninstalled": "Плагін выдалены",
@@ -95,7 +95,7 @@
"ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску", "ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску",
"Shows": "Шоу", "Shows": "Шоу",
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.", "StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
"SubtitleDownloadFailureFromForItem": "Субцітры для {1} не ўдалося спампаваць з {0}", "SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}",
"TvShows": "Тэлепраграма", "TvShows": "Тэлепраграма",
"Undefined": "Нявызначана", "Undefined": "Нявызначана",
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны", "UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
@@ -104,7 +104,7 @@
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}", "UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}",
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}", "UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку", "ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
"ValueSpecialEpisodeName": "Спецвыпуск - {0}", "ValueSpecialEpisodeName": "Спецэпізод - {0}",
"VersionNumber": "Версія {0}", "VersionNumber": "Версія {0}",
"TasksMaintenanceCategory": "Абслугоўванне", "TasksMaintenanceCategory": "Абслугоўванне",
"TasksLibraryCategory": "Бібліятэка", "TasksLibraryCategory": "Бібліятэка",
@@ -114,7 +114,7 @@
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.", "TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
"TaskRefreshChapterImages": "Вынуць выявы раздзелаў", "TaskRefreshChapterImages": "Вынуць выявы раздзелаў",
"TaskRefreshLibrary": "Сканаваць бібліятэку", "TaskRefreshLibrary": "Сканаваць бібліятэку",
"TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метаданыя.", "TaskRefreshLibraryDescription": "Скануе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
"TaskCleanLogs": "Ачысціць журнал", "TaskCleanLogs": "Ачысціць журнал",
"TaskRefreshPeople": "Абнавіць выканаўцаў", "TaskRefreshPeople": "Абнавіць выканаўцаў",
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.", "TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
@@ -123,9 +123,11 @@
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.", "TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
"TaskRefreshChannels": "Абнавіць каналы", "TaskRefreshChannels": "Абнавіць каналы",
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры", "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа выконвацца доўга.", "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.",
"TaskRefreshTrickplayImages": "Стварыць выявы Trickplay", "TaskRefreshTrickplayImages": "Стварыць выявы Trickplay",
"TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.", "TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.",
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты",
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.",
"TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.", "TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.",
"TaskAudioNormalization": "Нармалізацыя гуку", "TaskAudioNormalization": "Нармалізацыя гуку",
"TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.", "TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.",
@@ -134,6 +136,6 @@
"TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песняў", "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песняў",
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
"CleanupUserDataTask": "Задача па ачыстцы даных карыстальніка", "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён." "CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
} }

View File

@@ -15,7 +15,7 @@
"Favorites": "Любими", "Favorites": "Любими",
"Folders": "Папки", "Folders": "Папки",
"Genres": "Жанрове", "Genres": "Жанрове",
"HeaderAlbumArtists": "Изпълнители на албума", "HeaderAlbumArtists": "Изпълнители на албуми",
"HeaderContinueWatching": "Продължаване на гледането", "HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми", "HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители", "HeaderFavoriteArtists": "Любими изпълнители",
@@ -73,6 +73,7 @@
"Shows": "Сериали", "Shows": "Сериали",
"Songs": "Песни", "Songs": "Песни",
"StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.", "StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.",
"SubtitleDownloadFailureForItem": "Неуспешно изтегляне на субтитри за {0}",
"SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени", "SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени",
"Sync": "Синхронизиране", "Sync": "Синхронизиране",
"System": "Система", "System": "Система",
@@ -128,6 +129,8 @@
"TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки.", "TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки.",
"TaskDownloadMissingLyrics": "Свали липсващи текстове", "TaskDownloadMissingLyrics": "Свали липсващи текстове",
"TaskDownloadMissingLyricsDescription": "Свали текстове за песни", "TaskDownloadMissingLyricsDescription": "Свали текстове за песни",
"TaskCleanCollectionsAndPlaylists": "Изчисти колекциите и плейлистите",
"TaskCleanCollectionsAndPlaylistsDescription": "Премахни несъществуващи файлове в колекциите и плейлистите.",
"TaskAudioNormalization": "Нормализиране на звука", "TaskAudioNormalization": "Нормализиране на звука",
"TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука.", "TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука.",
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.", "TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",

View File

@@ -127,6 +127,8 @@
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি", "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।", "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
"TaskDownloadMissingLyricsDescription": "গানের জন্য লিরিকস ডাউনলোড করুন", "TaskDownloadMissingLyricsDescription": "গানের জন্য লিরিকস ডাউনলোড করুন",
"TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
"TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান", "TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্ট বের করে বা অর্জন করে।", "TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্ট বের করে বা অর্জন করে।",
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন", "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",

View File

@@ -1,139 +0,0 @@
{
"Albums": "Albumi",
"Artists": "Umjetnici",
"Books": "Knjige",
"Channels": "Kanalima",
"Collections": "Zbirke",
"Default": "Zadano",
"Favorites": "Omiljeni",
"Folders": "Mape",
"Genres": "Žanrovi",
"HeaderAlbumArtists": "Umjetnici albuma",
"HeaderContinueWatching": "Nastavi gledati",
"Movies": "Filmovi",
"MusicVideos": "Muzički spotovi",
"Photos": "Slike",
"Playlists": "Plejliste",
"Shows": "Pokazuje",
"Songs": "Pjesme",
"ValueSpecialEpisodeName": "Posebno - {0}",
"AppDeviceValues": "Aplikacija: {0}, Uređaj: {1}",
"Application": "Prijava",
"AuthenticationSucceededWithUserName": "{0} uspješno autentificirano",
"CameraImageUploadedFrom": "Nova slika s kamere je postavljena sa {0}",
"ChapterNameValue": "Poglavlje {0}",
"DeviceOfflineWithName": "{0} se odspojio",
"DeviceOnlineWithName": "{0} je povezan",
"External": "Vanjsko",
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave sa {0}",
"Forced": "Prisilno",
"HeaderFavoriteAlbums": "Omiljeni albumi",
"HeaderFavoriteArtists": "Omiljeni umjetnici",
"HeaderFavoriteEpisodes": "Omiljene epizode",
"HeaderFavoriteShows": "Omiljene emisije",
"HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo",
"HeaderNextUp": "Slijedi",
"HeaderRecordingGroups": "Grupe za snimanje",
"HearingImpaired": "Oštećen sluh",
"HomeVideos": "Kućni videozapisi",
"Inherit": "Nasljedi",
"ItemAddedWithName": "{0} je dodan u biblioteku",
"ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
"LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Trajanje: {0}",
"Latest": "Posljednje dodano",
"MessageApplicationUpdated": "Jellyfin Server je ažuriran",
"MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Sekcija za konfiguraciju servera {0} je ažurirana",
"MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
"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",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} je instaliran",
"PluginUninstalledWithName": "{0} je deinstaliran",
"PluginUpdatedWithName": "{0} je ažurirano",
"ProviderValue": "Pružatelj: {0}",
"ScheduledTaskFailedWithName": "{0} nije uspjelo",
"ScheduledTaskStartedWithName": "{0} počelo",
"ServerNameNeedsToBeRestarted": "{0} treba ponovo pokrenuti",
"StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Molimo pokušajte ponovo za kratko vrijeme.",
"SubtitleDownloadFailureFromForItem": "Podtitlovi nisu uspjeli preuzeti sa {0} za {1}",
"Sync": "Sinkronizacija",
"System": "Sistem",
"TvShows": "TV serije",
"Undefined": "Nedefinirano",
"User": "Korisnik",
"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}",
"UserPolicyUpdatedWithName": "Pravila za korisnike su ažurirana za {0}",
"UserStartedPlayingItemWithValues": "{0} igra protiv {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je završio igru protiv {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je dodan u vašu medijsku biblioteku",
"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

@@ -63,8 +63,8 @@
"Photos": "Fotos", "Photos": "Fotos",
"Playlists": "Llistes de reproducció", "Playlists": "Llistes de reproducció",
"Plugin": "Complement", "Plugin": "Complement",
"PluginInstalledWithName": "S'ha instal·lat {0}", "PluginInstalledWithName": "{0} ha estat instal·lat",
"PluginUninstalledWithName": "S'ha desinstal·lat {0}", "PluginUninstalledWithName": "S'ha instal·lat {0}",
"PluginUpdatedWithName": "S'ha actualitzat {0}", "PluginUpdatedWithName": "S'ha actualitzat {0}",
"ProviderValue": "Proveïdor: {0}", "ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat", "ScheduledTaskFailedWithName": "{0} ha fallat",
@@ -73,6 +73,7 @@
"Shows": "Sèries", "Shows": "Sèries",
"Songs": "Cançons", "Songs": "Cançons",
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.", "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}", "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
"Sync": "Sincronitza", "Sync": "Sincronitza",
"System": "Sistema", "System": "Sistema",
@@ -104,7 +105,7 @@
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.", "TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja dels registres", "TaskCleanLogs": "Neteja dels registres",
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.", "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.", "TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols", "TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.", "TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
@@ -126,6 +127,8 @@
"HearingImpaired": "Discapacitat auditiva", "HearingImpaired": "Discapacitat auditiva",
"TaskRefreshTrickplayImages": "Generació d'imatges de previsualització", "TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
"TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.", "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", "TaskAudioNormalization": "Estabilització de l'àudio",
"TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització 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", "TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",

View File

@@ -73,6 +73,7 @@
"Shows": "Seriály", "Shows": "Seriály",
"Songs": "Skladby", "Songs": "Skladby",
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.", "StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
"SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}",
"SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo", "SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo",
"Sync": "Synchronizace", "Sync": "Synchronizace",
"System": "Systém", "System": "Systém",
@@ -126,6 +127,8 @@
"HearingImpaired": "Sluchově postižení", "HearingImpaired": "Sluchově postižení",
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay", "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.", "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", "TaskAudioNormalization": "Normalizace zvuku",
"TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.", "TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.",
"TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni", "TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni",
@@ -135,6 +138,5 @@
"TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay", "TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay",
"TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.", "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í.", "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", "CleanupUserDataTask": "Pročistit uživatelská data"
"Original": "Originál"
} }

View File

@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}", "CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
"Books": "Llyfrau", "Books": "Llyfrau",
"AuthenticationSucceededWithUserName": "{0} wedii ddilysun llwyddiannus", "AuthenticationSucceededWithUserName": "{0} wedii ddilysun llwyddiannus",
"Artists": "Crewyr", "Artists": "Artistiaid",
"AppDeviceValues": "Ap: {0}, Dyfais: {1}", "AppDeviceValues": "Ap: {0}, Dyfais: {1}",
"Albums": "Albwmau", "Albums": "Albwmau",
"Genres": "Genres", "Genres": "Genres",
@@ -67,7 +67,7 @@
"NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain", "NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain",
"MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru", "MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru",
"MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru", "MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru",
"FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu o {0}", "FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu gan {0}",
"ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau", "ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau",
"UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}", "UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}",
"UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}", "UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}",
@@ -123,12 +123,5 @@
"TaskRefreshChapterImages": "Echdynnu Lluniau Pennod", "TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
"TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.", "TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
"TaskCleanCache": "Gwaghau Ffolder Cache", "TaskCleanCache": "Gwaghau Ffolder Cache",
"HearingImpaired": "Nam ar y clyw", "HearingImpaired": "Nam ar y clyw"
"TaskAudioNormalization": "Gwastatau Sain",
"TaskAudioNormalizationDescription": "Yn sganio ffeiliau am ddata gwastatau sain.",
"TaskRefreshTrickplayImages": "Creuwch lluniau Trickplay",
"TaskRefreshTrickplayImagesDescription": "Creu rhagolygon Trickplay ar gyfer fideos mewn llyfrgelloedd gweithredol.",
"TaskDownloadMissingLyrics": "Lawrlwytho geiriau coll",
"TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon",
"TaskExtractMediaSegments": "Sganio Darnau Cyfryngau"
} }

View File

@@ -73,6 +73,7 @@
"Shows": "Serier", "Shows": "Serier",
"Songs": "Sange", "Songs": "Sange",
"StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.", "StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
"SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}",
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}", "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
"Sync": "Synkroniser", "Sync": "Synkroniser",
"System": "System", "System": "System",
@@ -126,6 +127,8 @@
"HearingImpaired": "Hørehæmmet", "HearingImpaired": "Hørehæmmet",
"TaskRefreshTrickplayImages": "Generer trickplay-billeder", "TaskRefreshTrickplayImages": "Generer trickplay-billeder",
"TaskRefreshTrickplayImagesDescription": "Laver trickplay-billeder for videoer i aktiverede biblioteker.", "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.", "TaskAudioNormalizationDescription": "Skanner filer for data vedrørende lydnormalisering.",
"TaskAudioNormalization": "Lydnormalisering", "TaskAudioNormalization": "Lydnormalisering",
"TaskDownloadMissingLyricsDescription": "Søger på internettet efter manglende sangtekster baseret på metadata-konfigurationen", "TaskDownloadMissingLyricsDescription": "Søger på internettet efter manglende sangtekster baseret på metadata-konfigurationen",

View File

@@ -9,9 +9,9 @@
"Channels": "Kanäle", "Channels": "Kanäle",
"ChapterNameValue": "Kapitel {0}", "ChapterNameValue": "Kapitel {0}",
"Collections": "Sammlungen", "Collections": "Sammlungen",
"DeviceOfflineWithName": "{0} ist offline", "DeviceOfflineWithName": "{0} hat die Verbindung getrennt",
"DeviceOnlineWithName": "{0} ist online", "DeviceOnlineWithName": "{0} ist verbunden",
"FailedLoginAttemptWithUserName": "Anmeldung von {0} fehlgeschlagen", "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
"Favorites": "Favoriten", "Favorites": "Favoriten",
"Folders": "Verzeichnisse", "Folders": "Verzeichnisse",
"Genres": "Genres", "Genres": "Genres",
@@ -19,9 +19,9 @@
"HeaderContinueWatching": "Weiterschauen", "HeaderContinueWatching": "Weiterschauen",
"HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblingsinterpreten", "HeaderFavoriteArtists": "Lieblingsinterpreten",
"HeaderFavoriteEpisodes": "Lieblingsfolgen", "HeaderFavoriteEpisodes": "Lieblingsepisoden",
"HeaderFavoriteShows": "Lieblingsserien", "HeaderFavoriteShows": "Lieblingsserien",
"HeaderFavoriteSongs": "Lieblingssongs", "HeaderFavoriteSongs": "Lieblingslieder",
"HeaderLiveTV": "Live TV", "HeaderLiveTV": "Live TV",
"HeaderNextUp": "Als Nächstes", "HeaderNextUp": "Als Nächstes",
"HeaderRecordingGroups": "Aufnahme-Gruppen", "HeaderRecordingGroups": "Aufnahme-Gruppen",
@@ -46,7 +46,7 @@
"NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.", "NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.",
"NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar", "NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
"NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert", "NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
"NotificationOptionAudioPlayback": "Audio wird abgespielt", "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet",
"NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt", "NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt",
"NotificationOptionCameraImageUploaded": "Foto hochgeladen", "NotificationOptionCameraImageUploaded": "Foto hochgeladen",
"NotificationOptionInstallationFailed": "Installation fehlgeschlagen", "NotificationOptionInstallationFailed": "Installation fehlgeschlagen",
@@ -57,8 +57,8 @@
"NotificationOptionPluginUpdateInstalled": "Pluginaktualisierung installiert", "NotificationOptionPluginUpdateInstalled": "Pluginaktualisierung installiert",
"NotificationOptionServerRestartRequired": "Serverneustart notwendig", "NotificationOptionServerRestartRequired": "Serverneustart notwendig",
"NotificationOptionTaskFailed": "Geplante Aufgabe fehlgeschlagen", "NotificationOptionTaskFailed": "Geplante Aufgabe fehlgeschlagen",
"NotificationOptionUserLockedOut": "Benutzer gesperrt", "NotificationOptionUserLockedOut": "Benutzer ausgeschlossen",
"NotificationOptionVideoPlayback": "Video wird abgespielt", "NotificationOptionVideoPlayback": "Videowiedergabe gestartet",
"NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt", "NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt",
"Photos": "Fotos", "Photos": "Fotos",
"Playlists": "Wiedergabelisten", "Playlists": "Wiedergabelisten",
@@ -73,6 +73,7 @@
"Shows": "Serien", "Shows": "Serien",
"Songs": "Lieder", "Songs": "Lieder",
"StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.", "StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.",
"SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}",
"SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden", "SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden",
"Sync": "Synchronisation", "Sync": "Synchronisation",
"System": "System", "System": "System",
@@ -81,7 +82,7 @@
"UserCreatedWithName": "Benutzer {0} wurde erstellt", "UserCreatedWithName": "Benutzer {0} wurde erstellt",
"UserDeletedWithName": "Benutzer {0} wurde gelöscht", "UserDeletedWithName": "Benutzer {0} wurde gelöscht",
"UserDownloadingItemWithValues": "{0} lädt {1} herunter", "UserDownloadingItemWithValues": "{0} lädt {1} herunter",
"UserLockedOutWithName": "Benutzer {0} wurde gesperrt", "UserLockedOutWithName": "Benutzer {0} wurde ausgeschlossen",
"UserOfflineFromDevice": "{0} wurde getrennt von {1}", "UserOfflineFromDevice": "{0} wurde getrennt von {1}",
"UserOnlineFromDevice": "{0} ist online von {1}", "UserOnlineFromDevice": "{0} ist online von {1}",
"UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert", "UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert",
@@ -96,25 +97,25 @@
"TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.", "TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.",
"TaskRefreshChannels": "Kanäle aktualisieren", "TaskRefreshChannels": "Kanäle aktualisieren",
"TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.", "TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.",
"TaskCleanTranscode": "Transkodierungsverzeichnis leeren", "TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen",
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.", "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
"TaskUpdatePlugins": "Plugins aktualisieren", "TaskUpdatePlugins": "Plugins aktualisieren",
"TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.", "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
"TaskRefreshPeople": "Personen aktualisieren", "TaskRefreshPeople": "Personen aktualisieren",
"TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.", "TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.",
"TaskCleanLogs": "Protokollverzeichnis leeren", "TaskCleanLogs": "Log-Verzeichnis aufräumen",
"TaskRefreshLibraryDescription": "Durchsucht deine Medienbibliothek nach neuen Dateien und aktualisiert Metadaten.", "TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.",
"TaskRefreshLibrary": "Medien-Bibliothek scannen", "TaskRefreshLibrary": "Medien-Bibliothek scannen",
"TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videokapitel.", "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.",
"TaskRefreshChapterImages": "Kapitelvorschauen erstellen", "TaskRefreshChapterImages": "Kapitel-Bilder extrahieren",
"TaskCleanCacheDescription": "Löscht Cache-Dateien, die vom System nicht mehr benötigt werden.", "TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.",
"TaskCleanCache": "Cache-Verzeichnis leeren", "TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen",
"TasksChannelsCategory": "Internet-Kanäle", "TasksChannelsCategory": "Internet-Kanäle",
"TasksApplicationCategory": "Anwendung", "TasksApplicationCategory": "Anwendung",
"TasksLibraryCategory": "Bibliothek", "TasksLibraryCategory": "Bibliothek",
"TasksMaintenanceCategory": "Wartung", "TasksMaintenanceCategory": "Wartung",
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.", "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
"TaskCleanActivityLog": "Aktivitätsverlauf bereinigen", "TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen",
"Undefined": "Undefiniert", "Undefined": "Undefiniert",
"Forced": "Erzwungen", "Forced": "Erzwungen",
"Default": "Standard", "Default": "Standard",
@@ -126,6 +127,8 @@
"HearingImpaired": "Hörgeschädigt", "HearingImpaired": "Hörgeschädigt",
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren", "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
"TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.", "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", "TaskAudioNormalization": "Audio Normalisierung",
"TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.", "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
"TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter", "TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
@@ -135,6 +138,5 @@
"TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren", "TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.", "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.",
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten", "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.", "CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind."
"Original": "Original"
} }

View File

@@ -73,6 +73,7 @@
"Shows": "Σειρές", "Shows": "Σειρές",
"Songs": "Τραγούδια", "Songs": "Τραγούδια",
"StartupEmbyServerIsLoading": "Ο διακομιστής Jellyfin φορτώνει. Περιμένετε λίγο και δοκιμάστε ξανά.", "StartupEmbyServerIsLoading": "Ο διακομιστής Jellyfin φορτώνει. Περιμένετε λίγο και δοκιμάστε ξανά.",
"SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}",
"SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}", "SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}",
"Sync": "Συγχρονισμός", "Sync": "Συγχρονισμός",
"System": "Σύστημα", "System": "Σύστημα",
@@ -128,6 +129,8 @@
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.", "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
"TaskAudioNormalization": "Ομοιομορφία ήχου", "TaskAudioNormalization": "Ομοιομορφία ήχου",
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.", "TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
"TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
"TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον.",
"TaskMoveTrickplayImages": "Αλλαγή τοποθεσίας εικόνων Trickplay", "TaskMoveTrickplayImages": "Αλλαγή τοποθεσίας εικόνων Trickplay",
"TaskDownloadMissingLyrics": "Λήψη στίχων που λείπουν", "TaskDownloadMissingLyrics": "Λήψη στίχων που λείπουν",
"TaskMoveTrickplayImagesDescription": "Μετακινεί τα υπάρχοντα αρχεία trickplay σύμφωνα με τις ρυθμίσεις της βιβλιοθήκης.", "TaskMoveTrickplayImagesDescription": "Μετακινεί τα υπάρχοντα αρχεία trickplay σύμφωνα με τις ρυθμίσεις της βιβλιοθήκης.",

View File

@@ -73,6 +73,7 @@
"Shows": "Shows", "Shows": "Shows",
"Songs": "Songs", "Songs": "Songs",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
"Sync": "Sync", "Sync": "Sync",
"System": "System", "System": "System",
@@ -126,6 +127,8 @@
"HearingImpaired": "Hearing Impaired", "HearingImpaired": "Hearing Impaired",
"TaskRefreshTrickplayImages": "Generate Trickplay Images", "TaskRefreshTrickplayImages": "Generate Trickplay Images",
"TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.", "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", "TaskAudioNormalization": "Audio Normalisation",
"TaskAudioNormalizationDescription": "Scans files for audio normalisation data.", "TaskAudioNormalizationDescription": "Scans files for audio normalisation data.",
"TaskDownloadMissingLyrics": "Download missing lyrics", "TaskDownloadMissingLyrics": "Download missing lyrics",

View File

@@ -64,7 +64,6 @@
"NotificationOptionUserLockedOut": "User locked out", "NotificationOptionUserLockedOut": "User locked out",
"NotificationOptionVideoPlayback": "Video playback started", "NotificationOptionVideoPlayback": "Video playback started",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped", "NotificationOptionVideoPlaybackStopped": "Video playback stopped",
"Original": "Original",
"Photos": "Photos", "Photos": "Photos",
"Playlists": "Playlists", "Playlists": "Playlists",
"Plugin": "Plugin", "Plugin": "Plugin",
@@ -131,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.", "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", "TaskKeyframeExtractor": "Keyframe Extractor",
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", "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", "TaskExtractMediaSegments": "Media Segment Scan",
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location", "TaskMoveTrickplayImages": "Migrate Trickplay Image Location",

View File

@@ -20,7 +20,7 @@
"HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Capítulos favoritos", "HeaderFavoriteEpisodes": "Capítulos favoritos",
"HeaderFavoriteShows": "Series favoritas", "HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteSongs": "Canciones favoritas", "HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo", "HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "Siguiente", "HeaderNextUp": "Siguiente",
@@ -70,9 +70,10 @@
"ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciado", "ScheduledTaskStartedWithName": "{0} iniciado",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Series", "Shows": "Programas",
"Songs": "Canciones", "Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.", "StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}", "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}",
"Sync": "Sincronizar", "Sync": "Sincronizar",
"System": "Sistema", "System": "Sistema",
@@ -128,6 +129,8 @@
"TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.", "TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.",
"TaskAudioNormalization": "Normalización de audio", "TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de 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", "TaskDownloadMissingLyrics": "Descargar letra faltante",
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones", "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
"TaskExtractMediaSegments": "Escanear Segmentos de Media", "TaskExtractMediaSegments": "Escanear Segmentos de Media",

View File

@@ -73,6 +73,7 @@
"Shows": "Programas", "Shows": "Programas",
"Songs": "Canciones", "Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.", "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
"SubtitleDownloadFailureForItem": "Falló la descarga de subtítulos para {0}",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}", "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar", "Sync": "Sincronizar",
"System": "Sistema", "System": "Sistema",
@@ -128,6 +129,8 @@
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción", "TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
"TaskAudioNormalization": "Normalización de audio", "TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el 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", "TaskDownloadMissingLyrics": "descargar letras que faltan",
"TaskDownloadMissingLyricsDescription": "Descargar letras de canciones", "TaskDownloadMissingLyricsDescription": "Descargar letras de canciones",
"TaskExtractMediaSegments": "Escaneo de segmentos de medios", "TaskExtractMediaSegments": "Escaneo de segmentos de medios",

View File

@@ -73,6 +73,7 @@
"Shows": "Series", "Shows": "Series",
"Songs": "Canciones", "Songs": "Canciones",
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.", "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}",
"SubtitleDownloadFailureFromForItem": "Fallo en la descarga de subtítulos desde {0} para {1}", "SubtitleDownloadFailureFromForItem": "Fallo en la descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar", "Sync": "Sincronizar",
"System": "Sistema", "System": "Sistema",
@@ -126,6 +127,8 @@
"HearingImpaired": "Discapacidad Auditiva", "HearingImpaired": "Discapacidad Auditiva",
"TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo", "TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
"TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas.", "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", "TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización.", "TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización.",
"TaskDownloadMissingLyricsDescription": "Descargar letras para las canciones", "TaskDownloadMissingLyricsDescription": "Descargar letras para las canciones",
@@ -135,6 +138,5 @@
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.", "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay", "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
"CleanupUserDataTask": "Tarea de limpieza de datos del usuario", "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.", "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"
} }

View File

@@ -127,7 +127,9 @@
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.", "TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción", "TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
"TaskAudioNormalization": "Normalización de audio", "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.", "TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
"TaskDownloadMissingLyrics": "Descargar letra faltante", "TaskDownloadMissingLyrics": "Descargar letra faltante",
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones", "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de complementos habilitados para MediaSegment.", "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de complementos habilitados para MediaSegment.",

View File

@@ -41,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.", "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", "TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.", "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", "TvShows": "Series de TV",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}", "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"TaskRefreshChannels": "Actualizar canales", "TaskRefreshChannels": "Actualizar canales",

View File

@@ -72,7 +72,7 @@
"NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval", "NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
"NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.", "NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.",
"NameSeasonUnknown": "Tundmatu hooaeg", "NameSeasonUnknown": "Tundmatu hooaeg",
"NameSeasonNumber": "{0}. hooaeg", "NameSeasonNumber": "Hooaeg {0}",
"NameInstallFailed": "{0} paigaldamine nurjus", "NameInstallFailed": "{0} paigaldamine nurjus",
"MusicVideos": "Muusikavideod", "MusicVideos": "Muusikavideod",
"Music": "Muusika", "Music": "Muusika",
@@ -128,11 +128,13 @@
"TaskRefreshTrickplayImagesDescription": "Loob trickplay eelvaated videotele lubatud meediakogudes.", "TaskRefreshTrickplayImagesDescription": "Loob trickplay eelvaated videotele lubatud meediakogudes.",
"TaskAudioNormalization": "Normaliseeri helitugevus", "TaskAudioNormalization": "Normaliseeri helitugevus",
"TaskAudioNormalizationDescription": "Otsib failidest helitugevuse normaliseerimise teavet.", "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", "TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine", "TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.", "TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
"TaskExtractMediaSegments": "Skaneeri meedialõike", "TaskExtractMediaSegments": "Skaneeri meediasegmente",
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.", "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht", "TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
"CleanupUserDataTask": "Puhasta kasutajaandmed", "CleanupUserDataTask": "Puhasta kasutajaandmed",
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud." "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."

View File

@@ -130,6 +130,8 @@
"TaskDownloadMissingLyrics": "Deskargatu falta diren letrak", "TaskDownloadMissingLyrics": "Deskargatu falta diren letrak",
"TaskDownloadMissingLyricsDescription": "Deskargatu abestientzako letrak", "TaskDownloadMissingLyricsDescription": "Deskargatu abestientzako letrak",
"TaskExtractMediaSegments": "Multimedia segmentuen eskaneoa", "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.", "TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
"TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua", "TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.", "TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",

View File

@@ -73,6 +73,7 @@
"Shows": "سریال‌ها", "Shows": "سریال‌ها",
"Songs": "موسیقی‌ها", "Songs": "موسیقی‌ها",
"StartupEmbyServerIsLoading": "سرور Jellyfin در حال بارگیری است. لطفا کمی بعد دوباره تلاش کنید.", "StartupEmbyServerIsLoading": "سرور Jellyfin در حال بارگیری است. لطفا کمی بعد دوباره تلاش کنید.",
"SubtitleDownloadFailureForItem": "دانلود زیرنویس برای {0} ناموفق بود",
"SubtitleDownloadFailureFromForItem": "بارگیری زیرنویس برای {1} از {0} شکست خورد", "SubtitleDownloadFailureFromForItem": "بارگیری زیرنویس برای {1} از {0} شکست خورد",
"Sync": "همگام‌سازی", "Sync": "همگام‌سازی",
"System": "سیستم", "System": "سیستم",
@@ -126,6 +127,8 @@
"HearingImpaired": "مشکل شنوایی", "HearingImpaired": "مشکل شنوایی",
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay", "TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
"TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه.", "TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه.",
"TaskCleanCollectionsAndPlaylists": "پاکسازی مجموعه ها و لیست پخش",
"TaskCleanCollectionsAndPlaylistsDescription": "موارد را از مجموعه ها و لیست پخش هایی که دیگر وجود ندارند حذف میکند.",
"TaskAudioNormalizationDescription": "بررسی فایل برای داده‌های نرمال کردن صدا.", "TaskAudioNormalizationDescription": "بررسی فایل برای داده‌های نرمال کردن صدا.",
"TaskDownloadMissingLyrics": "دانلود متن‌های ناموجود", "TaskDownloadMissingLyrics": "دانلود متن‌های ناموجود",
"TaskDownloadMissingLyricsDescription": "دانلود متن شعر‌ها", "TaskDownloadMissingLyricsDescription": "دانلود متن شعر‌ها",

View File

@@ -39,8 +39,8 @@
"Channels": "Kanavat", "Channels": "Kanavat",
"CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}", "CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}",
"Books": "Kirjat", "Books": "Kirjat",
"AuthenticationSucceededWithUserName": "{0} todennus onnistunut", "AuthenticationSucceededWithUserName": "{0} on todennettu",
"Artists": "Artistit", "Artists": "Esittäjät",
"Application": "Sovellus", "Application": "Sovellus",
"AppDeviceValues": "Sovellus: {0}, Laite: {1}", "AppDeviceValues": "Sovellus: {0}, Laite: {1}",
"Albums": "Albumit", "Albums": "Albumit",
@@ -126,6 +126,8 @@
"HearingImpaired": "Kuulorajoitteinen", "HearingImpaired": "Kuulorajoitteinen",
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat", "TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.", "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", "TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja.", "TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja.",
"TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka", "TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka",

View File

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

View File

@@ -73,6 +73,7 @@
"Shows": "Séries", "Shows": "Séries",
"Songs": "Chansons", "Songs": "Chansons",
"StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.", "StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}", "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
"Sync": "Synchroniser", "Sync": "Synchroniser",
"System": "Système", "System": "Système",
@@ -126,6 +127,8 @@
"HearingImpaired": "Malentendants", "HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay", "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.", "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", "TaskAudioNormalization": "Normalisation audio",
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.", "TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
"TaskExtractMediaSegments": "Analyse des segments de média", "TaskExtractMediaSegments": "Analyse des segments de média",

View File

@@ -73,6 +73,7 @@
"Shows": "Séries", "Shows": "Séries",
"Songs": "Chansons", "Songs": "Chansons",
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.", "StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
"SubtitleDownloadFailureForItem": "Le téléchargement des sous-titres pour {0} a échoué.",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}", "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
"Sync": "Synchroniser", "Sync": "Synchroniser",
"System": "Système", "System": "Système",
@@ -126,6 +127,8 @@
"HearingImpaired": "Malentendants", "HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay", "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.", "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", "TaskAudioNormalization": "Normalisation audio",
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.", "TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
"TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons", "TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons",

View File

@@ -29,10 +29,12 @@
"TaskRefreshChannelsDescription": "Athnuachan eolas faoi chainéil idirlín.", "TaskRefreshChannelsDescription": "Athnuachan eolas faoi chainéil idirlín.",
"TaskOptimizeDatabase": "Bunachar sonraí a bharrfheabhsú", "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.", "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", "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.", "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í.", "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.", "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.", "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.", "TaskMoveTrickplayImagesDescription": "Bogtar comhaid trickplay atá ann cheana de réir socruithe na leabharlainne.",
"AppDeviceValues": "Aip: {0}, Gléas: {1}", "AppDeviceValues": "Aip: {0}, Gléas: {1}",
@@ -135,6 +137,5 @@
"TaskCleanTranscode": "Eolaire Transcode Glan", "TaskCleanTranscode": "Eolaire Transcode Glan",
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh", "TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh",
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora", "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.", "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"
} }

View File

@@ -128,6 +128,8 @@
"TaskRefreshTrickplayImagesDescription": "Crea miniaturas de previsualización para os vídeos nas bibliotecas habilitadas.", "TaskRefreshTrickplayImagesDescription": "Crea miniaturas de previsualización para os vídeos nas bibliotecas habilitadas.",
"TaskDownloadMissingLyrics": "Descargar letras que faltan", "TaskDownloadMissingLyrics": "Descargar letras que faltan",
"TaskDownloadMissingLyricsDescription": "Descarga as letras das cancións", "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.", "TaskExtractMediaSegmentsDescription": "Procura segmentos de medios cos plugins habilitados.",
"TaskExtractMediaSegments": "Escaneo de segmentos de medios", "TaskExtractMediaSegments": "Escaneo de segmentos de medios",
"TaskMoveTrickplayImages": "Migrar as miniaturas de previsualización a outra ubicación", "TaskMoveTrickplayImages": "Migrar as miniaturas de previsualización a outra ubicación",

View File

@@ -73,6 +73,7 @@
"Shows": "Serie", "Shows": "Serie",
"Songs": "Lieder", "Songs": "Lieder",
"StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.", "StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde", "SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde",
"Sync": "Synchronisation", "Sync": "Synchronisation",
"System": "System", "System": "System",

View File

@@ -73,6 +73,7 @@
"Shows": "סדרות", "Shows": "סדרות",
"Songs": "שירים", "Songs": "שירים",
"StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.", "StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה", "SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
"Sync": "סנכרון", "Sync": "סנכרון",
"System": "מערכת", "System": "מערכת",
@@ -127,7 +128,9 @@
"TaskRefreshTrickplayImages": "יצירת תמונות Trickplay", "TaskRefreshTrickplayImages": "יצירת תמונות Trickplay",
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.", "TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.",
"TaskAudioNormalization": "נרמול שמע", "TaskAudioNormalization": "נרמול שמע",
"TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.", "TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
"TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה",
"TaskDownloadMissingLyrics": "הורדת מילים חסרות", "TaskDownloadMissingLyrics": "הורדת מילים חסרות",
"TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים", "TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
"TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay", "TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",

View File

@@ -1,27 +1 @@
{ {}
"Books": "ספרים",
"NameSeasonNumber": "עונה {0}",
"Channels": "ערוצים",
"Movies": "סרטים",
"Music": "מוזיקה",
"Collections": "אוספים",
"Albums": "אלבומים",
"Application": "אפליקציה",
"Artists": "אמנים",
"ChapterNameValue": "פרק {0}",
"External": "חיצונית",
"Favorites": "מועדפים",
"Folders": "תיקיות",
"Genres": "ז'אנרים",
"HeaderAlbumArtists": "אמני אלבומים",
"HeaderContinueWatching": "להמשיך לצפות",
"HeaderFavoriteAlbums": "אלבומים אהובים",
"HeaderFavoriteArtists": "אמנים אהובים",
"HeaderFavoriteEpisodes": "פרקים אהובים",
"HeaderFavoriteShows": "תוכניות אהובות",
"HeaderFavoriteSongs": "שירים אהובים",
"HeaderLiveTV": "טלוויזיה בשידור חי",
"HeaderNextUp": "הבא",
"HearingImpaired": "ללקויי שמיעה",
"HomeVideos": "סרטונים ביתיים"
}

View File

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

View File

@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}", "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
"Channels": "Kanali", "Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}", "ChapterNameValue": "Poglavlje {0}",
"Collections": "Zbirke", "Collections": "Kolekcije",
"DeviceOfflineWithName": "{0} je prekinuo vezu", "DeviceOfflineWithName": "{0} je prekinuo vezu",
"DeviceOnlineWithName": "{0} je povezan", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}", "FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
@@ -23,7 +23,7 @@
"HeaderFavoriteShows": "Omiljene serije", "HeaderFavoriteShows": "Omiljene serije",
"HeaderFavoriteSongs": "Omiljene pjesme", "HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo", "HeaderLiveTV": "TV uživo",
"HeaderNextUp": "Sljedeće na redu", "HeaderNextUp": "Slijedi",
"HeaderRecordingGroups": "Grupa snimka", "HeaderRecordingGroups": "Grupa snimka",
"HomeVideos": "Kućni video", "HomeVideos": "Kućni video",
"Inherit": "Naslijedi", "Inherit": "Naslijedi",
@@ -70,13 +70,14 @@
"ScheduledTaskFailedWithName": "{0} neuspjelo", "ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto", "ScheduledTaskStartedWithName": "{0} pokrenuto",
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti", "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
"Shows": "Emisije", "Shows": "Serije",
"Songs": "Pjesme", "Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.", "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
"SubtitleDownloadFailureFromForItem": "Titlovi nisu uspješno preuzeti od {0} za {1}", "SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
"Sync": "Sinkronizacija", "Sync": "Sinkronizacija",
"System": "Sustav", "System": "Sustav",
"TvShows": "TV emisije", "TvShows": "Serije",
"User": "Korisnik", "User": "Korisnik",
"UserCreatedWithName": "Korisnik {0} je kreiran", "UserCreatedWithName": "Korisnik {0} je kreiran",
"UserDeletedWithName": "Korisnik {0} je obrisan", "UserDeletedWithName": "Korisnik {0} je obrisan",
@@ -88,26 +89,26 @@
"UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}", "UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
"UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}", "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je dodano u biblioteku medija", "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
"ValueSpecialEpisodeName": "Posebno {0}", "ValueSpecialEpisodeName": "Posebno - {0}",
"VersionNumber": "Verzija {0}", "VersionNumber": "Verzija {0}",
"TaskRefreshLibraryDescription": "Skenira biblioteku medija radi novih datoteka i osvježava metapodatke.", "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
"TaskRefreshLibrary": "Skeniraj biblioteku medija", "TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
"TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.", "TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
"TaskRefreshChapterImages": "Izdvoji slike poglavlja", "TaskRefreshChapterImages": "Izdvoji slike poglavlja",
"TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.", "TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
"TaskCleanCache": "Očisti mapu predmemorije", "TaskCleanCache": "Očisti mapu predmemorije",
"TasksApplicationCategory": "Aplikacija", "TasksApplicationCategory": "Aplikacija",
"TasksMaintenanceCategory": "Održavanje", "TasksMaintenanceCategory": "Održavanje",
"TaskDownloadMissingSubtitlesDescription": "Pretraži internet za nedsotajućim titlovima ne osnovi konfiguracije metapodataka.", "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
"TaskDownloadMissingSubtitles": "Preuzmi nedostajuće titlove", "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
"TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.", "TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
"TaskRefreshChannels": "Osvježi kanale", "TaskRefreshChannels": "Osvježi kanale",
"TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.", "TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
"TaskCleanTranscode": "Očisti mapu transkodiranja", "TaskCleanTranscode": "Očisti mapu transkodiranja",
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.", "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
"TaskUpdatePlugins": "Ažuriraj dodatke", "TaskUpdatePlugins": "Ažuriraj dodatke",
"TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u biblioteci medija.", "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
"TaskRefreshPeople": "Osvježi osobe", "TaskRefreshPeople": "Osvježi osobe",
"TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.", "TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
"TaskCleanLogs": "Očisti mapu dnevnika zapisa", "TaskCleanLogs": "Očisti mapu dnevnika zapisa",
@@ -119,7 +120,7 @@
"Forced": "Forsirani", "Forced": "Forsirani",
"Default": "Zadano", "Default": "Zadano",
"TaskOptimizeDatabase": "Optimiziraj bazu podataka", "TaskOptimizeDatabase": "Optimiziraj bazu podataka",
"External": "Eksterni", "External": "Vanjski",
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.", "TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira", "TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.", "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
@@ -128,13 +129,14 @@
"TaskRefreshTrickplayImagesDescription": "Stvara preglede brzog pregledavanja za videa u aktiviranim bibliotekama.", "TaskRefreshTrickplayImagesDescription": "Stvara preglede brzog pregledavanja za videa u aktiviranim bibliotekama.",
"TaskAudioNormalization": "Normalizacija zvuka", "TaskAudioNormalization": "Normalizacija zvuka",
"TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.", "TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.",
"TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz zbirki i popisa za reprodukciju koje više ne postoje.",
"TaskCleanCollectionsAndPlaylists": "Očisti zbirke i popise za reprodukciju",
"TaskExtractMediaSegments": "Skeniranje dijelova medija", "TaskExtractMediaSegments": "Skeniranje dijelova medija",
"TaskDownloadMissingLyrics": "Preuzmi tekstove koji nedostaju", "TaskDownloadMissingLyrics": "Preuzmi tekstove koji nedostaju",
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama", "TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
"TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.", "TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.",
"TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja", "TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja",
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja u postavke biblioteke.", "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja prema postavkama biblioteke.",
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka", "CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana.", "CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana."
"Original": "Original"
} }

View File

@@ -58,8 +58,5 @@
"ValueSpecialEpisodeName": "Spesyal - {0}", "ValueSpecialEpisodeName": "Spesyal - {0}",
"VersionNumber": "Vesyon {0}", "VersionNumber": "Vesyon {0}",
"TasksApplicationCategory": "Aplikasyon", "TasksApplicationCategory": "Aplikasyon",
"TasksMaintenanceCategory": "Antretyen", "TasksMaintenanceCategory": "Antretyen"
"AppDeviceValues": "Aplikasyon: {0}, Aparèy: {1}",
"AuthenticationSucceededWithUserName": "{0} otantifye avèk siksè",
"CameraImageUploadedFrom": "Une nouvelle image de la caméra a été téléchargée depuis {0}"
} }

View File

@@ -39,7 +39,7 @@
"MixedContent": "Vegyes tartalom", "MixedContent": "Vegyes tartalom",
"Movies": "Filmek", "Movies": "Filmek",
"Music": "Zenék", "Music": "Zenék",
"MusicVideos": "Zenei videók", "MusicVideos": "Zenei videóklipek",
"NameInstallFailed": "{0} sikertelen telepítés", "NameInstallFailed": "{0} sikertelen telepítés",
"NameSeasonNumber": "{0}. évad", "NameSeasonNumber": "{0}. évad",
"NameSeasonUnknown": "Ismeretlen évad", "NameSeasonUnknown": "Ismeretlen évad",
@@ -55,7 +55,7 @@
"NotificationOptionPluginInstalled": "Bővítmény telepítve", "NotificationOptionPluginInstalled": "Bővítmény telepítve",
"NotificationOptionPluginUninstalled": "Bővítmény eltávolítva", "NotificationOptionPluginUninstalled": "Bővítmény eltávolítva",
"NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve", "NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve",
"NotificationOptionServerRestartRequired": "A szerver újraindítása szükséges", "NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges",
"NotificationOptionTaskFailed": "Hiba az ütemezett feladatban", "NotificationOptionTaskFailed": "Hiba az ütemezett feladatban",
"NotificationOptionUserLockedOut": "Felhasználó tiltva", "NotificationOptionUserLockedOut": "Felhasználó tiltva",
"NotificationOptionVideoPlayback": "Videólejátszás elkezdve", "NotificationOptionVideoPlayback": "Videólejátszás elkezdve",
@@ -73,6 +73,7 @@
"Shows": "Sorozatok", "Shows": "Sorozatok",
"Songs": "Számok", "Songs": "Számok",
"StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.", "StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}", "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}",
"Sync": "Szinkronizálás", "Sync": "Szinkronizálás",
"System": "Rendszer", "System": "Rendszer",
@@ -127,7 +128,9 @@
"TaskRefreshTrickplayImages": "Trickplay képek előállítása", "TaskRefreshTrickplayImages": "Trickplay képek előállítása",
"TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.", "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.",
"TaskAudioNormalization": "Hangerő-normalizálás", "TaskAudioNormalization": "Hangerő-normalizálás",
"TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.",
"TaskAudioNormalizationDescription": "Hangerő-normalizálási adatok keresése.", "TaskAudioNormalizationDescription": "Hangerő-normalizálási adatok keresése.",
"TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása",
"TaskExtractMediaSegments": "Médiaszegmens felismerése", "TaskExtractMediaSegments": "Médiaszegmens felismerése",
"TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése", "TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése",
"TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése", "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
@@ -135,6 +138,5 @@
"TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.", "TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.",
"TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.", "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.",
"CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.", "CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.",
"CleanupUserDataTask": "Felhasználói adatok tisztítása feladat", "CleanupUserDataTask": "Felhasználói adatok tisztítása feladat"
"Original": "Eredeti"
} }

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