mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-08-05 05:37:04 +02:00
Merge branch 'master' into master
This commit is contained in:
commit
4b6fb6c4bb
@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.2",
|
||||
"version": "9.0.7",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
@ -1,6 +1,8 @@
|
||||
{
|
||||
"name": "Development Jellyfin Server",
|
||||
"image":"mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
|
||||
"postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
|
||||
// reads the extensions list and installs them
|
||||
@ -11,9 +13,11 @@
|
||||
"dotnetRuntimeVersions": "9.0",
|
||||
"aspNetCoreRuntimeVersions": "9.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
|
||||
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
|
||||
"preserve_apt_list": false,
|
||||
"packages": ["libfontconfig1"]
|
||||
"packages": [
|
||||
"libfontconfig1"
|
||||
]
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"dockerDashComposeVersion": "v2"
|
||||
|
9
.github/ISSUE_TEMPLATE/issue report.yml
vendored
9
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@ -1,6 +1,7 @@
|
||||
name: Issue Report
|
||||
description: File an issue report
|
||||
labels: [bug, triage]
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
id: introduction
|
||||
@ -140,7 +141,9 @@ body:
|
||||
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
||||
- **Base URL**: [e.g. none, yes: /example]
|
||||
- **Networking**: [e.g. Host, Bridge/NAT]
|
||||
- **Storage**: [e.g. local, NFS, cloud]
|
||||
- **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD]
|
||||
- **Media Storage**: [e.g. Local HDD, SMB Share]
|
||||
- **External Integrations**: [e.g. Jellystat, Jellyseerr]
|
||||
value: |
|
||||
- OS:
|
||||
- Linux Kernel:
|
||||
@ -155,7 +158,9 @@ body:
|
||||
- Reverse Proxy:
|
||||
- Base URL:
|
||||
- Networking:
|
||||
- Storage:
|
||||
- Jellyfin Data Storage:
|
||||
- Media Storage:
|
||||
- External Integrations:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
|
8
.github/workflows/ci-codeql-analysis.yml
vendored
8
.github/workflows/ci-codeql-analysis.yml
vendored
@ -22,16 +22,16 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
||||
uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
||||
uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
||||
uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
|
14
.github/workflows/ci-compat.yml
vendored
14
.github/workflows/ci-compat.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
@ -26,7 +26,7 @@ jobs:
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: abi-head
|
||||
retention-days: 14
|
||||
@ -47,7 +47,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
@ -65,7 +65,7 @@ jobs:
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: abi-base
|
||||
retention-days: 14
|
||||
@ -85,13 +85,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download abi-head
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: abi-head
|
||||
path: abi-head
|
||||
|
||||
- name: Download abi-base
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: abi-base
|
||||
path: abi-base
|
||||
@ -105,7 +105,7 @@ jobs:
|
||||
run: |
|
||||
{
|
||||
echo 'body<<EOF'
|
||||
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.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 )"
|
||||
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
|
||||
printf "\n${file}\n${COMPAT_OUTPUT}\n"
|
||||
|
24
.github/workflows/ci-openapi.yml
vendored
24
.github/workflows/ci-openapi.yml
vendored
@ -21,13 +21,13 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.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@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
@ -55,13 +55,13 @@ jobs:
|
||||
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@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.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@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
@ -80,12 +80,12 @@ jobs:
|
||||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
@ -158,12 +158,12 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Upload openapi.json (unstable) to repository server
|
||||
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
|
||||
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
@ -172,7 +172,7 @@ jobs:
|
||||
strip_components: 1
|
||||
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||
- name: Move openapi.json (unstable) into place
|
||||
uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1
|
||||
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
@ -220,12 +220,12 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Upload openapi.json (stable) to repository server
|
||||
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
|
||||
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
@ -234,7 +234,7 @@ jobs:
|
||||
strip_components: 1
|
||||
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||
- name: Move openapi.json (stable) into place
|
||||
uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1
|
||||
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
|
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
|
||||
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
with:
|
||||
dotnet-version: ${{ env.SDK_VERSION }}
|
||||
|
||||
@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@f1927db1dbfc029b056583ee488832e939447fe6 # v5.4.4
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@c1dd332d00304c5aa5d506aab698a5224a8fa24e # 5.4.11
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
4
.github/workflows/commands.yml
vendored
4
.github/workflows/commands.yml
vendored
@ -44,9 +44,9 @@ jobs:
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.13'
|
||||
cache: 'pip'
|
||||
- name: install python packages
|
||||
run: pip install -r rename/requirements.txt
|
||||
|
4
.github/workflows/issue-template-check.yml
vendored
4
.github/workflows/issue-template-check.yml
vendored
@ -14,9 +14,9 @@ jobs:
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.13'
|
||||
cache: 'pip'
|
||||
- name: install python packages
|
||||
run: pip install -r main-repo-triage/requirements.txt
|
||||
|
2
.github/workflows/pull-request-conflict.yml
vendored
2
.github/workflows/pull-request-conflict.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
label:
|
||||
name: Labeling
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin' && github.event.issue.pull_request }}
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
||||
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"dotnet.preferVisualStudioCodeFileSystemWatcher": true
|
||||
}
|
@ -27,6 +27,7 @@
|
||||
- [cryptobank](https://github.com/cryptobank)
|
||||
- [cvium](https://github.com/cvium)
|
||||
- [dannymichel](https://github.com/dannymichel)
|
||||
- [darioackermann](https://github.com/darioackermann)
|
||||
- [DaveChild](https://github.com/DaveChild)
|
||||
- [DavidFair](https://github.com/DavidFair)
|
||||
- [Delgan](https://github.com/Delgan)
|
||||
@ -60,6 +61,7 @@
|
||||
- [ikomhoog](https://github.com/ikomhoog)
|
||||
- [iwalton3](https://github.com/iwalton3)
|
||||
- [jftuga](https://github.com/jftuga)
|
||||
- [jkhsjdhjs](https://github.com/jkhsjdhjs)
|
||||
- [jmshrv](https://github.com/jmshrv)
|
||||
- [joern-h](https://github.com/joern-h)
|
||||
- [joshuaboniface](https://github.com/joshuaboniface)
|
||||
@ -196,6 +198,10 @@
|
||||
- [benedikt257](https://github.com/benedikt257)
|
||||
- [revam](https://github.com/revam)
|
||||
- [Jxiced](https://github.com/Jxiced)
|
||||
- [allesmi](https://github.com/allesmi)
|
||||
- [ThunderClapLP](https://github.com/ThunderClapLP)
|
||||
- [Shoham Peller](https://github.com/spellr)
|
||||
- [theshoeshiner](https://github.com/theshoeshiner)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
@ -270,3 +276,5 @@
|
||||
- [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)
|
||||
|
@ -4,82 +4,88 @@
|
||||
</PropertyGroup>
|
||||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||
<ItemGroup Label="Package Dependencies">
|
||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.4" />
|
||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.6" />
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.3.4" />
|
||||
<PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||
<PackageVersion Include="Diacritics" Version="4.0.17" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="3.1.0" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="3.3.0" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
|
||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||
<PackageVersion Include="Ignore" Version="0.2.1" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="4.0.10" />
|
||||
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
|
||||
<PackageVersion Include="libse" Version="4.0.12" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="NEbml" Version="0.12.0" />
|
||||
<PackageVersion Include="NEbml" Version="1.0.0.3" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="Polly" Version="8.6.2" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
||||
<PackageVersion Include="SkiaSharp" Version="2.88.9" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
||||
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="2.0.0.4" />
|
||||
<PackageVersion Include="Svg.Skia" Version="3.0.4" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.7" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.17.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.2.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
|
@ -187,7 +187,9 @@ namespace Emby.Naming.Common
|
||||
"disc",
|
||||
"disk",
|
||||
"vol",
|
||||
"volume"
|
||||
"volume",
|
||||
"part",
|
||||
"act"
|
||||
};
|
||||
|
||||
ArtistSubfolders = new[]
|
||||
@ -238,6 +240,7 @@ namespace Emby.Naming.Common
|
||||
".dsp",
|
||||
".dts",
|
||||
".dvf",
|
||||
".eac3",
|
||||
".far",
|
||||
".flac",
|
||||
".gdm",
|
||||
|
@ -1,43 +1,35 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to parse season paths.
|
||||
/// </summary>
|
||||
public static class SeasonPathParser
|
||||
public static partial class SeasonPathParser
|
||||
{
|
||||
/// <summary>
|
||||
/// A season folder must contain one of these somewhere in the name.
|
||||
/// </summary>
|
||||
private static readonly string[] _seasonFolderNames =
|
||||
{
|
||||
"season",
|
||||
"sæson",
|
||||
"temporada",
|
||||
"saison",
|
||||
"staffel",
|
||||
"series",
|
||||
"сезон",
|
||||
"stagione"
|
||||
};
|
||||
[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>.*)$")]
|
||||
private static partial Regex ProcessPre();
|
||||
|
||||
private static readonly char[] _splitChars = ['.', '_', ' ', '-'];
|
||||
[GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$")]
|
||||
private static partial Regex ProcessPost();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse season number from path.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to season.</param>
|
||||
/// <param name="parentPath">Folder name of the parent.</param>
|
||||
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
|
||||
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
|
||||
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
|
||||
public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
|
||||
public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders)
|
||||
{
|
||||
var result = new SeasonPathParserResult();
|
||||
var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
|
||||
|
||||
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
|
||||
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders);
|
||||
|
||||
result.SeasonNumber = seasonNumber;
|
||||
|
||||
@ -54,15 +46,24 @@ namespace Emby.Naming.TV
|
||||
/// Gets the season number from path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="parentFolderName">The parent folder name.</param>
|
||||
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
|
||||
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
|
||||
string path,
|
||||
string? parentFolderName,
|
||||
bool supportSpecialAliases,
|
||||
bool supportNumericSeasonFolders)
|
||||
{
|
||||
string filename = Path.GetFileName(path);
|
||||
filename = Regex.Replace(filename, "[ ._-]", string.Empty);
|
||||
|
||||
if (parentFolderName is not null)
|
||||
{
|
||||
parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty);
|
||||
filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (supportSpecialAliases)
|
||||
{
|
||||
@ -85,53 +86,38 @@ namespace Emby.Naming.TV
|
||||
}
|
||||
}
|
||||
|
||||
if (TryGetSeasonNumberFromPart(filename, out int seasonNumber))
|
||||
if (filename.StartsWith('s'))
|
||||
{
|
||||
var testFilename = filename.AsSpan()[1..];
|
||||
|
||||
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return (val, true);
|
||||
}
|
||||
}
|
||||
|
||||
var preMatch = ProcessPre().Match(filename);
|
||||
if (preMatch.Success)
|
||||
{
|
||||
return CheckMatch(preMatch);
|
||||
}
|
||||
else
|
||||
{
|
||||
var postMatch = ProcessPost().Match(filename);
|
||||
return CheckMatch(postMatch);
|
||||
}
|
||||
}
|
||||
|
||||
private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
|
||||
{
|
||||
var numberString = match.Groups["seasonnumber"];
|
||||
if (numberString.Success)
|
||||
{
|
||||
var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture);
|
||||
return (seasonNumber, true);
|
||||
}
|
||||
|
||||
// Look for one of the season folder names
|
||||
foreach (var name in _seasonFolderNames)
|
||||
{
|
||||
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
|
||||
if (result.SeasonNumber.HasValue)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (TryGetSeasonNumberFromPart(part, out seasonNumber))
|
||||
{
|
||||
return (seasonNumber, true);
|
||||
}
|
||||
}
|
||||
|
||||
return (null, true);
|
||||
}
|
||||
|
||||
private static bool TryGetSeasonNumberFromPart(ReadOnlySpan<char> part, out int seasonNumber)
|
||||
{
|
||||
seasonNumber = 0;
|
||||
if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
seasonNumber = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return (null, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -18,8 +18,9 @@ namespace Emby.Naming.Video
|
||||
/// </summary>
|
||||
/// <param name="path">Path to file.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
|
||||
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "")
|
||||
{
|
||||
var result = new ExtraResult();
|
||||
|
||||
@ -69,7 +70,9 @@ namespace Emby.Naming.Video
|
||||
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
||||
{
|
||||
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
|
||||
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||
string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
|
||||
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.ExtraType = rule.ExtraType;
|
||||
result.Rule = rule;
|
||||
|
@ -27,8 +27,9 @@ namespace Emby.Naming.Video
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
||||
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
||||
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
||||
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
|
||||
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "")
|
||||
{
|
||||
// Filter out all extras, otherwise they could cause stacks to not be resolved
|
||||
// See the unit test TestStackedWithTrailer
|
||||
@ -65,7 +66,7 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
var info = new VideoInfo(stack.Name)
|
||||
{
|
||||
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
|
||||
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot))
|
||||
.OfType<VideoFileInfo>()
|
||||
.ToList()
|
||||
};
|
||||
|
@ -17,10 +17,11 @@ namespace Emby.Naming.Video
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
||||
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||
/// <returns>VideoFileInfo.</returns>
|
||||
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
|
||||
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
|
||||
{
|
||||
return Resolve(path, true, namingOptions, parseName);
|
||||
return Resolve(path, true, namingOptions, parseName, libraryRoot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -28,10 +29,11 @@ namespace Emby.Naming.Video
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||
/// <returns>VideoFileInfo.</returns>
|
||||
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
|
||||
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions, string? libraryRoot = "")
|
||||
{
|
||||
return Resolve(path, false, namingOptions);
|
||||
return Resolve(path, false, namingOptions, libraryRoot: libraryRoot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -41,9 +43,10 @@ namespace Emby.Naming.Video
|
||||
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
|
||||
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||
/// <returns>VideoFileInfo.</returns>
|
||||
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
|
||||
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
|
||||
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
@ -75,7 +78,7 @@ namespace Emby.Naming.Video
|
||||
|
||||
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
||||
|
||||
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
|
||||
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions, libraryRoot);
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
|
@ -49,7 +49,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
var file = directoryService.GetFile(item.Path);
|
||||
return file is not null && file.LastWriteTimeUtc != item.DateModified;
|
||||
return file is not null && item.HasChanged(file.LastWriteTimeUtc);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
|
||||
var dateTaken = image.ImageTag.DateTime;
|
||||
if (dateTaken.HasValue)
|
||||
{
|
||||
item.DateCreated = dateTaken.Value;
|
||||
item.DateCreated = dateTaken.Value.ToUniversalTime();
|
||||
item.PremiereDate = dateTaken.Value;
|
||||
item.ProductionYear = dateTaken.Value.Year;
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Emby.Server.Implementations.AppBase
|
||||
@ -30,80 +33,91 @@ namespace Emby.Server.Implementations.AppBase
|
||||
ConfigurationDirectoryPath = configurationDirectoryPath;
|
||||
CachePath = cacheDirectoryPath;
|
||||
WebPath = webDirectoryPath;
|
||||
|
||||
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the program data folder.
|
||||
/// </summary>
|
||||
/// <value>The program data path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ProgramDataPath { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string WebPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the system folder.
|
||||
/// </summary>
|
||||
/// <value>The path to the system folder.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder path to the data directory.
|
||||
/// </summary>
|
||||
/// <value>The data directory.</value>
|
||||
/// <inheritdoc/>
|
||||
public string DataPath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string VirtualDataPath => "%AppDataPath%";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache path.
|
||||
/// </summary>
|
||||
/// <value>The image cache path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ImageCachePath => Path.Combine(CachePath, "images");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the plugin directory.
|
||||
/// </summary>
|
||||
/// <value>The plugins path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the plugin configurations directory.
|
||||
/// </summary>
|
||||
/// <value>The plugin configurations path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the log directory.
|
||||
/// </summary>
|
||||
/// <value>The log directory path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string LogDirectoryPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the application configuration root directory.
|
||||
/// </summary>
|
||||
/// <value>The configuration directory path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ConfigurationDirectoryPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the system configuration file.
|
||||
/// </summary>
|
||||
/// <value>The system configuration file path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the folder path to the cache directory.
|
||||
/// </summary>
|
||||
/// <value>The cache directory.</value>
|
||||
/// <inheritdoc/>
|
||||
public string CachePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder path to the temp directory within the cache folder.
|
||||
/// </summary>
|
||||
/// <value>The temp directory.</value>
|
||||
/// <inheritdoc/>
|
||||
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string TrickplayPath => Path.Combine(DataPath, "trickplay");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string BackupPath => Path.Combine(DataPath, "backups");
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void MakeSanityCheckOrThrow()
|
||||
{
|
||||
CreateAndCheckMarker(ConfigurationDirectoryPath, "config");
|
||||
CreateAndCheckMarker(LogDirectoryPath, "log");
|
||||
CreateAndCheckMarker(PluginsPath, "plugin");
|
||||
CreateAndCheckMarker(ProgramDataPath, "data");
|
||||
CreateAndCheckMarker(CachePath, "cache");
|
||||
CreateAndCheckMarker(DataPath, "data");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateAndCheckMarker(string path, string markerName, bool recursive = false)
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetMarkers(string path, bool recursive = false)
|
||||
{
|
||||
return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
||||
}
|
||||
|
||||
private void CheckOrCreateMarker(string path, string markerName, bool recursive = false)
|
||||
{
|
||||
var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName);
|
||||
if (otherMarkers != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}.");
|
||||
}
|
||||
|
||||
var markerPath = Path.Combine(path, markerName);
|
||||
if (!File.Exists(markerPath))
|
||||
{
|
||||
FileHelper.CreateEmpty(markerPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -227,6 +227,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
|
||||
Logger.LogInformation("Setting cache path: {Path}", cachePath);
|
||||
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
|
||||
CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -15,6 +15,7 @@ using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Photos;
|
||||
using Emby.Server.Implementations.Chapters;
|
||||
using Emby.Server.Implementations.Collections;
|
||||
using Emby.Server.Implementations.Configuration;
|
||||
using Emby.Server.Implementations.Cryptography;
|
||||
@ -39,9 +40,10 @@ using Jellyfin.Drawing;
|
||||
using Jellyfin.MediaEncoding.Hls.Playlist;
|
||||
using Jellyfin.Networking.Manager;
|
||||
using Jellyfin.Networking.Udp;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using Jellyfin.Server.Implementations.FullSystemBackup;
|
||||
using Jellyfin.Server.Implementations.Item;
|
||||
using Jellyfin.Server.Implementations.MediaSegments;
|
||||
using Jellyfin.Server.Implementations.SystemBackupService;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Events;
|
||||
@ -57,10 +59,14 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LibraryTaskScheduler;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.MediaSegments;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
@ -91,7 +97,6 @@ using MediaBrowser.Providers.Subtitles;
|
||||
using MediaBrowser.XbmcMetadata.Providers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -266,6 +271,8 @@ namespace Emby.Server.Implementations
|
||||
? Environment.MachineName
|
||||
: ConfigurationManager.Configuration.ServerName;
|
||||
|
||||
public string RestoreBackupPath { get; set; }
|
||||
|
||||
public string ExpandVirtualPath(string path)
|
||||
{
|
||||
if (path is null)
|
||||
@ -470,6 +477,7 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<IApplicationHost>(this);
|
||||
serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
||||
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||
serviceCollection.AddSingleton<IBackupService, BackupService>();
|
||||
|
||||
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
|
||||
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
|
||||
@ -504,10 +512,13 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
|
||||
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
|
||||
serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
|
||||
serviceCollection.AddSingleton<IKeyframeRepository, KeyframeRepository>();
|
||||
serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
|
||||
|
||||
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||
serviceCollection.AddSingleton<EncodingHelper>();
|
||||
serviceCollection.AddSingleton<IPathManager, PathManager>();
|
||||
serviceCollection.AddSingleton<IExternalDataManager, ExternalDataManager>();
|
||||
|
||||
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
||||
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
||||
@ -542,6 +553,7 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||
serviceCollection.AddSingleton<ILimitedConcurrencyLibraryScheduler, LimitedConcurrencyLibraryScheduler>();
|
||||
|
||||
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||
|
||||
@ -549,13 +561,14 @@ namespace Emby.Server.Implementations
|
||||
|
||||
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IAuthService, AuthService>();
|
||||
serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
|
||||
serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
|
||||
serviceCollection.AddSingleton<IKeyframeManager, KeyframeManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
|
||||
@ -572,20 +585,10 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// Create services registered with the service container that need to be initialized at application startup.
|
||||
/// </summary>
|
||||
/// <param name="startupConfig">The configuration used to initialise the application.</param>
|
||||
/// <returns>A task representing the service initialization operation.</returns>
|
||||
public async Task InitializeServices()
|
||||
public async Task InitializeServices(IConfiguration startupConfig)
|
||||
{
|
||||
var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (jellyfinDb.ConfigureAwait(false))
|
||||
{
|
||||
if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
|
||||
{
|
||||
Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
|
||||
await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false);
|
||||
Logger.LogInformation("EFCore migrations applied successfully");
|
||||
}
|
||||
}
|
||||
|
||||
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
|
||||
await localizationManager.LoadAll().ConfigureAwait(false);
|
||||
|
||||
@ -633,24 +636,26 @@ namespace Emby.Server.Implementations
|
||||
private void SetStaticProperties()
|
||||
{
|
||||
// For now there's no real way to inject these properly
|
||||
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
||||
BaseItem.ConfigurationManager = ConfigurationManager;
|
||||
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
||||
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||
BaseItem.ItemRepository = Resolve<IItemRepository>();
|
||||
BaseItem.ChapterRepository = Resolve<IChapterRepository>();
|
||||
BaseItem.FileSystem = Resolve<IFileSystem>();
|
||||
BaseItem.UserDataManager = Resolve<IUserDataManager>();
|
||||
BaseItem.ChapterManager = Resolve<IChapterManager>();
|
||||
BaseItem.ChannelManager = Resolve<IChannelManager>();
|
||||
Video.RecordingsManager = Resolve<IRecordingsManager>();
|
||||
Folder.UserViewManager = Resolve<IUserViewManager>();
|
||||
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
|
||||
UserView.CollectionManager = Resolve<ICollectionManager>();
|
||||
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
|
||||
BaseItem.ConfigurationManager = ConfigurationManager;
|
||||
BaseItem.FileSystem = Resolve<IFileSystem>();
|
||||
BaseItem.ItemRepository = Resolve<IItemRepository>();
|
||||
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
||||
BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>();
|
||||
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
|
||||
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
||||
BaseItem.UserDataManager = Resolve<IUserDataManager>();
|
||||
CollectionFolder.XmlSerializer = _xmlSerializer;
|
||||
CollectionFolder.ApplicationHost = this;
|
||||
Folder.UserViewManager = Resolve<IUserViewManager>();
|
||||
Folder.CollectionManager = Resolve<ICollectionManager>();
|
||||
Folder.LimitedConcurrencyLibraryScheduler = Resolve<ILimitedConcurrencyLibraryScheduler>();
|
||||
Episode.MediaEncoder = Resolve<IMediaEncoder>();
|
||||
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
|
||||
Video.RecordingsManager = Resolve<IRecordingsManager>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
313
Emby.Server.Implementations/Chapters/ChapterManager.cs
Normal file
313
Emby.Server.Implementations/Chapters/ChapterManager.cs
Normal file
@ -0,0 +1,313 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Chapters;
|
||||
|
||||
/// <summary>
|
||||
/// The chapter manager.
|
||||
/// </summary>
|
||||
public class ChapterManager : IChapterManager
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<ChapterManager> _logger;
|
||||
private readonly IMediaEncoder _encoder;
|
||||
private readonly IChapterRepository _chapterRepository;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IPathManager _pathManager;
|
||||
|
||||
/// <summary>
|
||||
/// The first chapter ticks.
|
||||
/// </summary>
|
||||
private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChapterManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger{ChapterManager}"/>.</param>
|
||||
/// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
|
||||
/// <param name="encoder">The <see cref="IMediaEncoder"/>.</param>
|
||||
/// <param name="chapterRepository">The <see cref="IChapterRepository"/>.</param>
|
||||
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
|
||||
/// <param name="pathManager">The <see cref="IPathManager"/>.</param>
|
||||
public ChapterManager(
|
||||
ILogger<ChapterManager> logger,
|
||||
IFileSystem fileSystem,
|
||||
IMediaEncoder encoder,
|
||||
IChapterRepository chapterRepository,
|
||||
ILibraryManager libraryManager,
|
||||
IPathManager pathManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_encoder = encoder;
|
||||
_chapterRepository = chapterRepository;
|
||||
_libraryManager = libraryManager;
|
||||
_pathManager = pathManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is eligible for chapter image extraction] [the specified video].
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
/// <param name="libraryOptions">The library options for the video.</param>
|
||||
/// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns>
|
||||
private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions)
|
||||
{
|
||||
if (video.IsPlaceHolder)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (video.IsShortcut)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!video.IsCompleteMedia)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Can't extract images if there are no video streams
|
||||
return video.DefaultVideoStreamIndex.HasValue;
|
||||
}
|
||||
|
||||
private long GetAverageDurationBetweenChapters(IReadOnlyList<ChapterInfo> chapters)
|
||||
{
|
||||
if (chapters.Count < 2)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
long sum = 0;
|
||||
for (int i = 1; i < chapters.Count; i++)
|
||||
{
|
||||
sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks;
|
||||
}
|
||||
|
||||
return sum / chapters.Count;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
|
||||
{
|
||||
if (chapters.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(video);
|
||||
|
||||
if (!IsEligibleForChapterImageExtraction(video, libraryOptions))
|
||||
{
|
||||
extractImages = false;
|
||||
}
|
||||
|
||||
var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
|
||||
var threshold = TimeSpan.FromSeconds(1).Ticks;
|
||||
if (averageChapterDuration < threshold)
|
||||
{
|
||||
_logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold);
|
||||
extractImages = false;
|
||||
}
|
||||
|
||||
var success = true;
|
||||
var changesMade = false;
|
||||
|
||||
var runtimeTicks = video.RunTimeTicks ?? 0;
|
||||
|
||||
var currentImages = GetSavedChapterImages(video, directoryService);
|
||||
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (chapter.StartPositionTicks >= runtimeTicks)
|
||||
{
|
||||
_logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name);
|
||||
break;
|
||||
}
|
||||
|
||||
var path = _pathManager.GetChapterImagePath(video, chapter.StartPositionTicks);
|
||||
|
||||
if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (extractImages)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
// Add some time for the first chapter to make sure we don't end up with a black image
|
||||
var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks);
|
||||
|
||||
var inputPath = video.Path;
|
||||
var directoryPath = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(directoryPath))
|
||||
{
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
}
|
||||
|
||||
var container = video.Container;
|
||||
var mediaSource = new MediaSourceInfo
|
||||
{
|
||||
VideoType = video.VideoType,
|
||||
IsoType = video.IsoType,
|
||||
Protocol = video.PathProtocol ?? MediaProtocol.File,
|
||||
};
|
||||
|
||||
_logger.LogInformation("Extracting chapter image for {Name} at {Path}", video.Name, inputPath);
|
||||
var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false);
|
||||
File.Copy(tempFile, path, true);
|
||||
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(tempFile);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile);
|
||||
}
|
||||
|
||||
chapter.ImagePath = path;
|
||||
chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
|
||||
changesMade = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path));
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(chapter.ImagePath))
|
||||
{
|
||||
chapter.ImagePath = null;
|
||||
changesMade = true;
|
||||
}
|
||||
}
|
||||
else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
chapter.ImagePath = path;
|
||||
chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
|
||||
changesMade = true;
|
||||
}
|
||||
else if (libraryOptions?.EnableChapterImageExtraction != true)
|
||||
{
|
||||
// We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image
|
||||
chapter.ImagePath = null;
|
||||
changesMade = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (saveChapters && changesMade)
|
||||
{
|
||||
_chapterRepository.SaveChapters(video.Id, chapters);
|
||||
}
|
||||
|
||||
DeleteDeadImages(currentImages, chapters);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
|
||||
{
|
||||
_chapterRepository.SaveChapters(video.Id, chapters);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ChapterInfo? GetChapter(Guid baseItemId, int index)
|
||||
{
|
||||
return _chapterRepository.GetChapter(baseItemId, index);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId)
|
||||
{
|
||||
return _chapterRepository.GetChapters(baseItemId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteChapterImages(Video video)
|
||||
{
|
||||
var path = _pathManager.GetChapterImageFolderPath(video);
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
_logger.LogInformation("Removing chapter images for {Name} [{Id}]", video.Name, video.Id);
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Failed to remove chapter image folder for {Item}: {Exception}", video.Id, ex);
|
||||
}
|
||||
|
||||
_chapterRepository.DeleteChapters(video.Id);
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
|
||||
{
|
||||
var path = _pathManager.GetChapterImageFolderPath(video);
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return directoryService.GetFilePaths(path);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteDeadImages(IEnumerable<string> images, IEnumerable<ChapterInfo> chapters)
|
||||
{
|
||||
var existingImages = chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i));
|
||||
var deadImages = images
|
||||
.Except(existingImages, StringComparer.OrdinalIgnoreCase)
|
||||
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
foreach (var image in deadImages)
|
||||
{
|
||||
_logger.LogDebug("Deleting dead chapter image {Path}", image);
|
||||
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(image!);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting {Path}.", image);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
var libraryOptions = new LibraryOptions
|
||||
{
|
||||
PathInfos = new[] { new MediaPathInfo(path) },
|
||||
PathInfos = [new MediaPathInfo(path)],
|
||||
EnableRealtimeMonitor = false,
|
||||
SaveLocalMetadata = true
|
||||
};
|
||||
@ -150,15 +150,15 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
var info = Directory.CreateDirectory(path);
|
||||
var collection = new BoxSet
|
||||
{
|
||||
Name = name,
|
||||
Path = path,
|
||||
IsLocked = options.IsLocked,
|
||||
ProviderIds = options.ProviderIds,
|
||||
DateCreated = DateTime.UtcNow
|
||||
DateCreated = info.CreationTimeUtc,
|
||||
DateModified = info.LastWriteTimeUtc
|
||||
};
|
||||
|
||||
parentFolder.AddChild(collection);
|
||||
@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
{
|
||||
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
|
||||
{
|
||||
throw new ArgumentException("No collection exists with the supplied Id");
|
||||
throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId);
|
||||
}
|
||||
|
||||
List<BaseItem>? itemList = null;
|
||||
@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
throw new ArgumentException("No item exists with the supplied Id");
|
||||
throw new ArgumentException("No item exists with the supplied Id " + id);
|
||||
}
|
||||
|
||||
if (!currentLinkedChildrenIds.Contains(id))
|
||||
|
@ -1,84 +1,114 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
namespace Emby.Server.Implementations.Data;
|
||||
|
||||
public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
||||
{
|
||||
public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly IPathManager _pathManager;
|
||||
|
||||
public CleanDatabaseScheduledTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<CleanDatabaseScheduledTask> logger,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider,
|
||||
IPathManager pathManager)
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_dbProvider = dbProvider;
|
||||
_pathManager = pathManager;
|
||||
}
|
||||
|
||||
public CleanDatabaseScheduledTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<CleanDatabaseScheduledTask> logger,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_dbProvider = dbProvider;
|
||||
}
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
|
||||
}
|
||||
HasDeadParentId = true
|
||||
});
|
||||
|
||||
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
var numComplete = 0;
|
||||
var numItems = itemIds.Count + 1;
|
||||
|
||||
_logger.LogDebug("Cleaning {Number} items with dead parents", numItems);
|
||||
|
||||
foreach (var itemId in itemIds)
|
||||
{
|
||||
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item is not null)
|
||||
{
|
||||
HasDeadParentId = true
|
||||
});
|
||||
_logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
|
||||
|
||||
var numComplete = 0;
|
||||
var numItems = itemIds.Count + 1;
|
||||
|
||||
_logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
|
||||
|
||||
foreach (var itemId in itemIds)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
|
||||
if (item is not null)
|
||||
foreach (var mediaSource in item.GetMediaSources(false))
|
||||
{
|
||||
_logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
|
||||
|
||||
_libraryManager.DeleteItem(item, new DeleteOptions
|
||||
// Delete extracted data
|
||||
var mediaSourceItem = _libraryManager.GetItemById(mediaSource.Id);
|
||||
if (mediaSourceItem is null)
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var extractedDataFolders = _pathManager.GetExtractedDataPaths(mediaSourceItem);
|
||||
foreach (var folder in extractedDataFolders)
|
||||
{
|
||||
if (Directory.Exists(folder))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(folder, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning("Failed to remove {Folder}: {Exception}", folder, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= numItems;
|
||||
progress.Report(percent * 100);
|
||||
}
|
||||
|
||||
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (context.ConfigureAwait(false))
|
||||
{
|
||||
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (transaction.ConfigureAwait(false))
|
||||
// Delete item
|
||||
_libraryManager.DeleteItem(item, new DeleteOptions
|
||||
{
|
||||
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
DeleteFileLocation = false
|
||||
});
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= numItems;
|
||||
progress.Report(percent * 100);
|
||||
}
|
||||
|
||||
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (context.ConfigureAwait(false))
|
||||
{
|
||||
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (transaction.ConfigureAwait(false))
|
||||
{
|
||||
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
@ -17,7 +17,6 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Trickplay;
|
||||
@ -41,7 +40,6 @@ namespace Emby.Server.Implementations.Dto
|
||||
private readonly ILogger<DtoService> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserDataManager _userDataRepository;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IProviderManager _providerManager;
|
||||
@ -52,13 +50,12 @@ namespace Emby.Server.Implementations.Dto
|
||||
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
|
||||
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
private readonly IChapterRepository _chapterRepository;
|
||||
private readonly IChapterManager _chapterManager;
|
||||
|
||||
public DtoService(
|
||||
ILogger<DtoService> logger,
|
||||
ILibraryManager libraryManager,
|
||||
IUserDataManager userDataRepository,
|
||||
IItemRepository itemRepo,
|
||||
IImageProcessor imageProcessor,
|
||||
IProviderManager providerManager,
|
||||
IRecordingsManager recordingsManager,
|
||||
@ -66,12 +63,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
Lazy<ILiveTvManager> livetvManagerFactory,
|
||||
ITrickplayManager trickplayManager,
|
||||
IChapterRepository chapterRepository)
|
||||
IChapterManager chapterManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_userDataRepository = userDataRepository;
|
||||
_itemRepo = itemRepo;
|
||||
_imageProcessor = imageProcessor;
|
||||
_providerManager = providerManager;
|
||||
_recordingsManager = recordingsManager;
|
||||
@ -79,7 +75,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_livetvManagerFactory = livetvManagerFactory;
|
||||
_trickplayManager = trickplayManager;
|
||||
_chapterRepository = chapterRepository;
|
||||
_chapterManager = chapterManager;
|
||||
}
|
||||
|
||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||
@ -99,11 +95,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
(channelTuples ??= new()).Add((dto, tvChannel));
|
||||
(channelTuples ??= []).Add((dto, tvChannel));
|
||||
}
|
||||
else if (item is LiveTvProgram)
|
||||
{
|
||||
(programTuples ??= new()).Add((item, dto));
|
||||
(programTuples ??= []).Add((item, dto));
|
||||
}
|
||||
|
||||
if (item is IItemByName byName)
|
||||
@ -590,12 +586,12 @@ namespace Emby.Server.Implementations.Dto
|
||||
if (dto.ImageBlurHashes is not null)
|
||||
{
|
||||
// Only add BlurHash for the person's image.
|
||||
baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
|
||||
baseItemPerson.ImageBlurHashes = [];
|
||||
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
|
||||
{
|
||||
if (blurHash is not null)
|
||||
{
|
||||
baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
|
||||
baseItemPerson.ImageBlurHashes[imageType] = [];
|
||||
foreach (var (imageId, blurHashValue) in blurHash)
|
||||
{
|
||||
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
|
||||
@ -674,11 +670,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (!string.IsNullOrEmpty(image.BlurHash))
|
||||
{
|
||||
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
|
||||
dto.ImageBlurHashes ??= [];
|
||||
|
||||
if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
|
||||
{
|
||||
value = new Dictionary<string, string>();
|
||||
value = [];
|
||||
dto.ImageBlurHashes[image.Type] = value;
|
||||
}
|
||||
|
||||
@ -709,7 +705,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (hashes.Count > 0)
|
||||
{
|
||||
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
|
||||
dto.ImageBlurHashes ??= [];
|
||||
|
||||
dto.ImageBlurHashes[imageType] = hashes;
|
||||
}
|
||||
@ -756,7 +752,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.AspectRatio = hasAspectRatio.AspectRatio;
|
||||
}
|
||||
|
||||
dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
|
||||
dto.ImageBlurHashes = [];
|
||||
|
||||
var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
|
||||
if (backdropLimit > 0)
|
||||
@ -772,7 +768,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.EnableImages)
|
||||
{
|
||||
dto.ImageTags = new Dictionary<ImageType, string>();
|
||||
dto.ImageTags = [];
|
||||
|
||||
// Prevent implicitly captured closure
|
||||
var currentItem = item;
|
||||
@ -1064,12 +1060,17 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.Chapters))
|
||||
{
|
||||
dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList();
|
||||
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.Trickplay))
|
||||
{
|
||||
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
||||
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
||||
dto.Trickplay = trickplay.ToDictionary(
|
||||
mediaStream => mediaStream.Key,
|
||||
mediaStream => mediaStream.Value.ToDictionary(
|
||||
width => width.Key,
|
||||
width => new TrickplayInfoDto(width.Value)));
|
||||
}
|
||||
|
||||
dto.ExtraType = video.ExtraType;
|
||||
|
@ -18,9 +18,11 @@
|
||||
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
|
||||
<ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
|
||||
<ProjectReference Include="..\src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BitFaster.Caching" />
|
||||
<PackageReference Include="DiscUtils.Udf" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
@ -62,10 +64,14 @@
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Ignore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Localization\iso6392.txt" />
|
||||
<EmbeddedResource Include="Localization\countries.json" />
|
||||
<EmbeddedResource Include="Localization\Core\*.json" />
|
||||
<EmbeddedResource Include="Localization\Ratings\*.csv" />
|
||||
<EmbeddedResource Include="Localization\Ratings\*.json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -5,8 +5,8 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Events;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
@ -1,7 +1,8 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
|
||||
_jsonOptions = JsonDefaults.Options;
|
||||
LastActivityDate = DateTime.Now;
|
||||
LastActivityDate = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.IO
|
||||
private void ProcessPathChanges(List<string> paths)
|
||||
{
|
||||
IEnumerable<BaseItem> itemsToRefresh = paths
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Distinct()
|
||||
.Select(GetAffectedBaseItem)
|
||||
.Where(item => item is not null)
|
||||
.DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where()
|
||||
|
@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.IO
|
||||
.Where(IsLibraryMonitorEnabled)
|
||||
.OfType<Folder>()
|
||||
.SelectMany(f => f.PhysicalLocations)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Distinct()
|
||||
.Order();
|
||||
|
||||
foreach (var path in paths)
|
||||
|
@ -160,12 +160,13 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
// Cross device move requires a copy
|
||||
Directory.CreateDirectory(destination);
|
||||
foreach (string file in Directory.GetFiles(source))
|
||||
var sourceDir = new DirectoryInfo(source);
|
||||
foreach (var file in sourceDir.EnumerateFiles())
|
||||
{
|
||||
File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true);
|
||||
file.CopyTo(Path.Combine(destination, file.Name), true);
|
||||
}
|
||||
|
||||
Directory.Delete(source, true);
|
||||
sourceDir.Delete(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -541,8 +542,8 @@ namespace Emby.Server.Implementations.IO
|
||||
return DriveInfo.GetDrives()
|
||||
.Where(
|
||||
d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
|
||||
&& d.IsReady
|
||||
&& d.TotalSize != 0)
|
||||
&& d.IsReady
|
||||
&& d.TotalSize != 0)
|
||||
.Select(d => new FileSystemMetadata
|
||||
{
|
||||
Name = d.Name,
|
||||
@ -560,11 +561,23 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
|
||||
{
|
||||
return GetFiles(path, null, false, recursive);
|
||||
return GetFiles(path, "*", recursive);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, bool recursive = false)
|
||||
{
|
||||
return GetFiles(path, searchPattern, null, false, recursive);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive)
|
||||
{
|
||||
return GetFiles(path, "*", extensions, enableCaseSensitiveExtensions, recursive);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||
{
|
||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||
|
||||
@ -572,10 +585,12 @@ namespace Emby.Server.Implementations.IO
|
||||
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
||||
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
|
||||
{
|
||||
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
|
||||
searchPattern = searchPattern.EndsWith(extensions[0], StringComparison.Ordinal) ? searchPattern : searchPattern + extensions[0];
|
||||
|
||||
return ToMetadata(new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions));
|
||||
}
|
||||
|
||||
var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
|
||||
var files = new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions);
|
||||
|
||||
if (extensions is not null && extensions.Count > 0)
|
||||
{
|
||||
|
@ -43,13 +43,11 @@ namespace Emby.Server.Implementations.Images
|
||||
protected IImageProcessor ImageProcessor { get; set; }
|
||||
|
||||
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
|
||||
= new ImageType[] { ImageType.Primary };
|
||||
= [ImageType.Primary];
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Dynamic Image Provider";
|
||||
|
||||
protected virtual int MaxImageAgeDays => 7;
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
protected virtual bool Supports(BaseItem item) => true;
|
||||
@ -292,8 +290,14 @@ namespace Emby.Server.Implementations.Images
|
||||
|
||||
protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
|
||||
{
|
||||
var age = DateTime.UtcNow - image.DateModified;
|
||||
return age.TotalDays > MaxImageAgeDays;
|
||||
var path = image.Path;
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var modificationDate = FileSystem.GetLastWriteTimeUtc(path);
|
||||
return image.DateModified != modificationDate;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
if (parent is not null)
|
||||
{
|
||||
// Ignore extras folders but allow it at the collection level
|
||||
// Ignore extras for unsupported types
|
||||
if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
|
||||
&& parent is not AggregateFolder
|
||||
&& parent is not UserRootFolder)
|
||||
@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
if (parent is not null)
|
||||
{
|
||||
// Don't resolve these into audio files
|
||||
// Don't resolve theme songs
|
||||
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
|
||||
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
|
||||
{
|
||||
|
94
Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
Normal file
94
Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Emby.Server.Implementations.Library;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver rule class for ignoring files via .ignore.
|
||||
/// </summary>
|
||||
public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
{
|
||||
private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
|
||||
{
|
||||
var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore"));
|
||||
if (ignoreFile.Exists)
|
||||
{
|
||||
return ignoreFile;
|
||||
}
|
||||
|
||||
var parentDir = directory.Parent;
|
||||
if (parentDir is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return FindIgnoreFile(parentDir);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent)
|
||||
{
|
||||
return IsIgnored(fileInfo, parent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not the file is ignored.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo">The file information.</param>
|
||||
/// <param name="parent">The parent BaseItem.</param>
|
||||
/// <returns>True if the file should be ignored.</returns>
|
||||
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
|
||||
{
|
||||
if (fileInfo.IsDirectory)
|
||||
{
|
||||
var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName));
|
||||
if (dirIgnoreFile is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// ignore the directory only if the .ignore file is empty
|
||||
// evaluate individual files otherwise
|
||||
return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile));
|
||||
}
|
||||
|
||||
var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
|
||||
if (string.IsNullOrEmpty(parentDirPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var folder = new DirectoryInfo(parentDirPath);
|
||||
var ignoreFile = FindIgnoreFile(folder);
|
||||
if (ignoreFile is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string ignoreFileString = GetFileContent(ignoreFile);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ignoreFileString))
|
||||
{
|
||||
// Ignore directory if we just have the file
|
||||
return true;
|
||||
}
|
||||
|
||||
// If file has content, base ignoring off the content .gitignore-style rules
|
||||
var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var ignore = new Ignore.Ignore();
|
||||
ignore.Add(ignoreRules);
|
||||
|
||||
return ignore.IsIgnored(fileInfo.FullName);
|
||||
}
|
||||
|
||||
private static string GetFileContent(FileInfo dirIgnoreFile)
|
||||
{
|
||||
using (var reader = dirIgnoreFile.OpenText())
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
71
Emby.Server.Implementations/Library/ExternalDataManager.cs
Normal file
71
Emby.Server.Implementations/Library/ExternalDataManager.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.MediaSegments;
|
||||
using MediaBrowser.Controller.Trickplay;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library;
|
||||
|
||||
/// <summary>
|
||||
/// IExternalDataManager implementation.
|
||||
/// </summary>
|
||||
public class ExternalDataManager : IExternalDataManager
|
||||
{
|
||||
private readonly IKeyframeManager _keyframeManager;
|
||||
private readonly IMediaSegmentManager _mediaSegmentManager;
|
||||
private readonly IPathManager _pathManager;
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
private readonly ILogger<ExternalDataManager> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExternalDataManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="keyframeManager">The keyframe manager.</param>
|
||||
/// <param name="mediaSegmentManager">The media segment manager.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
/// <param name="trickplayManager">The trickplay manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ExternalDataManager(
|
||||
IKeyframeManager keyframeManager,
|
||||
IMediaSegmentManager mediaSegmentManager,
|
||||
IPathManager pathManager,
|
||||
ITrickplayManager trickplayManager,
|
||||
ILogger<ExternalDataManager> logger)
|
||||
{
|
||||
_keyframeManager = keyframeManager;
|
||||
_mediaSegmentManager = mediaSegmentManager;
|
||||
_pathManager = pathManager;
|
||||
_trickplayManager = trickplayManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var validPaths = _pathManager.GetExtractedDataPaths(item).Where(Directory.Exists).ToList();
|
||||
var itemId = item.Id;
|
||||
if (validPaths.Count > 0)
|
||||
{
|
||||
foreach (var path in validPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Unable to prune external item data at {Path}: {Exception}", path, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using DotNet.Globbing;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
|
44
Emby.Server.Implementations/Library/KeyframeManager.cs
Normal file
44
Emby.Server.Implementations/Library/KeyframeManager.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.MediaEncoding.Keyframes;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
|
||||
namespace Emby.Server.Implementations.Library;
|
||||
|
||||
/// <summary>
|
||||
/// Manager for Keyframe data.
|
||||
/// </summary>
|
||||
public class KeyframeManager : IKeyframeManager
|
||||
{
|
||||
private readonly IKeyframeRepository _repository;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeyframeManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="repository">The keyframe repository.</param>
|
||||
public KeyframeManager(IKeyframeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId)
|
||||
{
|
||||
return _repository.GetKeyframeData(itemId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken)
|
||||
{
|
||||
await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -13,8 +13,10 @@ using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
@ -425,6 +427,7 @@ namespace Emby.Server.Implementations.Library
|
||||
if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
|
||||
{
|
||||
source.DefaultAudioStreamIndex = index;
|
||||
source.DefaultAudioIndexSource = AudioIndexSource.User;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -432,6 +435,15 @@ namespace Emby.Server.Implementations.Library
|
||||
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
|
||||
|
||||
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
|
||||
if (user.PlayDefaultAudioTrack)
|
||||
{
|
||||
source.DefaultAudioIndexSource |= AudioIndexSource.Default;
|
||||
}
|
||||
|
||||
if (preferredAudio.Count > 0)
|
||||
{
|
||||
source.DefaultAudioIndexSource |= AudioIndexSource.Language;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
|
||||
@ -669,17 +681,17 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
mediaInfo = await _mediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||
ExtractChapters = false
|
||||
},
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||
ExtractChapters = false
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cacheFilePath is not null)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
||||
FileStream createStream = File.Create(cacheFilePath);
|
||||
FileStream createStream = AsyncFile.Create(cacheFilePath);
|
||||
await using (createStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
@ -782,9 +794,13 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(id);
|
||||
|
||||
// TODO probably shouldn't throw here but it is kept for "backwards compatibility"
|
||||
var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
|
||||
return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
|
||||
var info = GetLiveStreamInfo(id);
|
||||
if (info is null)
|
||||
{
|
||||
return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(null, null));
|
||||
}
|
||||
|
||||
return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
|
||||
}
|
||||
|
||||
public ILiveStream GetLiveStreamInfo(string id)
|
||||
|
@ -3,7 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sort in the following order: Default > No tag > Forced
|
||||
var sortedStreams = streams
|
||||
.Where(i => i.Type == MediaStreamType.Subtitle)
|
||||
.OrderByDescending(x => x.IsExternal)
|
||||
.ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
.ThenByDescending(x => x.IsForced)
|
||||
.ThenByDescending(x => x.IsDefault)
|
||||
.ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase))
|
||||
.ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
|
||||
.ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
|
||||
.ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language))
|
||||
.ThenByDescending(x => x.IsForced)
|
||||
.ToList();
|
||||
|
||||
MediaStream? stream = null;
|
||||
|
||||
if (mode == SubtitlePlaybackMode.Default)
|
||||
{
|
||||
// Load subtitles according to external, forced and default flags.
|
||||
stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
|
||||
// Load subtitles according to external, default and forced flags.
|
||||
stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced);
|
||||
}
|
||||
else if (mode == SubtitlePlaybackMode.Smart)
|
||||
{
|
||||
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
|
||||
// If no subtitles of preferred language available, use default behaviour.
|
||||
// If no subtitles of preferred language available, use none.
|
||||
// If the audio language is one of the user's preferred subtitle languages behave like OnlyForced.
|
||||
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
|
||||
sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
|
||||
stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Respect forced flag.
|
||||
stream = sortedStreams.FirstOrDefault(x => x.IsForced);
|
||||
stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
else if (mode == SubtitlePlaybackMode.Always)
|
||||
{
|
||||
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour.
|
||||
stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
|
||||
sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
|
||||
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour.
|
||||
stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ??
|
||||
BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
|
||||
}
|
||||
else if (mode == SubtitlePlaybackMode.OnlyForced)
|
||||
{
|
||||
// Only load subtitles that are flagged forced.
|
||||
stream = sortedStreams.FirstOrDefault(x => x.IsForced);
|
||||
// Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
|
||||
stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
|
||||
}
|
||||
|
||||
return stream?.Index;
|
||||
@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library
|
||||
if (mode == SubtitlePlaybackMode.Default)
|
||||
{
|
||||
// Prefer embedded metadata over smart logic
|
||||
filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault)
|
||||
// Load subtitles according to external, default, and forced flags.
|
||||
filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced)
|
||||
.ToList();
|
||||
}
|
||||
else if (mode == SubtitlePlaybackMode.Smart)
|
||||
{
|
||||
// Prefer smart logic over embedded metadata
|
||||
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior.
|
||||
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
|
||||
filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
|
||||
}
|
||||
}
|
||||
else if (mode == SubtitlePlaybackMode.Always)
|
||||
{
|
||||
// Always load the most suitable full subtitles
|
||||
filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
|
||||
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior.
|
||||
filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages))
|
||||
.ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages);
|
||||
}
|
||||
else if (mode == SubtitlePlaybackMode.OnlyForced)
|
||||
{
|
||||
// Always load the most suitable full subtitles
|
||||
filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
|
||||
// Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
|
||||
filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
|
||||
}
|
||||
|
||||
// Load forced subs if we have found no suitable full subtitles
|
||||
var iterStreams = filteredStreams is null || filteredStreams.Count == 0
|
||||
? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
: filteredStreams;
|
||||
// If filteredStreams is null, initialize it as an empty list to avoid null reference errors
|
||||
filteredStreams ??= new List<MediaStream>();
|
||||
|
||||
foreach (var stream in iterStreams)
|
||||
foreach (var stream in filteredStreams)
|
||||
{
|
||||
stream.Score = GetStreamScore(stream, preferredLanguages);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> preferredLanguages)
|
||||
{
|
||||
// If preferredLanguages is empty, treat it as "any language" (wildcard)
|
||||
return preferredLanguages.Count == 0 ||
|
||||
preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsLanguageUndefined(string language)
|
||||
{
|
||||
// Check for null, empty, or known placeholders
|
||||
return string.IsNullOrEmpty(language) ||
|
||||
language.Equals("und", StringComparison.OrdinalIgnoreCase) ||
|
||||
language.Equals("unknown", StringComparison.OrdinalIgnoreCase) ||
|
||||
language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) ||
|
||||
language.Equals("mul", StringComparison.OrdinalIgnoreCase) ||
|
||||
language.Equals("zxx", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static List<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> preferredLanguages)
|
||||
{
|
||||
return sortedStreams
|
||||
.Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language)))
|
||||
.OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
|
||||
.ThenByDescending(s => IsLanguageUndefined(s.Language))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
|
||||
{
|
||||
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
|
||||
|
@ -4,8 +4,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
101
Emby.Server.Implementations/Library/PathManager.cs
Normal file
101
Emby.Server.Implementations/Library/PathManager.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
|
||||
namespace Emby.Server.Implementations.Library;
|
||||
|
||||
/// <summary>
|
||||
/// IPathManager implementation.
|
||||
/// </summary>
|
||||
public class PathManager : IPathManager
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The server configuration manager.</param>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
public PathManager(
|
||||
IServerConfigurationManager config,
|
||||
IApplicationPaths appPaths)
|
||||
{
|
||||
_config = config;
|
||||
_appPaths = appPaths;
|
||||
}
|
||||
|
||||
private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
|
||||
|
||||
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetAttachmentPath(string mediaSourceId, string fileName)
|
||||
{
|
||||
return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetAttachmentFolderPath(string mediaSourceId)
|
||||
{
|
||||
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
|
||||
|
||||
return Path.Join(AttachmentCachePath, id[..2], id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetSubtitleFolderPath(string mediaSourceId)
|
||||
{
|
||||
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
|
||||
|
||||
return Path.Join(SubtitleCachePath, id[..2], id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
|
||||
{
|
||||
return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
|
||||
{
|
||||
var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan();
|
||||
|
||||
return saveWithMedia
|
||||
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(Path.GetFileName(item.Path), ".trickplay"))
|
||||
: Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetChapterImageFolderPath(BaseItem item)
|
||||
{
|
||||
return Path.Combine(item.GetInternalMetadataPath(), "chapters");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetChapterImagePath(BaseItem item, long chapterPositionTicks)
|
||||
{
|
||||
var filename = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg";
|
||||
|
||||
return Path.Combine(GetChapterImageFolderPath(item), filename);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
|
||||
{
|
||||
var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
return [
|
||||
GetAttachmentFolderPath(mediaSourceId),
|
||||
GetSubtitleFolderPath(mediaSourceId),
|
||||
GetTrickplayDirectory(item, false),
|
||||
GetTrickplayDirectory(item, true),
|
||||
GetChapterImageFolderPath(item)
|
||||
];
|
||||
}
|
||||
}
|
@ -136,23 +136,33 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (config.UseFileCreationTimeForDateAdded)
|
||||
{
|
||||
// directoryService.getFile may return null
|
||||
if (info is not null)
|
||||
var fileCreationDate = info?.CreationTimeUtc;
|
||||
if (fileCreationDate is not null)
|
||||
{
|
||||
var dateCreated = info.CreationTimeUtc;
|
||||
|
||||
if (dateCreated.Equals(DateTime.MinValue))
|
||||
var dateCreated = fileCreationDate;
|
||||
if (dateCreated == DateTime.MinValue)
|
||||
{
|
||||
dateCreated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
item.DateCreated = dateCreated;
|
||||
item.DateCreated = dateCreated.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
item.DateCreated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (info is not null && !info.IsDirectory)
|
||||
{
|
||||
item.Size = info.Length;
|
||||
}
|
||||
|
||||
var fileModificationDate = info?.LastWriteTimeUtc;
|
||||
if (fileModificationDate.HasValue)
|
||||
{
|
||||
item.DateModified = fileModificationDate.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
_ => _videoResolvers
|
||||
};
|
||||
|
||||
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
|
||||
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
|
||||
{
|
||||
var extraResult = GetExtraInfo(path, _namingOptions);
|
||||
var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
|
||||
if (extraResult.ExtraType is null)
|
||||
{
|
||||
extraType = null;
|
||||
|
@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
}
|
||||
|
||||
var videoInfos = files
|
||||
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
|
||||
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath))
|
||||
.Where(f => f is not null)
|
||||
.ToList();
|
||||
|
||||
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
|
||||
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
|
||||
|
||||
var result = new MultiItemResolverResult
|
||||
{
|
||||
@ -456,12 +456,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
{
|
||||
var videoPath = result.Items[0].Path;
|
||||
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
|
||||
var hasOtherSubfolders = multiDiscFolders.Count > 0;
|
||||
|
||||
if (!hasPhotos)
|
||||
if (!hasPhotos && !hasOtherSubfolders)
|
||||
{
|
||||
var movie = (T)result.Items[0];
|
||||
movie.IsInMixedFolder = false;
|
||||
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
|
||||
if (collectionType == CollectionType.movies || collectionType is null)
|
||||
{
|
||||
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
|
||||
}
|
||||
|
||||
return movie;
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
|
||||
var path = args.Path;
|
||||
|
||||
var seasonParserResult = SeasonPathParser.Parse(path, true, true);
|
||||
var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true);
|
||||
|
||||
var season = new Season
|
||||
{
|
||||
|
@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
{
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
if (IsSeasonFolder(child.FullName, isTvContentType))
|
||||
if (IsSeasonFolder(child.FullName, path, isTvContentType))
|
||||
{
|
||||
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
|
||||
return true;
|
||||
@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
/// Determines whether [is season folder] [the specified path].
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="parentPath">The parentpath.</param>
|
||||
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
|
||||
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
|
||||
private static bool IsSeasonFolder(string path, bool isTvContentType)
|
||||
private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType)
|
||||
{
|
||||
var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
|
||||
var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber;
|
||||
|
||||
return seasonNumber.HasValue;
|
||||
}
|
||||
|
@ -3,8 +3,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -4,13 +4,13 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library;
|
||||
@ -77,15 +77,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
|
||||
CollapseBoxSetItems = false,
|
||||
Recursive = true,
|
||||
DtoOptions = new DtoOptions(false),
|
||||
ImageTypes = new[] { imageType },
|
||||
ImageTypes = [imageType],
|
||||
Limit = 30,
|
||||
// TODO max parental rating configurable
|
||||
MaxParentalRating = 10,
|
||||
OrderBy = new[]
|
||||
{
|
||||
MaxParentalRating = new(10, null),
|
||||
OrderBy =
|
||||
[
|
||||
(ItemSortBy.Random, SortOrder.Ascending)
|
||||
},
|
||||
IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series }
|
||||
],
|
||||
IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using BitFaster.Caching.Lru;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -26,11 +25,9 @@ namespace Emby.Server.Implementations.Library
|
||||
/// </summary>
|
||||
public class UserDataManager : IUserDataManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, UserItemData> _userData =
|
||||
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _repository;
|
||||
private readonly FastConcurrentLru<string, UserItemData> _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
|
||||
@ -43,6 +40,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
_config = config;
|
||||
_repository = repository;
|
||||
_cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var userId = user.InternalId;
|
||||
var cacheKey = GetCacheKey(userId, item.Id);
|
||||
_userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
|
||||
_cache.AddOrUpdate(cacheKey, userData);
|
||||
|
||||
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
|
||||
{
|
||||
@ -182,7 +180,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var cacheKey = GetCacheKey(user.InternalId, itemId);
|
||||
|
||||
if (_userData.TryGetValue(cacheKey, out var data))
|
||||
if (_cache.TryGet(cacheKey, out var data))
|
||||
{
|
||||
return data;
|
||||
}
|
||||
@ -197,7 +195,7 @@ namespace Emby.Server.Implementations.Library
|
||||
};
|
||||
}
|
||||
|
||||
return _userData.GetOrAdd(cacheKey, data);
|
||||
return _cache.GetOrAdd(cacheKey, _ => data);
|
||||
}
|
||||
|
||||
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
|
||||
|
@ -6,8 +6,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@ -370,6 +372,21 @@ namespace Emby.Server.Implementations.Library
|
||||
MediaTypes = mediaTypes
|
||||
};
|
||||
|
||||
if (request.GroupItems)
|
||||
{
|
||||
if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.tvshows))
|
||||
{
|
||||
query.Limit = limit;
|
||||
return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows);
|
||||
}
|
||||
|
||||
if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.music))
|
||||
{
|
||||
query.Limit = limit;
|
||||
return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
|
||||
}
|
||||
}
|
||||
|
||||
return _libraryManager.GetItemList(query, parents);
|
||||
}
|
||||
}
|
||||
|
@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Class ArtistsPostScanTask.
|
||||
/// </summary>
|
||||
public class ArtistsPostScanTask : ILibraryPostScanTask
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ArtistsPostScanTask.
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
public class ArtistsPostScanTask : ILibraryPostScanTask
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<ArtistsValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public ArtistsPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<ArtistsValidator> logger,
|
||||
IItemRepository itemRepo)
|
||||
{
|
||||
/// <summary>
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<ArtistsValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public ArtistsPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<ArtistsValidator> logger,
|
||||
IItemRepository itemRepo)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||
}
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -10,102 +10,101 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Class ArtistsValidator.
|
||||
/// </summary>
|
||||
public class ArtistsValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ArtistsValidator.
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
public class ArtistsValidator
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<ArtistsValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<ArtistsValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetAllArtistNames();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetAllArtistNames();
|
||||
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
|
||||
foreach (var name in names)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = _libraryManager.GetArtist(name);
|
||||
var item = _libraryManager.GetArtist(name);
|
||||
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't clutter the log
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't clutter the log
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
|
||||
}
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
}
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.MusicArtist],
|
||||
IsDeadArtist = true,
|
||||
IsLocked = false
|
||||
}).Cast<MusicArtist>().ToList();
|
||||
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
if (!item.IsAccessedByName)
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
|
||||
IsDeadArtist = true,
|
||||
IsLocked = false
|
||||
}).Cast<MusicArtist>().ToList();
|
||||
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
if (!item.IsAccessedByName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
|
||||
|
||||
_libraryManager.DeleteItem(
|
||||
item,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
},
|
||||
false);
|
||||
continue;
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
@ -4,153 +4,150 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Class CollectionPostScanTask.
|
||||
/// </summary>
|
||||
public class CollectionPostScanTask : ILibraryPostScanTask
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ICollectionManager _collectionManager;
|
||||
private readonly ILogger<CollectionPostScanTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Class CollectionPostScanTask.
|
||||
/// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
|
||||
/// </summary>
|
||||
public class CollectionPostScanTask : ILibraryPostScanTask
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="collectionManager">The collection manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public CollectionPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ICollectionManager collectionManager,
|
||||
ILogger<CollectionPostScanTask> logger)
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ICollectionManager _collectionManager;
|
||||
private readonly ILogger<CollectionPostScanTask> _logger;
|
||||
_libraryManager = libraryManager;
|
||||
_collectionManager = collectionManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="collectionManager">The collection manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public CollectionPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ICollectionManager collectionManager,
|
||||
ILogger<CollectionPostScanTask> logger)
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
|
||||
|
||||
foreach (var library in _libraryManager.RootFolder.Children)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_collectionManager = collectionManager;
|
||||
_logger = logger;
|
||||
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var startIndex = 0;
|
||||
var pagesize = 1000;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var movies = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = [MediaType.Video],
|
||||
IncludeItemTypes = [BaseItemKind.Movie],
|
||||
IsVirtualItem = false,
|
||||
OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)],
|
||||
Parent = library,
|
||||
StartIndex = startIndex,
|
||||
Limit = pagesize,
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
foreach (var m in movies)
|
||||
{
|
||||
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
|
||||
{
|
||||
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
|
||||
{
|
||||
movieList.Add(movie.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (movies.Count < pagesize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
startIndex += pagesize;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
var numComplete = 0;
|
||||
var count = collectionNameMoviesMap.Count;
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
|
||||
|
||||
foreach (var library in _libraryManager.RootFolder.Children)
|
||||
{
|
||||
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var startIndex = 0;
|
||||
var pagesize = 1000;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var movies = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = new[] { MediaType.Video },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Movie },
|
||||
IsVirtualItem = false,
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||
Parent = library,
|
||||
StartIndex = startIndex,
|
||||
Limit = pagesize,
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
foreach (var m in movies)
|
||||
{
|
||||
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
|
||||
{
|
||||
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
|
||||
{
|
||||
movieList.Add(movie.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (movies.Count < pagesize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
startIndex += pagesize;
|
||||
}
|
||||
}
|
||||
|
||||
var numComplete = 0;
|
||||
var count = collectionNameMoviesMap.Count;
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
progress.Report(100);
|
||||
return;
|
||||
}
|
||||
|
||||
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.BoxSet },
|
||||
CollapseBoxSetItems = false,
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
|
||||
{
|
||||
try
|
||||
{
|
||||
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
|
||||
if (boxSet is null)
|
||||
{
|
||||
// won't automatically create collection if only one movie in it
|
||||
if (movieIds.Count >= 2)
|
||||
{
|
||||
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
||||
{
|
||||
Name = collectionName,
|
||||
IsLocked = true
|
||||
});
|
||||
|
||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
return;
|
||||
}
|
||||
|
||||
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.BoxSet],
|
||||
CollapseBoxSetItems = false,
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
|
||||
{
|
||||
try
|
||||
{
|
||||
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
|
||||
if (boxSet is null)
|
||||
{
|
||||
// won't automatically create collection if only one movie in it
|
||||
if (movieIds.Count >= 2)
|
||||
{
|
||||
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
||||
{
|
||||
Name = collectionName,
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
|
@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Class GenresPostScanTask.
|
||||
/// </summary>
|
||||
public class GenresPostScanTask : ILibraryPostScanTask
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GenresPostScanTask.
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
public class GenresPostScanTask : ILibraryPostScanTask
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<GenresValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public GenresPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<GenresValidator> logger,
|
||||
IItemRepository itemRepo)
|
||||
{
|
||||
/// <summary>
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<GenresValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public GenresPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<GenresValidator> logger,
|
||||
IItemRepository itemRepo)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||
}
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -1,81 +1,103 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Class GenresValidator.
|
||||
/// </summary>
|
||||
public class GenresValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GenresValidator.
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
public class GenresValidator
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<GenresValidator> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenresValidator"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<GenresValidator> _logger;
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetGenreNames();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenresValidator"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetGenreNames();
|
||||
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
|
||||
foreach (var name in names)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = _libraryManager.GetGenre(name);
|
||||
var item = _libraryManager.GetGenre(name);
|
||||
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't clutter the log
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {GenreName}", name);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't clutter the log
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {GenreName}", name);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
}
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
|
||||
IsDeadGenre = true,
|
||||
IsLocked = false
|
||||
});
|
||||
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Class MusicGenresPostScanTask.
|
||||
/// </summary>
|
||||
public class MusicGenresPostScanTask : ILibraryPostScanTask
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MusicGenresPostScanTask.
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
public class MusicGenresPostScanTask : ILibraryPostScanTask
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<MusicGenresValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public MusicGenresPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<MusicGenresValidator> logger,
|
||||
IItemRepository itemRepo)
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<MusicGenresValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public MusicGenresPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<MusicGenresValidator> logger,
|
||||
IItemRepository itemRepo)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||
}
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -5,77 +5,76 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Class MusicGenresValidator.
|
||||
/// </summary>
|
||||
public class MusicGenresValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MusicGenresValidator.
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
public class MusicGenresValidator
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<MusicGenresValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<MusicGenresValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetMusicGenreNames();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetMusicGenreNames();
|
||||
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
|
||||
foreach (var name in names)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = _libraryManager.GetMusicGenre(name);
|
||||
var item = _libraryManager.GetMusicGenre(name);
|
||||
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't clutter the log
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {GenreName}", name);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't clutter the log
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {GenreName}", name);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
|
@ -9,119 +9,114 @@ using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Class PeopleValidator.
|
||||
/// </summary>
|
||||
public class PeopleValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Class PeopleValidator.
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
public class PeopleValidator
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// The _logger.
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PeopleValidator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
|
||||
{
|
||||
/// <summary>
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _logger.
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
/// <summary>
|
||||
/// Validates the people.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
var numComplete = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PeopleValidator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
|
||||
var numPeople = people.Count;
|
||||
|
||||
_logger.LogDebug("Will refresh {Amount} people", numPeople);
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
/// <summary>
|
||||
/// Validates the people.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
|
||||
|
||||
var numComplete = 0;
|
||||
|
||||
var numPeople = people.Count;
|
||||
|
||||
_logger.LogDebug("Will refresh {0} people", numPeople);
|
||||
|
||||
foreach (var person in people)
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
var item = _libraryManager.GetPerson(person);
|
||||
if (item is null)
|
||||
{
|
||||
var item = _libraryManager.GetPerson(person);
|
||||
if (item is null)
|
||||
{
|
||||
_logger.LogWarning("Failed to get person: {Name}", person);
|
||||
continue;
|
||||
}
|
||||
|
||||
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
|
||||
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
|
||||
};
|
||||
|
||||
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error validating IBN entry {Person}", person);
|
||||
_logger.LogWarning("Failed to get person: {Name}", person);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update progress
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= numPeople;
|
||||
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
|
||||
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
|
||||
};
|
||||
|
||||
progress.Report(100 * percent);
|
||||
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error validating IBN entry {Person}", person);
|
||||
}
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Person],
|
||||
IsDeadPerson = true,
|
||||
IsLocked = false
|
||||
});
|
||||
// Update progress
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= numPeople;
|
||||
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Deleting dead {2} {0} {1}.",
|
||||
item.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
item.Name,
|
||||
item.GetType().Name);
|
||||
|
||||
_libraryManager.DeleteItem(
|
||||
item,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
|
||||
_logger.LogInformation("People validation complete");
|
||||
progress.Report(100 * percent);
|
||||
}
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Person],
|
||||
IsDeadPerson = true,
|
||||
IsLocked = false
|
||||
});
|
||||
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
_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);
|
||||
|
||||
_logger.LogInformation("People validation complete");
|
||||
}
|
||||
}
|
||||
|
@ -5,46 +5,45 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Class MusicGenresPostScanTask.
|
||||
/// </summary>
|
||||
public class StudiosPostScanTask : ILibraryPostScanTask
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MusicGenresPostScanTask.
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
public class StudiosPostScanTask : ILibraryPostScanTask
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
private readonly ILogger<StudiosValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public StudiosPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<StudiosValidator> logger,
|
||||
IItemRepository itemRepo)
|
||||
{
|
||||
/// <summary>
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
private readonly ILogger<StudiosValidator> _logger;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public StudiosPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<StudiosValidator> logger,
|
||||
IItemRepository itemRepo)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||
}
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -8,98 +8,97 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Class StudiosValidator.
|
||||
/// </summary>
|
||||
public class StudiosValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Class StudiosValidator.
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
public class StudiosValidator
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<StudiosValidator> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StudiosValidator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
private readonly IItemRepository _itemRepo;
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetStudioNames();
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<StudiosValidator> _logger;
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StudiosValidator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="itemRepo">The item repository.</param>
|
||||
public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
|
||||
foreach (var name in names)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetStudioNames();
|
||||
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
|
||||
foreach (var name in names)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = _libraryManager.GetStudio(name);
|
||||
var item = _libraryManager.GetStudio(name);
|
||||
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't clutter the log
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {StudioName}", name);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't clutter the log
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {StudioName}", name);
|
||||
}
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Studio },
|
||||
IsDeadStudio = true,
|
||||
IsLocked = false
|
||||
});
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
_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(percent);
|
||||
}
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Studio],
|
||||
IsDeadStudio = true,
|
||||
IsLocked = false
|
||||
});
|
||||
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
@ -129,5 +129,11 @@
|
||||
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
|
||||
"TaskAudioNormalization": "Odio Normalisering",
|
||||
"TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.",
|
||||
"TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
|
||||
"TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
|
||||
"TaskExtractMediaSegments": "Media Segment Skandeer",
|
||||
"TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
|
||||
"TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
|
||||
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
|
||||
}
|
||||
|
@ -125,8 +125,8 @@
|
||||
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
|
||||
"External": "خارجي",
|
||||
"HearingImpaired": "ضعاف السمع",
|
||||
"TaskRefreshTrickplayImages": "توليد صور Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
|
||||
"TaskRefreshTrickplayImages": "توليد صور المعاينة السريعة",
|
||||
"TaskRefreshTrickplayImagesDescription": "يُولّد معاينات تنقل سريع لمقاطع الفيديو ضمن المكتبات المفعّلة.",
|
||||
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
|
||||
"TaskAudioNormalization": "تسوية الصوت",
|
||||
@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
|
||||
"TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
|
||||
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
|
||||
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة."
|
||||
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
|
||||
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
|
||||
"CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"Sync": "Сінхранізаваць",
|
||||
"Playlists": "Плэйлісты",
|
||||
"Playlists": "Спісы прайгравання",
|
||||
"Latest": "Апошні",
|
||||
"LabelIpAddressValue": "IP-адрас: {0}",
|
||||
"ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
|
||||
@ -16,7 +16,7 @@
|
||||
"Collections": "Калекцыі",
|
||||
"Default": "Па змаўчанні",
|
||||
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
|
||||
"Folders": "Папкі",
|
||||
"Folders": "Тэчкі",
|
||||
"Favorites": "Абранае",
|
||||
"External": "Знешні",
|
||||
"Genres": "Жанры",
|
||||
@ -135,5 +135,7 @@
|
||||
"TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень",
|
||||
"TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень",
|
||||
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
|
||||
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay"
|
||||
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
|
||||
"CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
|
||||
"CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
|
||||
}
|
||||
|
@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
|
||||
"TaskMoveTrickplayImages": "Мигриране на Локацията за Trickplay изображения",
|
||||
"TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
|
||||
"TaskExtractMediaSegments": "Сканиране за сегменти"
|
||||
"TaskExtractMediaSegments": "Сканиране за сегменти",
|
||||
"CleanupUserDataTask": "Задача за почистване на потребителски данни",
|
||||
"CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни."
|
||||
}
|
||||
|
@ -6,29 +6,29 @@
|
||||
"Channels": "চ্যানেলসমূহ",
|
||||
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
|
||||
"Books": "পুস্তকসমূহ",
|
||||
"AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
|
||||
"AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন",
|
||||
"Artists": "শিল্পীগণ",
|
||||
"Application": "অ্যাপ্লিকেশন",
|
||||
"Albums": "অ্যালবামসমূহ",
|
||||
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
|
||||
"HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো",
|
||||
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
|
||||
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
|
||||
"HeaderContinueWatching": "দেখতে থাকুন",
|
||||
"HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
|
||||
"Genres": "শৈলীধারাসমূহ",
|
||||
"Genres": "জনরা",
|
||||
"Folders": "ফোল্ডারসমূহ",
|
||||
"Favorites": "পছন্দসমূহ",
|
||||
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
|
||||
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
|
||||
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}",
|
||||
"VersionNumber": "সংস্করণ {0}",
|
||||
"ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
|
||||
"UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
|
||||
"UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
|
||||
"UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}",
|
||||
"UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}",
|
||||
"UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে",
|
||||
"UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে",
|
||||
"UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন",
|
||||
"UserOfflineFromDevice": "{0} {1} থেকে বিযুক্ত হয়ে গেছে",
|
||||
"UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে",
|
||||
"UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে",
|
||||
"UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না",
|
||||
"UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে",
|
||||
"UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে",
|
||||
@ -36,8 +36,8 @@
|
||||
"User": "ব্যবহারকারী",
|
||||
"TvShows": "টিভি শোগুলো",
|
||||
"System": "সিস্টেম",
|
||||
"Sync": "সমলয় স্থাপন",
|
||||
"SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
|
||||
"Sync": "সমন্বয় করুন",
|
||||
"SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে",
|
||||
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
|
||||
"Songs": "সঙ্গীতসমূহ",
|
||||
"Shows": "টিভি পর্ব",
|
||||
@ -46,18 +46,18 @@
|
||||
"ScheduledTaskFailedWithName": "{0} ব্যর্থ",
|
||||
"ProviderValue": "প্রদানকারী: {0}",
|
||||
"PluginUpdatedWithName": "{0} আপডেট করা হয়েছে",
|
||||
"PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
|
||||
"PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
|
||||
"PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে",
|
||||
"PluginInstalledWithName": "{0} ইন্সটল হয়েছে",
|
||||
"Plugin": "প্লাগিন",
|
||||
"Playlists": "প্লে লিস্ট সমূহ",
|
||||
"Photos": "চিত্রসমূহ",
|
||||
"NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
|
||||
"NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
|
||||
"Photos": "ছবিসমূহ",
|
||||
"NotificationOptionVideoPlaybackStopped": "ভিডিও বন্ধ হয়েছে",
|
||||
"NotificationOptionVideoPlayback": "ভিডিও শুরু হয়েছে",
|
||||
"NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
|
||||
"NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ",
|
||||
"NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট বাধ্যতামূলক",
|
||||
"NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল করা হয়েছে",
|
||||
"NotificationOptionPluginUninstalled": "প্লাগিন বাদ দেয়া হয়েছে",
|
||||
"NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট করা লাগবে",
|
||||
"NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল হয়েছে",
|
||||
"NotificationOptionPluginUninstalled": "প্লাগিন আনইনষ্টল হয়েছে",
|
||||
"NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে",
|
||||
"NotificationOptionPluginError": "প্লাগিন ব্যর্থ",
|
||||
"NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে",
|
||||
@ -76,8 +76,8 @@
|
||||
"Movies": "চলচ্চিত্রসমূহ",
|
||||
"MixedContent": "মিশ্র কন্টেন্ট",
|
||||
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
|
||||
"HeaderRecordingGroups": "রেকর্ডিং দল",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
|
||||
"HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে",
|
||||
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
|
||||
"MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
|
||||
"Latest": "সর্বশেষ",
|
||||
@ -85,51 +85,57 @@
|
||||
"LabelIpAddressValue": "আইপি এড্রেস: {0}",
|
||||
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
|
||||
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
|
||||
"Inherit": "থেকে পাওয়া",
|
||||
"Inherit": "মূল থেকে গ্রহণ করুন",
|
||||
"HomeVideos": "হোম ভিডিও",
|
||||
"HeaderNextUp": "এরপরে আসছে",
|
||||
"HeaderLiveTV": "লাইভ টিভি",
|
||||
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
|
||||
"HeaderFavoriteShows": "প্রিয় শোগুলো",
|
||||
"TasksLibraryCategory": "গ্রন্থাগার",
|
||||
"TasksLibraryCategory": "লাইব্রেরি",
|
||||
"TasksMaintenanceCategory": "রক্ষণাবেক্ষণ",
|
||||
"TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি",
|
||||
"TaskRefreshChapterImagesDescription": "অধ্যায়গুলিতে থাকা ভিডিওগুলির জন্য থাম্বনেইল তৈরি ।",
|
||||
"TaskRefreshChapterImages": "অধ্যায়ের চিত্রগুলি বের করুন",
|
||||
"TaskCleanCacheDescription": "সিস্টেমে আর প্রয়োজন নেই ক্যাশ, ফাইলগুলি মুছে ফেলুন।",
|
||||
"TaskRefreshChapterImagesDescription": "যেসব ভিডিওতে চ্যাপ্টার রয়েছে, তাদের জন্য থাম্বনেইল তৈরি করবে।",
|
||||
"TaskRefreshChapterImages": "চ্যাপ্টার ইমেজ বের করুন",
|
||||
"TaskCleanCacheDescription": "সিস্টেমের অপ্রয়োজনীয় ক্যাশ ফাইলগুলো মুছে ফেলবে।",
|
||||
"TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি",
|
||||
"TasksChannelsCategory": "ইন্টারনেট চ্যানেল",
|
||||
"TasksApplicationCategory": "আবেদন",
|
||||
"TasksApplicationCategory": "অ্যাপ্লিকেশন",
|
||||
"TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।",
|
||||
"TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন",
|
||||
"TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।",
|
||||
"TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন",
|
||||
"TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলুন।",
|
||||
"TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলবে।",
|
||||
"TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন",
|
||||
"TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।",
|
||||
"TaskUpdatePlugins": "প্লাগইন আপডেট করুন",
|
||||
"TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করুন।",
|
||||
"TaskRefreshPeople": "পিপল রিফ্রেশ করুন",
|
||||
"TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।",
|
||||
"TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন",
|
||||
"TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।",
|
||||
"TaskUpdatePlugins": "আপডেট প্লাগইন",
|
||||
"TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করবে।",
|
||||
"TaskRefreshPeople": "ব্যক্তিদের তথ্য রিফ্রেশ",
|
||||
"TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলবে।",
|
||||
"TaskCleanLogs": "ক্লিন লগ ডিরেক্টরি",
|
||||
"TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করবে।",
|
||||
"Undefined": "অসঙ্গায়িত",
|
||||
"Forced": "জোরকরে",
|
||||
"TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
|
||||
"TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
|
||||
"TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের অ্যাক্টিভিটি লগ মুছে দিবে।",
|
||||
"TaskCleanActivityLog": "অ্যাক্টিভিটি লগ মুছুন",
|
||||
"Default": "ডিফল্ট",
|
||||
"HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
|
||||
"HearingImpaired": "শ্রবণ প্রতিবন্ধী",
|
||||
"TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
|
||||
"External": "বাহ্যিক",
|
||||
"TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
|
||||
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
|
||||
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
|
||||
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন",
|
||||
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
|
||||
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
|
||||
"TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে",
|
||||
"TaskCleanCollectionsAndPlaylists": "সংগ্রহ এবং প্লেলিস্ট পরিষ্কার করুন",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "সংগ্রহ এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
|
||||
"TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
|
||||
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
|
||||
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন"
|
||||
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্রিয় প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
|
||||
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
|
||||
"TaskMoveTrickplayImagesDescription": "লাইব্রেরির সেটিং অনুযায়ী বিদ্যমান ট্রিকপ্লে ফাইলগুলো সরিয়ে নেবে।",
|
||||
"TaskAudioNormalizationDescription": "অডিও নর্মালাইজেশন তথ্যের জন্য ফাইল স্ক্যান করবে।",
|
||||
"CleanupUserDataTaskDescription": "৯০ দিন বা তার বেশি সময় ধরে অনুপস্থিত মিডিয়া থেকে সকল ব্যবহারকারীর ডেটা (ওয়াচ স্টেট, ফেভারিট স্ট্যাটাস ইত্যাদি) মুছে ফেলবে।",
|
||||
"TaskMoveTrickplayImages": "ট্রিকপ্লে ইমেজের অবস্থান পরিবর্তন",
|
||||
"TaskAudioNormalization": "অডিও নর্মলাইজেশন",
|
||||
"CleanupUserDataTask": "ব্যবহারকারীর ডেটা পরিষ্কারের কাজ"
|
||||
}
|
||||
|
@ -13,10 +13,10 @@
|
||||
"DeviceOnlineWithName": "{0} està connectat",
|
||||
"FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
|
||||
"Favorites": "Preferits",
|
||||
"Folders": "Carpetes",
|
||||
"Folders": "Directoris",
|
||||
"Genres": "Gèneres",
|
||||
"HeaderAlbumArtists": "Artistes de l'àlbum",
|
||||
"HeaderContinueWatching": "Continua veient",
|
||||
"HeaderContinueWatching": "Continueu mirant",
|
||||
"HeaderFavoriteAlbums": "Àlbums preferits",
|
||||
"HeaderFavoriteArtists": "Artistes preferits",
|
||||
"HeaderFavoriteEpisodes": "Episodis preferits",
|
||||
@ -24,11 +24,11 @@
|
||||
"HeaderFavoriteSongs": "Cançons preferides",
|
||||
"HeaderLiveTV": "TV en directe",
|
||||
"HeaderNextUp": "A continuació",
|
||||
"HeaderRecordingGroups": "Grups Musicals",
|
||||
"HeaderRecordingGroups": "Grups musicals",
|
||||
"HomeVideos": "Vídeos domèstics",
|
||||
"Inherit": "Heretat",
|
||||
"ItemAddedWithName": "{0} s'ha afegit a la biblioteca",
|
||||
"ItemRemovedWithName": "{0} s'ha eliminat de la biblioteca",
|
||||
"ItemAddedWithName": "{0} s'ha afegit a la mediateca",
|
||||
"ItemRemovedWithName": "{0} s'ha eliminat de la mediateca",
|
||||
"LabelIpAddressValue": "Adreça IP: {0}",
|
||||
"LabelRunningTimeValue": "Temps en marxa: {0}",
|
||||
"Latest": "Darrers",
|
||||
@ -43,7 +43,7 @@
|
||||
"NameInstallFailed": "{0} instal·lació fallida",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameSeasonUnknown": "Temporada desconeguda",
|
||||
"NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
|
||||
"NewVersionIsAvailable": "Hi ha disponible una versió nova del servidor de Jellyfin per a la descàrrega.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada",
|
||||
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
|
||||
@ -64,7 +64,7 @@
|
||||
"Playlists": "Llistes de reproducció",
|
||||
"Plugin": "Complement",
|
||||
"PluginInstalledWithName": "{0} ha estat instal·lat",
|
||||
"PluginUninstalledWithName": "S'ha instalat {0}",
|
||||
"PluginUninstalledWithName": "S'ha instal·lat {0}",
|
||||
"PluginUpdatedWithName": "S'ha actualitzat {0}",
|
||||
"ProviderValue": "Proveïdor: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} ha fallat",
|
||||
@ -72,10 +72,10 @@
|
||||
"ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}",
|
||||
"Shows": "Sèries",
|
||||
"Songs": "Cançons",
|
||||
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu 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}",
|
||||
"Sync": "Sincronitzar",
|
||||
"Sync": "Sincronitza",
|
||||
"System": "Sistema",
|
||||
"TvShows": "Sèries de TV",
|
||||
"User": "Usuari",
|
||||
@ -89,52 +89,54 @@
|
||||
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}",
|
||||
"ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la teva biblioteca",
|
||||
"ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la mediateca",
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"VersionNumber": "Versió {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
|
||||
"TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
|
||||
"TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin",
|
||||
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
|
||||
"TaskRefreshChannels": "Actualitza els canals",
|
||||
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
|
||||
"TaskCleanTranscode": "Neteja les transcodificacions",
|
||||
"TaskUpdatePluginsDescription": "Actualitza els complements que estan configurats per a actualitzar-se automàticament.",
|
||||
"TaskUpdatePlugins": "Actualitza els complements",
|
||||
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva biblioteca de mitjans.",
|
||||
"TaskRefreshPeople": "Actualitza les persones",
|
||||
"TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
|
||||
"TaskCleanLogs": "Neteja els registres",
|
||||
"TaskRefreshLibraryDescription": "Escaneja la biblioteca de mitjans buscant fitxers nous i refresca les metadades.",
|
||||
"TaskRefreshLibrary": "Escaneja la biblioteca de mitjans",
|
||||
"TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
|
||||
"TaskRefreshChapterImages": "Extreure les imatges dels capítols",
|
||||
"TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
|
||||
"TaskCleanCache": "Elimina la memòria cau",
|
||||
"TaskCleanTranscodeDescription": "Elimina els fitxers de transcodificacions que tinguin més d'un dia.",
|
||||
"TaskCleanTranscode": "Neteja de les transcodificacions",
|
||||
"TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.",
|
||||
"TaskUpdatePlugins": "Actualització dels complements",
|
||||
"TaskRefreshPeopleDescription": "Actualització de les metadades dels actors i directors de la mediateca.",
|
||||
"TaskRefreshPeople": "Actualització de les persones",
|
||||
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
|
||||
"TaskCleanLogs": "Neteja dels registres",
|
||||
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
|
||||
"TaskRefreshLibrary": "Escaneig de les mediateques",
|
||||
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
|
||||
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
|
||||
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
|
||||
"TaskCleanCache": "Eliminació de la memòria cau",
|
||||
"TasksChannelsCategory": "Canals per internet",
|
||||
"TasksApplicationCategory": "Aplicatiu",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksLibraryCategory": "Mediateca",
|
||||
"TasksMaintenanceCategory": "Manteniment",
|
||||
"TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
|
||||
"TaskCleanActivityLog": "Buidar el registre d'activitat",
|
||||
"TaskCleanActivityLogDescription": "Eliminació de les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
|
||||
"TaskCleanActivityLog": "Buidatge del registre d'activitat",
|
||||
"Undefined": "Indefinit",
|
||||
"Forced": "Forçat",
|
||||
"Default": "Per defecte",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
|
||||
"TaskOptimizeDatabase": "Optimitzar la base de dades",
|
||||
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
|
||||
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
|
||||
"TaskOptimizeDatabase": "Optimització de la base de dades",
|
||||
"TaskKeyframeExtractorDescription": "Extracció de fotogrames clau dels fitxers de vídeo per a crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
|
||||
"TaskKeyframeExtractor": "Extracció de fotogrames clau",
|
||||
"External": "Extern",
|
||||
"HearingImpaired": "Discapacitat auditiva",
|
||||
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
|
||||
"TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
|
||||
"TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció",
|
||||
"TaskAudioNormalization": "Estabilització d'Àudio",
|
||||
"TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització d'àudio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Baixar les lletres de les cançons",
|
||||
"TaskDownloadMissingLyrics": "Baixar les lletres que falten",
|
||||
"TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció",
|
||||
"TaskAudioNormalization": "Estabilització de l'àudio",
|
||||
"TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
|
||||
"TaskDownloadMissingLyrics": "Descàrrega de les lletres que faltin",
|
||||
"TaskExtractMediaSegments": "Escaneig de segments multimèdia",
|
||||
"TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
|
||||
"TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la biblioteca."
|
||||
"TaskMoveTrickplayImages": "Migració de la ubicació de la imatge de previsualització",
|
||||
"TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.",
|
||||
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
|
||||
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari"
|
||||
}
|
||||
|
@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Skenování segmentů médií",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.",
|
||||
"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í.",
|
||||
"CleanupUserDataTask": "Pročistit uživatelská data"
|
||||
}
|
||||
|
@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Scan for mediesegmenter",
|
||||
"TaskMoveTrickplayImages": "Migrer billedelokationer for trickplay-billeder",
|
||||
"TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.",
|
||||
"TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment."
|
||||
"TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.",
|
||||
"CleanupUserDataTask": "Brugerdata oprydningsopgave",
|
||||
"CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage."
|
||||
}
|
||||
|
@ -43,7 +43,7 @@
|
||||
"NameInstallFailed": "Installation von {0} fehlgeschlagen",
|
||||
"NameSeasonNumber": "Staffel {0}",
|
||||
"NameSeasonUnknown": "Staffel unbekannt",
|
||||
"NewVersionIsAvailable": "Eine neue Version von Jellyfin-Server steht zum Download bereit.",
|
||||
"NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
|
||||
"NotificationOptionAudioPlayback": "Audiowiedergabe gestartet",
|
||||
@ -72,12 +72,12 @@
|
||||
"ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden",
|
||||
"Shows": "Serien",
|
||||
"Songs": "Lieder",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin-Server startet, 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",
|
||||
"Sync": "Synchronisation",
|
||||
"System": "System",
|
||||
"TvShows": "TV-Sendungen",
|
||||
"TvShows": "Serien",
|
||||
"User": "Benutzer",
|
||||
"UserCreatedWithName": "Benutzer {0} wurde erstellt",
|
||||
"UserDeletedWithName": "Benutzer {0} wurde gelöscht",
|
||||
@ -90,32 +90,32 @@
|
||||
"UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet",
|
||||
"UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet",
|
||||
"ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt",
|
||||
"ValueSpecialEpisodeName": "Extra - {0}",
|
||||
"ValueSpecialEpisodeName": "Extra – {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Suche im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
|
||||
"TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter",
|
||||
"TaskRefreshChannelsDescription": "Aktualisiere Internet-Kanal-Informationen.",
|
||||
"TaskRefreshChannels": "Aktualisiere Kanäle",
|
||||
"TaskCleanTranscodeDescription": "Löscht Transkodierdateien, die älter als einen Tag sind.",
|
||||
"TaskCleanTranscode": "Räume Transkodierungs-Verzeichnis auf",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
|
||||
"TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen",
|
||||
"TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.",
|
||||
"TaskRefreshChannels": "Kanäle aktualisieren",
|
||||
"TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.",
|
||||
"TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen",
|
||||
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
|
||||
"TaskUpdatePlugins": "Aktualisiere Plugins",
|
||||
"TaskUpdatePlugins": "Plugins aktualisieren",
|
||||
"TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
|
||||
"TaskRefreshPeople": "Aktualisiere Personen",
|
||||
"TaskRefreshPeople": "Personen aktualisieren",
|
||||
"TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.",
|
||||
"TaskCleanLogs": "Räumt Log-Verzeichnis auf",
|
||||
"TaskRefreshLibraryDescription": "Scannt alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiere Metadaten.",
|
||||
"TaskRefreshLibrary": "Scanne Medien-Bibliothek",
|
||||
"TaskCleanLogs": "Log-Verzeichnis aufräumen",
|
||||
"TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.",
|
||||
"TaskRefreshLibrary": "Medien-Bibliothek scannen",
|
||||
"TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.",
|
||||
"TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder",
|
||||
"TaskCleanCacheDescription": "Löscht nicht mehr benötigte Zwischenspeicherdateien.",
|
||||
"TaskCleanCache": "Leere Zwischenspeicher",
|
||||
"TaskRefreshChapterImages": "Kapitel-Bilder extrahieren",
|
||||
"TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.",
|
||||
"TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen",
|
||||
"TasksChannelsCategory": "Internet-Kanäle",
|
||||
"TasksApplicationCategory": "Anwendung",
|
||||
"TasksLibraryCategory": "Bibliothek",
|
||||
"TasksMaintenanceCategory": "Wartung",
|
||||
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
|
||||
"TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen",
|
||||
"TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen",
|
||||
"Undefined": "Undefiniert",
|
||||
"Forced": "Erzwungen",
|
||||
"Default": "Standard",
|
||||
@ -128,13 +128,15 @@
|
||||
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
|
||||
"TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Löscht nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
|
||||
"TaskAudioNormalization": "Audio Normalisierung",
|
||||
"TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
|
||||
"TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
|
||||
"TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen",
|
||||
"TaskExtractMediaSegments": "Scanne Mediensegmente",
|
||||
"TaskExtractMediaSegments": "Mediensegmente scannen",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.",
|
||||
"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",
|
||||
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Anschaustatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind."
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
"Collections": "Συλλογές",
|
||||
"DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
|
||||
"DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
|
||||
"FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}",
|
||||
"FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}",
|
||||
"Favorites": "Αγαπημένα",
|
||||
"Folders": "Φάκελοι",
|
||||
"Genres": "Είδη",
|
||||
@ -27,8 +27,8 @@
|
||||
"HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
|
||||
"HomeVideos": "Προσωπικά Βίντεο",
|
||||
"Inherit": "Κληρονόμηση",
|
||||
"ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη",
|
||||
"ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη",
|
||||
"ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη",
|
||||
"ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη",
|
||||
"LabelIpAddressValue": "Διεύθυνση IP: {0}",
|
||||
"LabelRunningTimeValue": "Διάρκεια: {0}",
|
||||
"Latest": "Πρόσφατα",
|
||||
@ -40,7 +40,7 @@
|
||||
"Movies": "Ταινίες",
|
||||
"Music": "Μουσική",
|
||||
"MusicVideos": "Μουσικά Βίντεο",
|
||||
"NameInstallFailed": "{0} η εγκατάσταση απέτυχε",
|
||||
"NameInstallFailed": "H εγκατάσταση του {0} απέτυχε",
|
||||
"NameSeasonNumber": "Κύκλος {0}",
|
||||
"NameSeasonUnknown": "Άγνωστος Κύκλος",
|
||||
"NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.",
|
||||
@ -54,7 +54,7 @@
|
||||
"NotificationOptionPluginError": "Αποτυχία του πρόσθετου",
|
||||
"NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε",
|
||||
"NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε",
|
||||
"NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε",
|
||||
"NotificationOptionPluginUpdateInstalled": "Η ενημέρωση του πρόσθετου εγκαταστάθηκε",
|
||||
"NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση",
|
||||
"NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας",
|
||||
"NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε",
|
||||
@ -63,9 +63,9 @@
|
||||
"Photos": "Φωτογραφίες",
|
||||
"Playlists": "Λίστες αναπαραγωγής",
|
||||
"Plugin": "Πρόσθετο",
|
||||
"PluginInstalledWithName": "{0} εγκαταστήθηκε",
|
||||
"PluginUninstalledWithName": "{0} έχει απεγκατασταθεί",
|
||||
"PluginUpdatedWithName": "{0} έχει αναβαθμιστεί",
|
||||
"PluginInstalledWithName": "Το {0} εγκαταστάθηκε",
|
||||
"PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί",
|
||||
"PluginUpdatedWithName": "Το {0} ενημερώθηκε",
|
||||
"ProviderValue": "Πάροχος: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} αποτυχία",
|
||||
"ScheduledTaskStartedWithName": "{0} ξεκίνησε",
|
||||
@ -96,7 +96,7 @@
|
||||
"TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
|
||||
"TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής",
|
||||
"TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.",
|
||||
"TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων",
|
||||
"TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων",
|
||||
"TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.",
|
||||
"TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου",
|
||||
"TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.",
|
||||
@ -125,7 +125,7 @@
|
||||
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
|
||||
"External": "Εξωτερικό",
|
||||
"HearingImpaired": "Με προβλήματα ακοής",
|
||||
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
|
||||
"TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
|
||||
"TaskAudioNormalization": "Ομοιομορφία ήχου",
|
||||
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
|
||||
|
@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Media Segment Scan",
|
||||
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
|
||||
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.",
|
||||
"CleanupUserDataTask": "User data cleanup task",
|
||||
"CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favourite status etc) from media that is no longer present for at least 90 days."
|
||||
}
|
||||
|
@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegments": "Media Segment Scan",
|
||||
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
|
||||
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.",
|
||||
"CleanupUserDataTask": "User data cleanup task",
|
||||
"CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favorite status etc) from media that is no longer present for at least 90 days."
|
||||
}
|
||||
|
@ -122,5 +122,9 @@
|
||||
"AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis",
|
||||
"TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.",
|
||||
"TaskKeyframeExtractor": "Eltiri Ĉefkadrojn",
|
||||
"External": "Ekstera"
|
||||
"External": "Ekstera",
|
||||
"TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.",
|
||||
"TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)",
|
||||
"TaskAudioNormalization": "Normaligo Sonnivela",
|
||||
"HearingImpaired": "Surda"
|
||||
}
|
||||
|
@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
|
||||
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
||||
"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",
|
||||
"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."
|
||||
}
|
||||
|
@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
|
||||
"TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
|
||||
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
|
||||
"TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu."
|
||||
"TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu.",
|
||||
"CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.",
|
||||
"CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina"
|
||||
}
|
||||
|
@ -135,5 +135,7 @@
|
||||
"TaskDownloadMissingLyricsDescription": "Ladataan sanoituksia",
|
||||
"TaskExtractMediaSegmentsDescription": "Poimii tai hankkii mediasegmenttejä MediaSegment-yhteensopivista laajennuksista.",
|
||||
"TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti",
|
||||
"TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan."
|
||||
"TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan.",
|
||||
"CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä",
|
||||
"CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään."
|
||||
}
|
||||
|
@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
|
||||
"TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes",
|
||||
"TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment."
|
||||
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
|
||||
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
|
||||
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
|
||||
}
|
||||
|
@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Analyse des segments de média",
|
||||
"TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque."
|
||||
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
|
||||
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
|
||||
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
|
||||
}
|
||||
|
@ -135,5 +135,7 @@
|
||||
"TaskUpdatePlugins": "Nuashonraigh Breiseáin",
|
||||
"TaskCleanTranscodeDescription": "Scriostar comhaid traschódaithe níos mó ná lá amháin d'aois.",
|
||||
"TaskCleanTranscode": "Eolaire Transcode Glan",
|
||||
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh"
|
||||
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh",
|
||||
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
|
||||
"CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad."
|
||||
}
|
||||
|
@ -123,5 +123,18 @@
|
||||
"TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.",
|
||||
"External": "Externo",
|
||||
"HearingImpaired": "Problemas de audición",
|
||||
"TaskKeyframeExtractor": "Extractor de fragmentos"
|
||||
"TaskKeyframeExtractor": "Extractor de fragmentos",
|
||||
"TaskAudioNormalization": "Normalización do audio",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reprodución con truco para vídeos en bibliotecas activadas.",
|
||||
"TaskDownloadMissingLyrics": "Descargar letras que faltan",
|
||||
"TaskDownloadMissingLyricsDescription": "Descargas de letras das cancións",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de coleccións e listas de reprodución que xa non existen.",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrae ou obtén segmentos multimedia de complementos habilitados para o Segmento de medios.",
|
||||
"TaskExtractMediaSegments": "Escaneo de segmentos multimedia",
|
||||
"TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.",
|
||||
"TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay",
|
||||
"TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de datos do usuario"
|
||||
}
|
||||
|
@ -32,8 +32,8 @@
|
||||
"LabelIpAddressValue": "Ip כתובת: {0}",
|
||||
"LabelRunningTimeValue": "משך צפייה: {0}",
|
||||
"Latest": "אחרון",
|
||||
"MessageApplicationUpdated": "שרת ג'ליפין עודכן",
|
||||
"MessageApplicationUpdatedTo": "שרת ג'ליפין עודכן לגרסה {0}",
|
||||
"MessageApplicationUpdated": "שרת Jellyfin עודכן",
|
||||
"MessageApplicationUpdatedTo": "שרת Jellyfin עודכן לגרסה {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
|
||||
"MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
|
||||
"MixedContent": "תוכן מעורב",
|
||||
@ -43,7 +43,7 @@
|
||||
"NameInstallFailed": "התקנת {0} נכשלה",
|
||||
"NameSeasonNumber": "עונה {0}",
|
||||
"NameSeasonUnknown": "עונה לא ידועה",
|
||||
"NewVersionIsAvailable": "גרסה חדשה של שרת ג'ליפין זמינה להורדה.",
|
||||
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
|
||||
"NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
|
||||
"NotificationOptionAudioPlayback": "ניגון שמע החל",
|
||||
@ -72,7 +72,7 @@
|
||||
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
|
||||
"Shows": "סדרות",
|
||||
"Songs": "שירים",
|
||||
"StartupEmbyServerIsLoading": "שרת ג'ליפין טוען. נא לנסות שוב בקרוב.",
|
||||
"StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
|
||||
"Sync": "סנכרון",
|
||||
@ -100,14 +100,14 @@
|
||||
"TasksLibraryCategory": "ספרייה",
|
||||
"TasksMaintenanceCategory": "תחזוקה",
|
||||
"TaskUpdatePlugins": "עדכן תוספים",
|
||||
"TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
|
||||
"TaskRefreshPeopleDescription": "מעדכן מטא-דאטה עבור שחקנים ובמאים בספריית המדיה שלך.",
|
||||
"TaskRefreshPeople": "רענן אנשים",
|
||||
"TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
|
||||
"TaskCleanLogs": "ניקוי תיקיית יומן",
|
||||
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
|
||||
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא-דאטה.",
|
||||
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
|
||||
"TasksChannelsCategory": "ערוצי אינטרנט",
|
||||
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט כתוביות חסרות בהתבסס על המטא-דאטה.",
|
||||
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
|
||||
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
|
||||
"TaskRefreshChannels": "רענן ערוץ",
|
||||
@ -125,16 +125,18 @@
|
||||
"TaskKeyframeExtractor": "מחלץ תמונות מפתח",
|
||||
"External": "חיצוני",
|
||||
"HearingImpaired": "לקוי שמיעה",
|
||||
"TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
|
||||
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.",
|
||||
"TaskRefreshTrickplayImages": "יצירת תמונות Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.",
|
||||
"TaskAudioNormalization": "נרמול שמע",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
|
||||
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
|
||||
"TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה",
|
||||
"TaskDownloadMissingLyrics": "הורדת מילים חסרות",
|
||||
"TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
|
||||
"TaskMoveTrickplayImages": "העברת מיקום התמונות",
|
||||
"TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",
|
||||
"TaskExtractMediaSegments": "סריקת מדיה",
|
||||
"TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
|
||||
"TaskMoveTrickplayImagesDescription": "הזזת קבצי טריקפליי קיימים בהתאם להגדרות הספרייה."
|
||||
"TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה.",
|
||||
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
|
||||
"CleanupUserDataTask": "משימת ניקוי מידע משתמש"
|
||||
}
|
||||
|
1
Emby.Server.Implementations/Localization/Core/he_IL.json
Normal file
1
Emby.Server.Implementations/Localization/Core/he_IL.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"Albums": "Albumok",
|
||||
"AppDeviceValues": "Program: {0}, eszköz: {1}",
|
||||
"AppDeviceValues": "Program: {0}, Eszköz: {1}",
|
||||
"Application": "Alkalmazás",
|
||||
"Artists": "Előadók",
|
||||
"AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
|
||||
@ -13,7 +13,7 @@
|
||||
"DeviceOnlineWithName": "{0} belépett",
|
||||
"FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
|
||||
"Favorites": "Kedvencek",
|
||||
"Folders": "Könyvtárak",
|
||||
"Folders": "Mappák",
|
||||
"Genres": "Műfajok",
|
||||
"HeaderAlbumArtists": "Albumelőadók",
|
||||
"HeaderContinueWatching": "Megtekintés folytatása",
|
||||
@ -136,5 +136,7 @@
|
||||
"TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
|
||||
"TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése",
|
||||
"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.",
|
||||
"CleanupUserDataTask": "Felhasználói adatok tisztítása feladat"
|
||||
}
|
||||
|
@ -129,5 +129,13 @@
|
||||
"TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
|
||||
"TaskAudioNormalization": "Normalisasi Audio",
|
||||
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada.",
|
||||
"TaskDownloadMissingLyricsDescription": "Unduh lirik untuk lagu",
|
||||
"TaskExtractMediaSegmentsDescription": "Mengekstrak atau memperoleh segmen media dari plugin yang mendukung MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Memindahkan file trickplay yang sudah ada sesuai dengan pengaturan pustaka.",
|
||||
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (status tontonan, status favorit, dll.) dari media yang sudah tidak ada selama setidaknya 90 hari.",
|
||||
"TaskExtractMediaSegments": "Scan Segmen media",
|
||||
"TaskMoveTrickplayImages": "Migrasikan Lokasi Gambar Trickplay",
|
||||
"TaskDownloadMissingLyrics": "Unduh Lirik yang Hilang",
|
||||
"CleanupUserDataTask": "Tugas Pembersihan Data Pengguna"
|
||||
}
|
||||
|
@ -131,5 +131,8 @@
|
||||
"TaskCleanCollectionsAndPlaylists": "Hreinsa söfn og spilunarlista",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjarlægir hluti úr söfnum og spilalistum sem eru ekki lengur til.",
|
||||
"TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
|
||||
"TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar"
|
||||
"TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar",
|
||||
"TaskExtractMediaSegments": "Skönnun efnishluta",
|
||||
"CleanupUserDataTask": "Hreinsun notendagagna",
|
||||
"CleanupUserDataTaskDescription": "Hreinsar öll notendagögn (spilunarstöðu, uppáhöld o.s.frv.) um gögn sem hafa ekki verið til staðar í að lámarki 90 daga."
|
||||
}
|
||||
|
@ -58,8 +58,8 @@
|
||||
"NotificationOptionServerRestartRequired": "Riavvio del server necessario",
|
||||
"NotificationOptionTaskFailed": "Operazione pianificata fallita",
|
||||
"NotificationOptionUserLockedOut": "Utente bloccato",
|
||||
"NotificationOptionVideoPlayback": "La riproduzione video è iniziata",
|
||||
"NotificationOptionVideoPlaybackStopped": "La riproduzione video è stata interrotta",
|
||||
"NotificationOptionVideoPlayback": "Riproduzione video iniziata",
|
||||
"NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
|
||||
"Photos": "Foto",
|
||||
"Playlists": "Playlist",
|
||||
"Plugin": "Plugin",
|
||||
@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "Sposta le immagini Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.",
|
||||
"TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
|
||||
"TaskExtractMediaSegments": "Scansiona Segmento Media"
|
||||
"TaskExtractMediaSegments": "Scansiona Segmento Media",
|
||||
"CleanupUserDataTask": "Task di pulizia dei dati utente",
|
||||
"CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni."
|
||||
}
|
||||
|
@ -135,5 +135,7 @@
|
||||
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
|
||||
"TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。",
|
||||
"TaskDownloadMissingLyrics": "失われた歌詞をダウンロード",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。"
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。",
|
||||
"CleanupUserDataTask": "ユーザーデータのクリーンアップタスク",
|
||||
"CleanupUserDataTaskDescription": "90日以上存在しないメディアに対して、視聴状態やお気に入り状態などのユーザーデータをすべて削除します。"
|
||||
}
|
||||
|
@ -25,7 +25,7 @@
|
||||
"DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
|
||||
"DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
|
||||
"External": "ಹೊರಗಿನ",
|
||||
"FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
|
||||
"FailedLoginAttemptWithUserName": "ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ ಸಂಖ್ಯೆ {0}",
|
||||
"Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
|
||||
"Folders": "ಫೋಲ್ಡರ್ಗಳು",
|
||||
"Forced": "ಬಲವಂತವಾಗಿ",
|
||||
@ -123,5 +123,13 @@
|
||||
"TaskUpdatePlugins": "ಪ್ಲಗಿನ್ಗಳನ್ನು ನವೀಕರಿಸಿ",
|
||||
"TaskCleanTranscode": "ಟ್ರಾನ್ಸ್ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
|
||||
"TaskRefreshChannels": "ಚಾನಲ್ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
|
||||
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
|
||||
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
|
||||
"TaskAudioNormalizationDescription": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ ಮಾಹಿತಿಗಾಗಿ ಕಡತಗಳನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ.",
|
||||
"TaskDownloadMissingLyricsDescription": "ಹಾಡುಗಳಿಗೆ ಸಾಹಿತ್ಯ ಪಡೆಯಿರಿ",
|
||||
"TaskExtractMediaSegments": "ಮಾಧ್ಯಮ ವಿಭಾಗದ ಹುಡುಕು",
|
||||
"TaskDownloadMissingLyrics": "ಇಲ್ಲದ ಸಾಹಿತ್ಯವನ್ನು ಪಡೆಯಿರಿ",
|
||||
"TaskAudioNormalization": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ",
|
||||
"TaskRefreshTrickplayImages": "ಟ್ರಿಕ್ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ",
|
||||
"TaskCleanCollectionsAndPlaylists": "ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ಇಲ್ಲದ ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ."
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"CameraImageUploadedFrom": "Nauja nuotrauka įkelta iš kameros {0}",
|
||||
"Channels": "Kanalai",
|
||||
"ChapterNameValue": "Scena{0}",
|
||||
"Collections": "Kolekcijos",
|
||||
"Collections": "Rinkiniai",
|
||||
"DeviceOfflineWithName": "{0} buvo atjungtas",
|
||||
"DeviceOnlineWithName": "{0} prisijungęs",
|
||||
"FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti",
|
||||
@ -17,18 +17,18 @@
|
||||
"Genres": "Žanrai",
|
||||
"HeaderAlbumArtists": "Albumo atlikėjai",
|
||||
"HeaderContinueWatching": "Žiūrėti toliau",
|
||||
"HeaderFavoriteAlbums": "Mėgstami Albumai",
|
||||
"HeaderFavoriteArtists": "Mėgstami Atlikėjai",
|
||||
"HeaderFavoriteAlbums": "Mėgstami albumai",
|
||||
"HeaderFavoriteArtists": "Mėgstami atlikėjai",
|
||||
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
|
||||
"HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
|
||||
"HeaderFavoriteSongs": "Mėgstamos Dainos",
|
||||
"HeaderLiveTV": "Tiesioginė TV",
|
||||
"HeaderNextUp": "Toliau eilėje",
|
||||
"HeaderNextUp": "Toliau",
|
||||
"HeaderRecordingGroups": "Įrašų grupės",
|
||||
"HomeVideos": "Namų vaizdo įrašai",
|
||||
"Inherit": "Paveldėti",
|
||||
"ItemAddedWithName": "{0} - buvo įkeltas į mediateką",
|
||||
"ItemRemovedWithName": "{0} - buvo pašalinta iš mediatekos",
|
||||
"ItemAddedWithName": "{0} - buvo įkeltas į biblioteką",
|
||||
"ItemRemovedWithName": "{0} - buvo pašalinta iš bibliotekos",
|
||||
"LabelIpAddressValue": "IP adresas: {0}",
|
||||
"LabelRunningTimeValue": "Trukmė: {0}",
|
||||
"Latest": "Naujausi",
|
||||
@ -36,7 +36,7 @@
|
||||
"MessageApplicationUpdatedTo": "\"Jellyfin Server\" buvo atnaujinta iki {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Serverio nustatymai (skyrius {0}) buvo atnaujinti",
|
||||
"MessageServerConfigurationUpdated": "Serverio nustatymai buvo atnaujinti",
|
||||
"MixedContent": "Mixed content",
|
||||
"MixedContent": "Mišrus turinys",
|
||||
"Movies": "Filmai",
|
||||
"Music": "Muzika",
|
||||
"MusicVideos": "Muzikiniai vaizdo įrašai",
|
||||
@ -53,21 +53,21 @@
|
||||
"NotificationOptionNewLibraryContent": "Naujas turinys įkeltas",
|
||||
"NotificationOptionPluginError": "Įskiepio klaida",
|
||||
"NotificationOptionPluginInstalled": "Įskiepis įdiegtas",
|
||||
"NotificationOptionPluginUninstalled": "Įskiepis pašalintas",
|
||||
"NotificationOptionPluginUninstalled": "Įskiepis išdiegtas",
|
||||
"NotificationOptionPluginUpdateInstalled": "Įskiepio atnaujinimas įdiegtas",
|
||||
"NotificationOptionServerRestartRequired": "Reikalingas serverio perleidimas",
|
||||
"NotificationOptionTaskFailed": "Suplanuotos užduoties klaida",
|
||||
"NotificationOptionUserLockedOut": "Vartotojas užblokuotas",
|
||||
"NotificationOptionUserLockedOut": "Naudotojas užblokuotas",
|
||||
"NotificationOptionVideoPlayback": "Vaizdo įrašo atkūrimas pradėtas",
|
||||
"NotificationOptionVideoPlaybackStopped": "Vaizdo įrašo atkūrimas sustabdytas",
|
||||
"Photos": "Nuotraukos",
|
||||
"Playlists": "Grojaraštis",
|
||||
"Plugin": "Plugin",
|
||||
"Playlists": "Grojaraščiai",
|
||||
"Plugin": "Įskiepis",
|
||||
"PluginInstalledWithName": "{0} buvo įdiegtas",
|
||||
"PluginUninstalledWithName": "{0} buvo pašalintas",
|
||||
"PluginUpdatedWithName": "{0} buvo atnaujintas",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} klaida",
|
||||
"ProviderValue": "Paslaugos tiekėjas: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} nepavyko",
|
||||
"ScheduledTaskStartedWithName": "{0} paleista",
|
||||
"ServerNameNeedsToBeRestarted": "{0} reikia iš naujo paleisti",
|
||||
"Shows": "Laidos",
|
||||
@ -76,65 +76,67 @@
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
|
||||
"Sync": "Sinchronizuoti",
|
||||
"System": "System",
|
||||
"TvShows": "TV Serialai",
|
||||
"User": "User",
|
||||
"UserCreatedWithName": "Vartotojas {0} buvo sukurtas",
|
||||
"UserDeletedWithName": "Vartotojas {0} ištrintas",
|
||||
"System": "Sistema",
|
||||
"TvShows": "TV laidos",
|
||||
"User": "Naudotojas",
|
||||
"UserCreatedWithName": "Buvo sukurtas {0} naudotojas",
|
||||
"UserDeletedWithName": "Naudotojas {0} ištrintas",
|
||||
"UserDownloadingItemWithValues": "{0} siunčiasi {1}",
|
||||
"UserLockedOutWithName": "Vartotojas {0} užblokuotas",
|
||||
"UserLockedOutWithName": "Naudotojas {0} užblokuotas",
|
||||
"UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
|
||||
"UserOnlineFromDevice": "{0} prisijungęs iš {1}",
|
||||
"UserPasswordChangedWithName": "Slaptažodis pakeistas vartotojui {0}",
|
||||
"UserPolicyUpdatedWithName": "Vartotojo {0} teisės buvo pakeistos",
|
||||
"UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}",
|
||||
"UserPolicyUpdatedWithName": "Naudotojo {0} teisės buvo pakeistos",
|
||||
"UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką",
|
||||
"ValueSpecialEpisodeName": "Ypatinga - {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.",
|
||||
"TaskUpdatePlugins": "Atnaujinti Priedus",
|
||||
"ValueSpecialEpisodeName": "Ypatingų - {0}",
|
||||
"VersionNumber": "Versija {0}",
|
||||
"TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.",
|
||||
"TaskUpdatePlugins": "Atnaujinti įskieius",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
|
||||
"TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
|
||||
"TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija",
|
||||
"TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.",
|
||||
"TaskRefreshLibrary": "Skenuoti Mediateka",
|
||||
"TaskCleanTranscode": "Išvalyti perkodavimo katalogą",
|
||||
"TaskRefreshLibraryDescription": "Skenuoja medijos biblioteką, ieškodamas naujų failų, ir atnaujina metaduomenis.",
|
||||
"TaskRefreshLibrary": "Skenuoti medijos biblioteką",
|
||||
"TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
|
||||
"TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informaciją.",
|
||||
"TaskRefreshChannels": "Atnaujinti kanalus",
|
||||
"TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.",
|
||||
"TaskRefreshPeople": "Atnaujinti Žmones",
|
||||
"TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų medijos bibliotekoje.",
|
||||
"TaskRefreshPeople": "Atnaujinti žmones",
|
||||
"TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
|
||||
"TaskCleanLogs": "Išvalyti Žurnalą",
|
||||
"TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.",
|
||||
"TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus",
|
||||
"TaskCleanCache": "Išvalyti Talpyklą",
|
||||
"TaskCleanLogs": "Išvalyti žurnalą",
|
||||
"TaskRefreshChapterImagesDescription": "Sukuria vaizdo įrašų, kuriuose yra skyrių, miniatiūras.",
|
||||
"TaskRefreshChapterImages": "Ištraukti skyrių vaizdus",
|
||||
"TaskCleanCache": "Išvalyti talpyklą",
|
||||
"TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
|
||||
"TasksChannelsCategory": "Internetiniai Kanalai",
|
||||
"TasksChannelsCategory": "Internetiniai kanalai",
|
||||
"TasksApplicationCategory": "Programa",
|
||||
"TasksLibraryCategory": "Mediateka",
|
||||
"TasksLibraryCategory": "Biblioteka",
|
||||
"TasksMaintenanceCategory": "Priežiūra",
|
||||
"TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
|
||||
"Undefined": "Neapibrėžtas",
|
||||
"Forced": "Priverstas",
|
||||
"Forced": "Priverstinis",
|
||||
"Default": "Numatytas",
|
||||
"TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius.",
|
||||
"TaskCleanActivityLogDescription": "Ištrina senesnius nei nustatytas amžius veiklos žurnalo įrašus.",
|
||||
"TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
|
||||
"TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.",
|
||||
"TaskKeyframeExtractor": "Pagrindinių kadrų išgavėjas",
|
||||
"TaskKeyframeExtractor": "Reikšminių kadrų (KeyFrame) išgavėjas",
|
||||
"TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazę, gali pagerinti greitaveiką.",
|
||||
"External": "Išorinis",
|
||||
"HearingImpaired": "Su klausos sutrikimais",
|
||||
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
|
||||
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Išvalo duomenis kolekcijose ir grojaraščiuose",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš kolekcijų ir grojaraščių.",
|
||||
"TaskAudioNormalization": "Garso Normalizavimas",
|
||||
"TaskAudioNormalizationDescription": "Skenuoti garso normalizavimo informacijos failuose.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Išvalo duomenis rinkiniuose ir grojaraščiuose",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš rinkinių ir grojaraščių.",
|
||||
"TaskAudioNormalization": "Garso normalizavimas",
|
||||
"TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.",
|
||||
"TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
|
||||
"TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
|
||||
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų papildinių.",
|
||||
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.",
|
||||
"TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
|
||||
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
|
||||
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius"
|
||||
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
|
||||
"CleanupUserDataTask": "Naudotojo duomenų valymo užduotis",
|
||||
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)."
|
||||
}
|
||||
|
@ -135,5 +135,7 @@
|
||||
"TaskMoveTrickplayImages": "Trickplay attēlu pārvietošana",
|
||||
"TaskMoveTrickplayImagesDescription": "Pārvieto esošos trickplay failus atbilstoši bibliotēkas iestatījumiem.",
|
||||
"TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus",
|
||||
"TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām"
|
||||
"TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām",
|
||||
"CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums",
|
||||
"CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas."
|
||||
}
|
||||
|
@ -2,5 +2,10 @@
|
||||
"Albums": "辑册",
|
||||
"Artists": "艺人",
|
||||
"AuthenticationSucceededWithUserName": "{0} 授之权矣",
|
||||
"Books": "册"
|
||||
"Books": "册",
|
||||
"Genres": "类",
|
||||
"HeaderAlbumArtists": "辑者",
|
||||
"Favorites": "至爱",
|
||||
"Folders": "箧",
|
||||
"HeaderContinueWatching": "接目未竟"
|
||||
}
|
||||
|
@ -1,14 +1,141 @@
|
||||
{
|
||||
"Books": "Номууд",
|
||||
"HeaderNextUp": "Дараах",
|
||||
"HeaderNextUp": "Дараа нь",
|
||||
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
||||
"Songs": "Дуунууд",
|
||||
"Playlists": "Тоглуулах жагсаалт",
|
||||
"Movies": "Кино",
|
||||
"Latest": "Сүүлийн үеийн",
|
||||
"Genres": "Төрөл зүйл",
|
||||
"Genres": "Төрлүүд",
|
||||
"Favorites": "Дуртай",
|
||||
"Collections": "Багц",
|
||||
"Artists": "Зураачуд",
|
||||
"Albums": "Цомгууд"
|
||||
"Artists": "Уран бүтээлчид",
|
||||
"Albums": "Цомгууд",
|
||||
"TaskExtractMediaSegments": "Медиа сегмент шалга",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment идэвхжүүлсэн залгаасуудаас медиа сегментүүдийг задлах эсвэл олж авах.",
|
||||
"TaskMoveTrickplayImages": "Трикплэй зургуудын байршлыг шилжүүлэх",
|
||||
"TaskMoveTrickplayImagesDescription": "Одоогоор байгаа трикплэй файлуудыг сангийн тохиргоонд тохируулан шилжүүлнэ.",
|
||||
"TaskDownloadMissingLyrics": "Алга болсон дууны үгийг татаж авах",
|
||||
"TaskDownloadMissingLyricsDescription": "Дууны үгийг татаж авах",
|
||||
"TaskOptimizeDatabase": "Датабаазыг сайжруулах",
|
||||
"TaskKeyframeExtractor": "Түлхүүр кадр гаргагч",
|
||||
"TaskCleanCache": "Кэш санг цэвэрлэх",
|
||||
"NewVersionIsAvailable": "Jellyfin Server-н шинэ хувилбар татаж авахад нээлттэй боллоо.",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server-н {0}-р хэсгийн тохиргоо шинэчлэгдлээ",
|
||||
"NotificationOptionAudioPlaybackStopped": "Дууг зогсоов",
|
||||
"NotificationOptionNewLibraryContent": "Шинэ агуулга орлоо",
|
||||
"NotificationOptionServerRestartRequired": "Server-г дахин асаана уу",
|
||||
"NotificationOptionVideoPlaybackStopped": "Бичлэгийг зогсоов",
|
||||
"UserPasswordChangedWithName": "Хэрэглэгч {0}-н нууц үгийг өөрчиллөө",
|
||||
"TaskCleanCollectionsAndPlaylists": "Цуглуулга ба тоглуулах жагсаалтыг цэвэрлэх",
|
||||
"ScheduledTaskFailedWithName": "{0} амжилтгүй",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server ачааллаж байна. Хэсэг хугацааны дараа дахин оролдоно уу.",
|
||||
"TaskCleanActivityLog": "Үйл ажиллагааны бүртгэлийг цэвэрлэх",
|
||||
"SubtitleDownloadFailureFromForItem": "{0}-г {1}-д зориулсан хадмал орчуулгыг татаж авч чадсангүй",
|
||||
"TaskRefreshLibraryDescription": "Таны медиа санг шинэ файлуудын хувьд шалгаж, мета мэдээллийг шинэчилнэ.",
|
||||
"UserOfflineFromDevice": "{0}-г {1}-с салгалаа",
|
||||
"ValueHasBeenAddedToLibrary": "{0}-г медиа сан руу нэмэгдлээ",
|
||||
"TaskRefreshPeopleDescription": "Таны медиа санд байгаа жүжигчид болон найруулагчдын мета мэдээллийг шинэчилнэ.",
|
||||
"TaskCleanTranscodeDescription": "Нэг өдрөөс илүү настай транскодлох файлуудыг устгана.",
|
||||
"TaskRefreshChannelsDescription": "Интернет сувгуудын мэдээллийг шинэчлэх.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Мета мэдээллийн тохиргоонд үндэслэн интернетээс алга болсон дэд гарчгийг хайна.",
|
||||
"TaskOptimizeDatabaseDescription": "Мэдээллийн сантайг шахаж, чөлөөтэй зайг багасгана. Санг шалгаж, мэдээллийн сантай холбоотой өөрчлөлт хийхийн дараа энэ үйлдлийг гүйцэтгэх нь гүйцэтгэлийг сайжруулах боломжтой.",
|
||||
"TaskKeyframeExtractorDescription": "Видео файлуудаас түлхүүр кадруудыг гаргаж, илүү нарийвчилсан HLS тоглуулах жагсаалт үүсгэнэ. Энэ үйлдэл удаан хугацаанд үргэлжлэх боломжтой.",
|
||||
"NotificationOptionAudioPlayback": "Дууг тоглууллаа",
|
||||
"TaskRefreshTrickplayImages": "Трикплэй зургуудыг үүсгэх",
|
||||
"TaskUpdatePlugins": "Plugin-уудыг шинэчлэх",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Одоо байхгүй болсон зүйлсийг цуглуулга ба тоглуулах жагсаалтаас устгана.",
|
||||
"TaskAudioNormalization": "Аудиог хэвшүүлэх",
|
||||
"TaskAudioNormalizationDescription": "Файлуудаас дууны хэвийн хэмжээсийн мэдээллийг шалгана.",
|
||||
"TaskRefreshTrickplayImagesDescription": "Идэвхжсэн сангуудад байгаа видеонуудын трикплэй урьдчилсан харагдацыг үүсгэнэ.",
|
||||
"TaskUpdatePluginsDescription": "Автомат шинэчлэлд тохируулсан залгаасуудын шинэчлэлтийг татаж авч суулгана.",
|
||||
"TaskCleanTranscode": "Транскодлох санг цэвэрлэх",
|
||||
"TaskRefreshChannels": "Сувгуудыг шинэчлэх",
|
||||
"TaskDownloadMissingSubtitles": "Алга болсон хадмал орчуулгыг татах",
|
||||
"External": "Гадны",
|
||||
"HeaderFavoriteArtists": "Дуртай уран бүтээлчид",
|
||||
"HeaderFavoriteEpisodes": "Дуртай ангиуд",
|
||||
"HeaderFavoriteShows": "Дуртай нэвтрүүлэг",
|
||||
"HeaderFavoriteSongs": "Дуртай дуу",
|
||||
"AppDeviceValues": "Aпп: {0}, Төхөөрөмж: {1}",
|
||||
"Application": "Aпп",
|
||||
"AuthenticationSucceededWithUserName": "{0} амжилттай нэвтэрлээ",
|
||||
"CameraImageUploadedFrom": "{0}-с шинэ зураг байршуулагдлаа",
|
||||
"Channels": "Сувгууд",
|
||||
"ChapterNameValue": "{0}-р бүлэг",
|
||||
"Default": "Өгөгдмөл",
|
||||
"DeviceOfflineWithName": "{0}-н холболт саллаа",
|
||||
"DeviceOnlineWithName": "{0} холбогдлоо",
|
||||
"FailedLoginAttemptWithUserName": "{0}-н нэвтрэх оролдлого амжилтгүй",
|
||||
"Folders": "Хавтаснууд",
|
||||
"Forced": "Хүчээр",
|
||||
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
|
||||
"HeaderFavoriteAlbums": "Дуртай цомгууд",
|
||||
"HeaderLiveTV": "Шууд",
|
||||
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
|
||||
"HearingImpaired": "Сонсголын бэрхшээлтэй",
|
||||
"HomeVideos": "Үндсэн дүрсүүд",
|
||||
"Inherit": "Уламжлах",
|
||||
"ItemAddedWithName": "{0}-г санд нэмлээ",
|
||||
"ItemRemovedWithName": "{0}-с сангаас хаслаа",
|
||||
"LabelIpAddressValue": "IP хаяг: {0}",
|
||||
"LabelRunningTimeValue": "Үргэлжлэх хугацаа: {0}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server шинэчлэгдлээ",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server {0} болж шинэчлэгдлээ",
|
||||
"MessageServerConfigurationUpdated": "Server-н тохиргоо шинэчлэгдлээ",
|
||||
"MixedContent": "Холимог агуулга",
|
||||
"Music": "Дуу",
|
||||
"MusicVideos": "Дууны клип",
|
||||
"NameInstallFailed": "{0} суулгахад алдаа гарлаа",
|
||||
"NameSeasonNumber": "{0}-р улирал",
|
||||
"NameSeasonUnknown": "Улирал олдсонгүй",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Апп шинэчлэлт бий болсон байна",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Апп-н шинэчлэлийг суулгалаа",
|
||||
"NotificationOptionCameraImageUploaded": "Камерын зураг орууллаа",
|
||||
"NotificationOptionInstallationFailed": "Суулгалт амжилтгүй",
|
||||
"NotificationOptionPluginError": "Plugin-д алдаа гарлаа",
|
||||
"NotificationOptionPluginInstalled": "Plugin-г суулгалаа",
|
||||
"NotificationOptionPluginUninstalled": "Plugin-г устгалаа",
|
||||
"NotificationOptionPluginUpdateInstalled": "Plugin-ны шинэчлэн суулгалаа",
|
||||
"NotificationOptionTaskFailed": "Товолсон ажил амжилтгүй",
|
||||
"NotificationOptionUserLockedOut": "Хэрэглэгчийг түгжив",
|
||||
"NotificationOptionVideoPlayback": "Бичлэгийг тоглуулж эхлэв",
|
||||
"Photos": "Зургууд",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0}-г суулгалаа",
|
||||
"PluginUninstalledWithName": "{0}-г устгалаа",
|
||||
"PluginUpdatedWithName": "{0}-г шинэчиллээ",
|
||||
"ProviderValue": "Нийлүүлэгч: {0}",
|
||||
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
|
||||
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
|
||||
"Shows": "Нэвтрүүлгүүд",
|
||||
"Sync": "Дахин",
|
||||
"System": "Систем",
|
||||
"TvShows": "ТВ нэвтрүүлгүүд",
|
||||
"Undefined": "Танисангүй",
|
||||
"User": "Хэрэглэгч",
|
||||
"UserCreatedWithName": "Хэрэглэгч {0}-г үүсгэлээ",
|
||||
"UserDeletedWithName": "Хэрэглэгч {0}-г устгалаа",
|
||||
"UserDownloadingItemWithValues": "{0} нь {1}-г татаж байна",
|
||||
"UserLockedOutWithName": "Хэрэглэгч {0}-г түгжлээ",
|
||||
"UserOnlineFromDevice": "{0} нь {1}-тэй холбоотой байна",
|
||||
"UserPolicyUpdatedWithName": "Хэрэглэгчийн журмыг {0}-д зориулан шинэчиллээ",
|
||||
"UserStartedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж байна",
|
||||
"UserStoppedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж дуусгалаа",
|
||||
"ValueSpecialEpisodeName": "Тусгай - {0}",
|
||||
"VersionNumber": "Хувилбар {0}",
|
||||
"TasksMaintenanceCategory": "Засвар",
|
||||
"TasksLibraryCategory": "Сан",
|
||||
"TasksApplicationCategory": "Апп",
|
||||
"TasksChannelsCategory": "Интернет сувгууд",
|
||||
"TaskCleanActivityLogDescription": "Тохируулсан хугацаанаас хуучин үйл ажиллагааны бүртгэлийн бичлэгүүдийг устгана.",
|
||||
"TaskCleanLogs": "Бүртгэлийн санг цэвэрлэх",
|
||||
"TaskCleanLogsDescription": "{0} өдрөөс илүү настай бүртгэлийн файлуудыг устгана.",
|
||||
"TaskRefreshPeople": "Хүмүүсийг шинэчлэх",
|
||||
"TaskCleanCacheDescription": "Системд хэрэггүй болсон кэш файлуудыг устгана.",
|
||||
"TaskRefreshChapterImages": "Бүлгийн зураг авах",
|
||||
"TaskRefreshChapterImagesDescription": "Бүлгүүдтэй видеонуудын хуудсан зураг үүсгэнэ.",
|
||||
"TaskRefreshLibrary": "Медиа санг шалгах",
|
||||
"CleanupUserDataTask": "Хэрэглэгчийн өгөгдлийн цэвэрлэгээний үүрэг",
|
||||
"CleanupUserDataTaskDescription": "Хугацаа нь 90 хоногоос дээш хугацаанд байхгүй болсон медианаас бүх хэрэглэгчийн өгөгдлийг (үзсэн төлөв, дуртай жагсаалт гэх мэт) цэвэрлэнэ."
|
||||
}
|
||||
|
@ -118,12 +118,19 @@
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "सर्व्हर कॉन्फिगरेशन विभाग {0} अद्यतनित केला गेला आहे",
|
||||
"Inherit": "वारसा",
|
||||
"Forced": "सक्ती केली आहे",
|
||||
"FailedLoginAttemptWithUserName": "अयशस्वी लॉगिन {0} पासून प्रयत्न करा",
|
||||
"FailedLoginAttemptWithUserName": "{0} कडून लॉगिन करण्याचा प्रयत्न अयशस्वी झाला",
|
||||
"External": "बाहेरचा",
|
||||
"DeviceOnlineWithName": "{0} कनेक्ट झाले",
|
||||
"DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे",
|
||||
"AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत",
|
||||
"HearingImpaired": "कर्णबधीर",
|
||||
"TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा",
|
||||
"TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते."
|
||||
"TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते.",
|
||||
"TaskCleanCollectionsAndPlaylists": "संग्रह आणि प्लेलिस्ट व्यवस्थित करा",
|
||||
"TaskExtractMediaSegments": "मिडिया विभाग तपासणी",
|
||||
"TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा",
|
||||
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
|
||||
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user