mirror of https://github.com/jellyfin/jellyfin.git
Fix merge conflict
This commit is contained in:
commit
1d65d4feff
|
@ -47,7 +47,7 @@ jobs:
|
|||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
|
||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) --label "org.opencontainers.image.url=$(Build.Repository.Uri)" --label "org.opencontainers.image.revision=$(Build.SourceVersion)" deployment'
|
||||
displayName: 'Build Dockerfile'
|
||||
|
||||
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
|
||||
|
|
|
@ -30,9 +30,9 @@ body:
|
|||
label: Jellyfin Version
|
||||
description: What version of Jellyfin are you running?
|
||||
options:
|
||||
- 10.8.0
|
||||
- 10.8.z
|
||||
- 10.8.9
|
||||
- 10.7.7
|
||||
- 10.7.z
|
||||
- 10.6.4
|
||||
- Other
|
||||
validations:
|
||||
|
@ -47,13 +47,15 @@ body:
|
|||
label: Environment
|
||||
description: |
|
||||
Examples:
|
||||
- **OS**: [e.g. Debian, Windows]
|
||||
- **OS**: [e.g. Debian 11, Windows 10]
|
||||
- **Linux Kernel**: [e.g. none, 5.15, 6.1, etc.]
|
||||
- **Virtualization**: [e.g. Docker, KVM, LXC]
|
||||
- **Clients**: [Browser, Android, Fire Stick, etc.]
|
||||
- **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
|
||||
- **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
|
||||
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
|
||||
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
|
||||
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
|
||||
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
|
||||
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
|
||||
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
||||
- **Base URL**: [e.g. none, yes: /example]
|
||||
|
@ -61,12 +63,14 @@ body:
|
|||
- **Storage**: [e.g. local, NFS, cloud]
|
||||
value: |
|
||||
- OS:
|
||||
- Linux Kernel:
|
||||
- Virtualization:
|
||||
- Clients:
|
||||
- Browser:
|
||||
- FFmpeg Version:
|
||||
- Playback Method:
|
||||
- Hardware Acceleration:
|
||||
- GPU Model:
|
||||
- Plugins:
|
||||
- Reverse Proxy:
|
||||
- Base URL:
|
||||
|
@ -84,8 +88,8 @@ body:
|
|||
id: ffmpeg-logs
|
||||
attributes:
|
||||
label: FFmpeg logs
|
||||
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||
placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
|
||||
description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log.
|
||||
placeholder: This field is mandatory for debugging hardware transcoding issues. It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: browserlogs
|
||||
|
|
|
@ -19,6 +19,7 @@ jobs:
|
|||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
project:
|
||||
|
|
|
@ -20,18 +20,18 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
|
||||
uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
|
||||
uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
|
||||
uses: github/codeql-action/analyze@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
|
||||
|
|
|
@ -17,14 +17,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
@ -43,7 +43,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||
if: ${{ github.event.comment != null }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
@ -51,14 +51,14 @@ jobs:
|
|||
reactions: eyes
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Notify as running
|
||||
id: comment_running
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||
if: ${{ github.event.comment != null }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
@ -93,7 +93,7 @@ jobs:
|
|||
exit ${retcode}
|
||||
|
||||
- name: Notify with result success
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||
if: ${{ github.event.comment != null && success() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
@ -108,7 +108,7 @@ jobs:
|
|||
reactions: hooray
|
||||
|
||||
- name: Notify with result failure
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||
if: ${{ github.event.comment != null && failure() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
|
|
@ -14,18 +14,18 @@ jobs:
|
|||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '7.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@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
|
@ -39,25 +39,27 @@ jobs:
|
|||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
- name: Checkout common ancestor
|
||||
env:
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
run: |
|
||||
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
|
||||
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
|
||||
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
|
||||
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@607fce577a46308457984d59e4954e075820f10a # tag=v3
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '7.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@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
|
@ -76,12 +78,12 @@ jobs:
|
|||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
|
@ -103,14 +105,14 @@ jobs:
|
|||
body="${body//$'\r'/'%0D'}"
|
||||
echo ::set-output name=body::$body
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@85a676a52594b4481e0532825a2d8906ef96dac2 # v2
|
||||
uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
direction: last
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||
if: ${{ steps.read-diff.outputs.body != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
@ -125,7 +127,7 @@ jobs:
|
|||
|
||||
</details>
|
||||
- name: Edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: Issue Stale Check
|
||||
name: Stale Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
@ -7,12 +7,15 @@ on:
|
|||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
issues:
|
||||
name: Check issues
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
|
||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
days-before-stale: 120
|
||||
|
@ -28,3 +31,21 @@ jobs:
|
|||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
|
||||
prs-conflicts:
|
||||
name: Check PRs with merge conflicts
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
operations-per-run: 75
|
||||
# The merge conflict action will remove the label when updated
|
||||
remove-stale-when-updated: false
|
||||
days-before-stale: -1
|
||||
days-before-close: 90
|
||||
days-before-issue-close: -1
|
||||
stale-pr-label: merge conflict
|
||||
close-pr-message: |-
|
||||
This PR has been closed due to having unresolved merge conflicts.
|
||||
|
|
|
@ -126,6 +126,7 @@
|
|||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||
- [tbraeutigam](https://github.com/tbraeutigam)
|
||||
- [teacupx](https://github.com/teacupx)
|
||||
- [TelepathicWalrus](https://github.com/TelepathicWalrus)
|
||||
- [Terror-Gene](https://github.com/Terror-Gene)
|
||||
- [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
|
||||
- [ThibaultNocchi](https://github.com/ThibaultNocchi)
|
||||
|
@ -163,6 +164,8 @@
|
|||
- [vgambier](https://github.com/vgambier)
|
||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||
- [ipitio](https://github.com/ipitio)
|
||||
- [TheTyrius](https://github.com/TheTyrius)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
|
|
@ -6,78 +6,80 @@
|
|||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||
|
||||
<ItemGroup Label="Package Dependencies">
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.17.0" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.17.0" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.0" />
|
||||
<PackageVersion Include="BDInfo" Version="0.7.6.2" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.2.0" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.14" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.18" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="3.6.10" />
|
||||
<PackageVersion Include="LrcParser" Version="2022.529.1" />
|
||||
<PackageVersion Include="libse" Version="3.6.13" />
|
||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
||||
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="NEbml" Version="0.11.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageVersion Include="PlaylistsNET" Version="1.3.1" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="7.0.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="PlaylistsNET" Version="1.4.0" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||
<PackageVersion Include="prometheus-net" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="2.3.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.0.1" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
|
||||
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" />
|
||||
<PackageVersion Include="SkiaSharp" Version="2.88.3" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
|
||||
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.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="7.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="7.0.2" />
|
||||
<PackageVersion Include="System.Text.Json" Version="7.0.3" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.0.0" />
|
||||
|
|
|
@ -10,6 +10,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
|
|||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM debian:stable-slim as app
|
||||
|
|
|
@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
|
|||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-arm as qemu
|
||||
|
|
|
@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
|
|||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace Emby.Dlna.ConnectionManager
|
|||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
||||
private static IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
var list = new List<StateVariable>
|
||||
return new StateVariable[]
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
|
@ -114,8 +114,6 @@ namespace Emby.Dlna.ConnectionManager
|
|||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
||||
private static IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
var list = new List<StateVariable>
|
||||
return new StateVariable[]
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
|
@ -154,8 +154,6 @@ namespace Emby.Dlna.ContentDirectory
|
|||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ using System.Text;
|
|||
using System.Xml;
|
||||
using Emby.Dlna.ContentDirectory;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -870,11 +871,11 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
var types = new[]
|
||||
{
|
||||
PersonType.Director,
|
||||
PersonType.Writer,
|
||||
PersonType.Producer,
|
||||
PersonType.Composer,
|
||||
"creator"
|
||||
PersonKind.Director,
|
||||
PersonKind.Writer,
|
||||
PersonKind.Producer,
|
||||
PersonKind.Composer,
|
||||
PersonKind.Creator
|
||||
};
|
||||
|
||||
// Seeing some LG models locking up due content with large lists of people
|
||||
|
@ -888,10 +889,13 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
foreach (var actor in people)
|
||||
{
|
||||
var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
|
||||
?? PersonType.Actor;
|
||||
var type = types.FirstOrDefault(i => i == actor.Type || string.Equals(actor.Role, i.ToString(), StringComparison.OrdinalIgnoreCase));
|
||||
if (type == PersonKind.Unknown)
|
||||
{
|
||||
type = PersonKind.Actor;
|
||||
}
|
||||
|
||||
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
|
||||
AddValue(writer, "upnp", type.ToString().ToLowerInvariant(), actor.Name, NsUpnp);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -164,7 +164,7 @@ namespace Emby.Dlna.Eventing
|
|||
|
||||
try
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
|
||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
|
|
@ -7,7 +7,6 @@ using System.Globalization;
|
|||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.PlayTo;
|
||||
using Emby.Dlna.Ssdp;
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
|
@ -15,7 +17,10 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class DlnaHttpClient
|
||||
/// <summary>
|
||||
/// Http client for Dlna PlayTo function.
|
||||
/// </summary>
|
||||
public partial class DlnaHttpClient
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
@ -44,25 +49,44 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
|
||||
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using MemoryStream ms = new MemoryStream();
|
||||
await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
ms,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
catch (XmlException)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse response");
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
// try correcting the Xml response with common errors
|
||||
ms.Position = 0;
|
||||
using StreamReader sr = new StreamReader(ms);
|
||||
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return null;
|
||||
// find and replace unescaped ampersands (&)
|
||||
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
|
||||
|
||||
try
|
||||
{
|
||||
// retry reading Xml
|
||||
using var xmlReader = new StringReader(xmlString);
|
||||
return await XDocument.LoadAsync(
|
||||
xmlReader,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse response");
|
||||
_logger.LogDebug("Malformed response: {Content}\n", xmlString);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,5 +128,12 @@ namespace Emby.Dlna.PlayTo
|
|||
// Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
|
||||
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile-time generated regular expression for escaping ampersands.
|
||||
/// </summary>
|
||||
/// <returns>Compiled regular expression.</returns>
|
||||
[GeneratedRegex("(&(?![a-z]*;))")]
|
||||
private static partial Regex EscapeAmpersandRegex();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -66,7 +64,8 @@ namespace Emby.Dlna.PlayTo
|
|||
IUserDataManager userDataManager,
|
||||
ILocalizationManager localization,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IMediaEncoder mediaEncoder)
|
||||
IMediaEncoder mediaEncoder,
|
||||
Device device)
|
||||
{
|
||||
_session = session;
|
||||
_sessionManager = sessionManager;
|
||||
|
@ -82,14 +81,7 @@ namespace Emby.Dlna.PlayTo
|
|||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
public bool IsSessionActive => !_disposed && _device is not null;
|
||||
|
||||
public bool SupportsMediaControl => IsSessionActive;
|
||||
|
||||
public void Init(Device device)
|
||||
{
|
||||
_device = device;
|
||||
_device.OnDeviceUnavailable = OnDeviceUnavailable;
|
||||
_device.PlaybackStart += OnDevicePlaybackStart;
|
||||
|
@ -102,6 +94,10 @@ namespace Emby.Dlna.PlayTo
|
|||
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
||||
}
|
||||
|
||||
public bool IsSessionActive => !_disposed;
|
||||
|
||||
public bool SupportsMediaControl => IsSessionActive;
|
||||
|
||||
/*
|
||||
* Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||
*/
|
||||
|
@ -131,22 +127,22 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
var info = e.Argument;
|
||||
|
||||
if (!_disposed
|
||||
&& info.Headers.TryGetValue("USN", out string usn)
|
||||
&& info.Headers.TryGetValue("USN", out string? usn)
|
||||
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| (info.Headers.TryGetValue("NT", out string nt)
|
||||
|| (info.Headers.TryGetValue("NT", out string? nt)
|
||||
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
|
||||
{
|
||||
OnDeviceUnavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
||||
private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
|
||||
{
|
||||
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
|
||||
{
|
||||
|
@ -188,7 +184,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e)
|
||||
private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
|
@ -257,7 +253,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e)
|
||||
private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
|
@ -281,7 +277,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e)
|
||||
private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
|
@ -486,9 +482,9 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
private PlaylistItem CreatePlaylistItem(
|
||||
BaseItem item,
|
||||
User user,
|
||||
User? user,
|
||||
long startPostionTicks,
|
||||
string mediaSourceId,
|
||||
string? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex)
|
||||
{
|
||||
|
@ -525,7 +521,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return playlistItem;
|
||||
}
|
||||
|
||||
private string GetDlnaHeaders(PlaylistItem item)
|
||||
private string? GetDlnaHeaders(PlaylistItem item)
|
||||
{
|
||||
var profile = item.Profile;
|
||||
var streamInfo = item.StreamInfo;
|
||||
|
@ -579,7 +575,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return null;
|
||||
}
|
||||
|
||||
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
||||
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
||||
{
|
||||
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
@ -696,7 +692,6 @@ namespace Emby.Dlna.PlayTo
|
|||
_device.MediaChanged -= OnDeviceMediaChanged;
|
||||
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
|
||||
_device.OnDeviceUnavailable = null;
|
||||
_device = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
@ -716,7 +711,7 @@ namespace Emby.Dlna.PlayTo
|
|||
case GeneralCommandType.ToggleMute:
|
||||
return _device.ToggleMute(cancellationToken);
|
||||
case GeneralCommandType.SetAudioStreamIndex:
|
||||
if (command.Arguments.TryGetValue("Index", out string index))
|
||||
if (command.Arguments.TryGetValue("Index", out string? index))
|
||||
{
|
||||
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
|
@ -740,7 +735,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
|
||||
case GeneralCommandType.SetVolume:
|
||||
if (command.Arguments.TryGetValue("Volume", out string vol))
|
||||
if (command.Arguments.TryGetValue("Volume", out string? vol))
|
||||
{
|
||||
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
|
||||
{
|
||||
|
@ -865,34 +860,19 @@ namespace Emby.Dlna.PlayTo
|
|||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
|
||||
if (_device is null)
|
||||
return name switch
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (name == SessionMessageType.Play)
|
||||
{
|
||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (name == SessionMessageType.Playstate)
|
||||
{
|
||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (name == SessionMessageType.GeneralCommand)
|
||||
{
|
||||
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
|
||||
}
|
||||
|
||||
// Not supported or needed right now
|
||||
return Task.CompletedTask;
|
||||
SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
|
||||
SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
|
||||
SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
|
||||
_ => Task.CompletedTask // Not supported or needed right now
|
||||
};
|
||||
}
|
||||
|
||||
private class StreamParams
|
||||
{
|
||||
private MediaSourceInfo _mediaSource;
|
||||
private IMediaSourceManager _mediaSourceManager;
|
||||
private MediaSourceInfo? _mediaSource;
|
||||
private IMediaSourceManager? _mediaSourceManager;
|
||||
|
||||
public Guid ItemId { get; set; }
|
||||
|
||||
|
@ -904,17 +884,17 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
public string DeviceProfileId { get; set; }
|
||||
public string? DeviceProfileId { get; set; }
|
||||
|
||||
public string DeviceId { get; set; }
|
||||
public string? DeviceId { get; set; }
|
||||
|
||||
public string MediaSourceId { get; set; }
|
||||
public string? MediaSourceId { get; set; }
|
||||
|
||||
public string LiveStreamId { get; set; }
|
||||
public string? LiveStreamId { get; set; }
|
||||
|
||||
public BaseItem Item { get; set; }
|
||||
public BaseItem? Item { get; set; }
|
||||
|
||||
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
|
||||
public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_mediaSource is not null)
|
||||
{
|
||||
|
@ -944,8 +924,8 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (Guid.TryParse(parts[i + 1], out var result))
|
||||
{
|
||||
|
|
|
@ -205,12 +205,11 @@ namespace Emby.Dlna.PlayTo
|
|||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_mediaEncoder);
|
||||
_mediaEncoder,
|
||||
device);
|
||||
|
||||
sessionInfo.AddController(controller);
|
||||
|
||||
controller.Init(device);
|
||||
|
||||
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
|
||||
_dlnaManager.GetDefaultProfile();
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "")
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
|
@ -137,10 +137,10 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary)
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
|
@ -150,9 +150,9 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else if (dictionary.ContainsKey(arg.Name))
|
||||
else if (dictionary.TryGetValue(arg.Name, out var argValue))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
|
||||
stateString += BuildArgumentXml(arg, argValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -160,7 +160,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
|
||||
|
|
|
@ -147,11 +147,16 @@ namespace Emby.Dlna.Server
|
|||
}
|
||||
}
|
||||
|
||||
private string GetFriendlyName()
|
||||
internal string GetFriendlyName()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_profile.FriendlyName))
|
||||
{
|
||||
return "Jellyfin - " + _serverName;
|
||||
return _serverName;
|
||||
}
|
||||
|
||||
if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _profile.FriendlyName;
|
||||
}
|
||||
|
||||
var characterList = new List<char>();
|
||||
|
@ -164,13 +169,18 @@ namespace Emby.Dlna.Server
|
|||
}
|
||||
}
|
||||
|
||||
var characters = characterList.ToArray();
|
||||
var serverName = string.Create(
|
||||
characterList.Count,
|
||||
characterList,
|
||||
(dest, source) =>
|
||||
{
|
||||
for (int i = 0; i < dest.Length; i++)
|
||||
{
|
||||
dest[i] = source[i];
|
||||
}
|
||||
});
|
||||
|
||||
var serverName = new string(characters);
|
||||
|
||||
var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return name ?? string.Empty;
|
||||
return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AppendIconList(StringBuilder builder)
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Globalization;
|
|||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Emby.Naming.Audio
|
||||
{
|
||||
|
@ -58,13 +59,7 @@ namespace Emby.Naming.Audio
|
|||
|
||||
var tmp = trimmedFilename.Slice(prefix.Length).Trim();
|
||||
|
||||
int index = tmp.IndexOf(' ');
|
||||
if (index != -1)
|
||||
{
|
||||
tmp = tmp.Slice(0, index);
|
||||
}
|
||||
|
||||
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||
if (int.TryParse(tmp.LeftPart(' '), CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook
|
|||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
foreach (var expression in _options.AudioBookPartsExpressions)
|
||||
{
|
||||
var match = new Regex(expression, RegexOptions.IgnoreCase).Match(fileName);
|
||||
var match = Regex.Match(fileName, expression, RegexOptions.IgnoreCase);
|
||||
if (match.Success)
|
||||
{
|
||||
if (!result.ChapterNumber.HasValue)
|
||||
|
@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook
|
|||
var value = match.Groups["chapter"];
|
||||
if (value.Success)
|
||||
{
|
||||
if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
{
|
||||
result.ChapterNumber = intValue;
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook
|
|||
var value = match.Groups["part"];
|
||||
if (value.Success)
|
||||
{
|
||||
if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
{
|
||||
result.PartNumber = intValue;
|
||||
}
|
||||
|
|
|
@ -79,25 +79,25 @@ namespace Emby.Naming.AudioBook
|
|||
{
|
||||
if (group.Count() > 1 || haveChaptersOrPages)
|
||||
{
|
||||
var ex = new List<AudioBookFileInfo>();
|
||||
var alt = new List<AudioBookFileInfo>();
|
||||
List<AudioBookFileInfo>? ex = null;
|
||||
List<AudioBookFileInfo>? alt = null;
|
||||
|
||||
foreach (var audioFile in group)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
|
||||
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
|
||||
var name = Path.GetFileNameWithoutExtension(audioFile.Path.AsSpan());
|
||||
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
alt.Add(audioFile);
|
||||
(alt ??= new()).Add(audioFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
ex.Add(audioFile);
|
||||
(ex ??= new()).Add(audioFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (ex.Count > 0)
|
||||
if (ex is not null)
|
||||
{
|
||||
var extra = ex
|
||||
.OrderBy(x => x.Container)
|
||||
|
@ -108,7 +108,7 @@ namespace Emby.Naming.AudioBook
|
|||
extras.AddRange(extra);
|
||||
}
|
||||
|
||||
if (alt.Count > 0)
|
||||
if (alt is not null)
|
||||
{
|
||||
var alternatives = alt
|
||||
.OrderBy(x => x.Container)
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace Emby.Naming.AudioBook
|
|||
AudioBookNameParserResult result = default;
|
||||
foreach (var expression in _options.AudioBookNamesExpressions)
|
||||
{
|
||||
var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name);
|
||||
var match = Regex.Match(name, expression, RegexOptions.IgnoreCase);
|
||||
if (match.Success)
|
||||
{
|
||||
if (result.Name is null)
|
||||
|
@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook
|
|||
var value = match.Groups["year"];
|
||||
if (value.Success)
|
||||
{
|
||||
if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
{
|
||||
result.Year = intValue;
|
||||
}
|
||||
|
|
|
@ -141,8 +141,7 @@ namespace Emby.Naming.Common
|
|||
VideoFileStackingRules = new[]
|
||||
{
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
|
||||
};
|
||||
|
||||
CleanDateTimes = new[]
|
||||
|
@ -157,7 +156,8 @@ namespace Emby.Naming.Common
|
|||
@"^(?<cleaned>.+?)(\[.*\])",
|
||||
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
||||
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
||||
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
|
||||
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
|
||||
@"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
|
||||
};
|
||||
|
||||
SubtitleFileExtensions = new[]
|
||||
|
@ -270,7 +270,6 @@ namespace Emby.Naming.Common
|
|||
".sfx",
|
||||
".shn",
|
||||
".sid",
|
||||
".spc",
|
||||
".stm",
|
||||
".strm",
|
||||
".ult",
|
||||
|
@ -338,7 +337,15 @@ namespace Emby.Naming.Common
|
|||
}
|
||||
},
|
||||
|
||||
// This isn't a Kodi naming rule, but the expression below causes false positives,
|
||||
// This isn't a Kodi naming rule, but the expression below causes false episode numbers for
|
||||
// Title Season X Episode X naming schemes.
|
||||
// "Series Season X Episode X - Title.avi", "Series S03 E09.avi", "s3 e9 - Title.avi"
|
||||
new EpisodeExpression(@".*[\\\/]((?<seriesname>[^\\/]+?)\s)?[Ss](?:eason)?\s*(?<seasonnumber>[0-9]+)\s+[Ee](?:pisode)?\s*(?<epnumber>[0-9]+).*$")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// Not a Kodi rule as well, but the expression below also causes false positives,
|
||||
// so we make sure this one gets tested first.
|
||||
// "Foo Bar 889"
|
||||
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
|
||||
|
@ -453,16 +460,6 @@ namespace Emby.Naming.Common
|
|||
},
|
||||
};
|
||||
|
||||
EpisodeWithoutSeasonExpressions = new[]
|
||||
{
|
||||
@"[/\._ \-]()([0-9]+)(-[0-9]+)?"
|
||||
};
|
||||
|
||||
EpisodeMultiPartExpressions = new[]
|
||||
{
|
||||
@"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)"
|
||||
};
|
||||
|
||||
VideoExtraRules = new[]
|
||||
{
|
||||
new ExtraRule(
|
||||
|
@ -797,16 +794,6 @@ namespace Emby.Naming.Common
|
|||
/// </summary>
|
||||
public EpisodeExpression[] EpisodeExpressions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of raw episode without season regular expressions strings.
|
||||
/// </summary>
|
||||
public string[] EpisodeWithoutSeasonExpressions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of raw multi-part episodes regular expressions strings.
|
||||
/// </summary>
|
||||
public string[] EpisodeMultiPartExpressions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of video file extensions.
|
||||
/// </summary>
|
||||
|
@ -877,16 +864,6 @@ namespace Emby.Naming.Common
|
|||
/// </summary>
|
||||
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets list of episode without season regular expressions.
|
||||
/// </summary>
|
||||
public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty<Regex>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets list of multi-part episode regular expressions.
|
||||
/// </summary>
|
||||
public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty<Regex>();
|
||||
|
||||
/// <summary>
|
||||
/// Compiles raw regex strings into regexes.
|
||||
/// </summary>
|
||||
|
@ -894,8 +871,6 @@ namespace Emby.Naming.Common
|
|||
{
|
||||
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
|
||||
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
|
||||
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
|
||||
EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray();
|
||||
}
|
||||
|
||||
private Regex Compile(string exp)
|
||||
|
|
|
@ -113,7 +113,7 @@ namespace Emby.Naming.TV
|
|||
if (expression.DateTimeFormats.Length > 0)
|
||||
{
|
||||
if (DateTime.TryParseExact(
|
||||
match.Groups[0].Value,
|
||||
match.Groups[0].ValueSpan,
|
||||
expression.DateTimeFormats,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
|
@ -125,7 +125,7 @@ namespace Emby.Naming.TV
|
|||
result.Success = true;
|
||||
}
|
||||
}
|
||||
else if (DateTime.TryParse(match.Groups[0].Value, out date))
|
||||
else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date))
|
||||
{
|
||||
result.Year = date.Year;
|
||||
result.Month = date.Month;
|
||||
|
@ -138,12 +138,12 @@ namespace Emby.Naming.TV
|
|||
}
|
||||
else if (expression.IsNamed)
|
||||
{
|
||||
if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
|
||||
if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
|
||||
{
|
||||
result.SeasonNumber = num;
|
||||
}
|
||||
|
||||
if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
|
||||
if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
|
||||
{
|
||||
result.EpisodeNumber = num;
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ namespace Emby.Naming.TV
|
|||
if (nextIndex >= name.Length
|
||||
|| !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
|
||||
{
|
||||
if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
|
||||
if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
|
||||
{
|
||||
result.EndingEpisodeNumber = num;
|
||||
}
|
||||
|
@ -170,12 +170,12 @@ namespace Emby.Naming.TV
|
|||
}
|
||||
else
|
||||
{
|
||||
if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
|
||||
if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
|
||||
{
|
||||
result.SeasonNumber = num;
|
||||
}
|
||||
|
||||
if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
|
||||
if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
|
||||
{
|
||||
result.EpisodeNumber = num;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace Emby.Naming.TV
|
|||
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
|
||||
/// preserving namings like "S.H.O.W".
|
||||
/// </summary>
|
||||
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
|
||||
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Resolve information about series from path.
|
||||
|
|
|
@ -43,7 +43,7 @@ namespace Emby.Naming.Video
|
|||
&& match.Groups.Count == 5
|
||||
&& match.Groups[1].Success
|
||||
&& match.Groups[2].Success
|
||||
&& int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
|
||||
&& int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
|
||||
{
|
||||
result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
|
||||
return true;
|
||||
|
|
|
@ -56,7 +56,7 @@ namespace Emby.Naming.Video
|
|||
}
|
||||
else if (rule.RuleType == ExtraRuleType.Regex)
|
||||
{
|
||||
var filename = Path.GetFileName(path);
|
||||
var filename = Path.GetFileName(path.AsSpan());
|
||||
|
||||
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ public class FileStackRule
|
|||
/// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
|
||||
public FileStackRule(string token, bool isNumerical)
|
||||
{
|
||||
_tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
|
||||
_tokenRegex = new Regex(token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
IsNumerical = isNumerical;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
|
@ -13,6 +14,8 @@ namespace Emby.Naming.Video
|
|||
/// </summary>
|
||||
public static class VideoListResolver
|
||||
{
|
||||
private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves alternative versions and extras from list of video files.
|
||||
/// </summary>
|
||||
|
@ -106,6 +109,7 @@ namespace Emby.Naming.Video
|
|||
}
|
||||
|
||||
// Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
|
||||
VideoInfo? primary = null;
|
||||
for (var i = 0; i < videos.Count; i++)
|
||||
{
|
||||
var video = videos[i];
|
||||
|
@ -114,29 +118,43 @@ namespace Emby.Naming.Video
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
|
||||
if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
|
||||
{
|
||||
return videos;
|
||||
}
|
||||
|
||||
if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal))
|
||||
{
|
||||
primary = video;
|
||||
}
|
||||
}
|
||||
|
||||
// The list is created and overwritten in the caller, so we are allowed to do in-place sorting
|
||||
videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
|
||||
if (videos.Count > 1)
|
||||
{
|
||||
var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
|
||||
videos.Clear();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Key)
|
||||
{
|
||||
videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
|
||||
}
|
||||
else
|
||||
{
|
||||
videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
primary ??= videos[0];
|
||||
videos.Remove(primary);
|
||||
|
||||
var list = new List<VideoInfo>
|
||||
{
|
||||
videos[0]
|
||||
primary
|
||||
};
|
||||
|
||||
var alternateVersionsLen = videos.Count - 1;
|
||||
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
||||
for (int i = 0; i < alternateVersionsLen; i++)
|
||||
{
|
||||
var video = videos[i + 1];
|
||||
alternateVersions[i] = video.Files[0];
|
||||
}
|
||||
|
||||
list[0].AlternateVersions = alternateVersions;
|
||||
list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
|
||||
list[0].Name = folderName.ToString();
|
||||
|
||||
return list;
|
||||
|
@ -161,9 +179,8 @@ namespace Emby.Naming.Video
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
|
||||
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, NamingOptions namingOptions)
|
||||
{
|
||||
var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
|
||||
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
|
@ -176,16 +193,15 @@ namespace Emby.Naming.Video
|
|||
}
|
||||
|
||||
// There are no span overloads for regex unfortunately
|
||||
var tmpTestFilename = testFilename.ToString();
|
||||
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
|
||||
if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
|
||||
{
|
||||
tmpTestFilename = cleanName.Trim();
|
||||
testFilename = cleanName.AsSpan().Trim();
|
||||
}
|
||||
|
||||
// The CleanStringParser should have removed common keywords etc.
|
||||
return string.IsNullOrEmpty(tmpTestFilename)
|
||||
return testFilename.IsEmpty
|
||||
|| testFilename[0] == '-'
|
||||
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
||||
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,8 +87,7 @@ namespace Emby.Naming.Video
|
|||
name = cleanDateTimeResult.Name;
|
||||
year = cleanDateTimeResult.Year;
|
||||
|
||||
if (extraResult.ExtraType is null
|
||||
&& TryCleanString(name, namingOptions, out var newName))
|
||||
if (TryCleanString(name, namingOptions, out var newName))
|
||||
{
|
||||
name = newName;
|
||||
}
|
||||
|
|
|
@ -80,11 +80,13 @@ using MediaBrowser.Controller.Subtitles;
|
|||
using MediaBrowser.Controller.SyncPlay;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.LocalMetadata.Savers;
|
||||
using MediaBrowser.MediaEncoding.BdInfo;
|
||||
using MediaBrowser.MediaEncoding.Subtitles;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.System;
|
||||
|
@ -529,6 +531,8 @@ namespace Emby.Server.Implementations
|
|||
|
||||
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||
|
||||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||
|
||||
|
@ -623,6 +627,9 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
}
|
||||
|
||||
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
|
||||
((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
|
||||
|
||||
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
|
||||
await localizationManager.LoadAll().ConfigureAwait(false);
|
||||
|
||||
|
@ -630,9 +637,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
SetStaticProperties();
|
||||
|
||||
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
|
||||
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
|
||||
|
||||
FindParts();
|
||||
}
|
||||
|
||||
|
|
|
@ -157,16 +157,16 @@ namespace Emby.Server.Implementations.Channels
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
|
||||
public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
|
||||
{
|
||||
var user = query.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(query.UserId);
|
||||
|
||||
var channels = GetAllChannels()
|
||||
.Select(GetChannelEntity)
|
||||
var channels = await GetAllChannelEntitiesAsync()
|
||||
.OrderBy(i => i.SortName)
|
||||
.ToList();
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (query.IsRecordingsFolder.HasValue)
|
||||
{
|
||||
|
@ -226,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
if (user is not null)
|
||||
{
|
||||
var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
channels = channels.Where(i =>
|
||||
{
|
||||
if (!i.IsVisible(user))
|
||||
|
@ -235,7 +236,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
try
|
||||
{
|
||||
return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture));
|
||||
return GetChannelProvider(i).IsEnabledFor(userId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -258,7 +259,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
{
|
||||
foreach (var item in all)
|
||||
{
|
||||
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
|
||||
await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,13 +270,13 @@ namespace Emby.Server.Implementations.Channels
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
|
||||
public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
|
||||
{
|
||||
var user = query.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(query.UserId);
|
||||
|
||||
var internalResult = GetChannelsInternal(query);
|
||||
var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
|
||||
|
@ -327,9 +328,12 @@ namespace Emby.Server.Implementations.Channels
|
|||
progress.Report(100);
|
||||
}
|
||||
|
||||
private Channel GetChannelEntity(IChannel channel)
|
||||
private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync()
|
||||
{
|
||||
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult();
|
||||
foreach (IChannel channel in GetAllChannels())
|
||||
{
|
||||
yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
|
||||
|
@ -401,7 +405,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
}
|
||||
else
|
||||
{
|
||||
results = new List<MediaSourceInfo>();
|
||||
results = Enumerable.Empty<MediaSourceInfo>();
|
||||
}
|
||||
|
||||
return results
|
||||
|
|
|
@ -112,7 +112,8 @@ namespace Emby.Server.Implementations.Collections
|
|||
return Path.Combine(_appPaths.DataPath, "collections");
|
||||
}
|
||||
|
||||
private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
|
||||
/// <inheritdoc />
|
||||
public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
|
||||
{
|
||||
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
|
||||
}
|
||||
|
@ -206,8 +207,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
throw new ArgumentException("No collection exists with the supplied Id");
|
||||
}
|
||||
|
||||
var list = new List<LinkedChild>();
|
||||
var itemList = new List<BaseItem>();
|
||||
List<BaseItem>? itemList = null;
|
||||
|
||||
var linkedChildrenList = collection.GetLinkedChildren();
|
||||
var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList();
|
||||
|
@ -223,18 +223,23 @@ namespace Emby.Server.Implementations.Collections
|
|||
|
||||
if (!currentLinkedChildrenIds.Contains(id))
|
||||
{
|
||||
itemList.Add(item);
|
||||
(itemList ??= new()).Add(item);
|
||||
|
||||
list.Add(LinkedChild.Create(item));
|
||||
linkedChildrenList.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (list.Count > 0)
|
||||
if (itemList is not null)
|
||||
{
|
||||
LinkedChild[] newChildren = new LinkedChild[collection.LinkedChildren.Length + list.Count];
|
||||
var originalLen = collection.LinkedChildren.Length;
|
||||
var newItemCount = itemList.Count;
|
||||
LinkedChild[] newChildren = new LinkedChild[originalLen + newItemCount];
|
||||
collection.LinkedChildren.CopyTo(newChildren, 0);
|
||||
list.CopyTo(newChildren, collection.LinkedChildren.Length);
|
||||
for (int i = 0; i < newItemCount; i++)
|
||||
{
|
||||
newChildren[originalLen + i] = LinkedChild.Create(itemList[i]);
|
||||
}
|
||||
|
||||
collection.LinkedChildren = newChildren;
|
||||
collection.UpdateRatingToItems(linkedChildrenList);
|
||||
|
||||
|
|
|
@ -11,14 +11,15 @@ namespace Emby.Server.Implementations
|
|||
/// <summary>
|
||||
/// Gets a new copy of the default configuration options.
|
||||
/// </summary>
|
||||
public static Dictionary<string, string?> DefaultConfiguration => new Dictionary<string, string?>
|
||||
public static Dictionary<string, string?> DefaultConfiguration => new()
|
||||
{
|
||||
{ HostWebClientKey, bool.TrueString },
|
||||
{ DefaultRedirectKey, "web/index.html" },
|
||||
{ DefaultRedirectKey, "web/" },
|
||||
{ FfmpegProbeSizeKey, "1G" },
|
||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
||||
{ BindToUnixSocketKey, bool.FalseString }
|
||||
{ BindToUnixSocketKey, bool.FalseString },
|
||||
{ SqliteCacheSizeKey, "20000" }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
@ -27,9 +26,19 @@ namespace Emby.Server.Implementations.Data
|
|||
/// <summary>
|
||||
/// Gets or sets the path to the DB file.
|
||||
/// </summary>
|
||||
/// <value>Path to the DB file.</value>
|
||||
protected string DbFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of write connections to create.
|
||||
/// </summary>
|
||||
/// <value>Path to the DB file.</value>
|
||||
protected int WriteConnectionsCount { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of read connections to create.
|
||||
/// </summary>
|
||||
protected int ReadConnectionsCount { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
/// </summary>
|
||||
|
@ -63,7 +72,7 @@ namespace Emby.Server.Implementations.Data
|
|||
/// <summary>
|
||||
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
|
||||
/// </summary>
|
||||
protected virtual string LockingMode => "EXCLUSIVE";
|
||||
protected virtual string LockingMode => "NORMAL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
|
||||
|
@ -73,9 +82,10 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
/// <summary>
|
||||
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
|
||||
/// The default (-1) is overriden to prevent unconstrained WAL size, as reported by users.
|
||||
/// </summary>
|
||||
/// <value>The journal size limit.</value>
|
||||
protected virtual int? JournalSizeLimit => 0;
|
||||
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
|
||||
|
||||
/// <summary>
|
||||
/// Gets the page size.
|
||||
|
@ -88,7 +98,7 @@ namespace Emby.Server.Implementations.Data
|
|||
/// </summary>
|
||||
/// <value>The temp store mode.</value>
|
||||
/// <see cref="TempStoreMode"/>
|
||||
protected virtual TempStoreMode TempStore => TempStoreMode.Default;
|
||||
protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the synchronous mode.
|
||||
|
@ -101,63 +111,106 @@ namespace Emby.Server.Implementations.Data
|
|||
/// Gets or sets the write lock.
|
||||
/// </summary>
|
||||
/// <value>The write lock.</value>
|
||||
protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1);
|
||||
protected ConnectionPool WriteConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the write connection.
|
||||
/// </summary>
|
||||
/// <value>The write connection.</value>
|
||||
protected SQLiteDatabaseConnection WriteConnection { get; set; }
|
||||
protected ConnectionPool ReadConnections { get; set; }
|
||||
|
||||
public virtual void Initialize()
|
||||
{
|
||||
WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection);
|
||||
ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection);
|
||||
|
||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.Execute("VACUUM");
|
||||
}
|
||||
}
|
||||
|
||||
protected ManagedConnection GetConnection(bool readOnly = false)
|
||||
{
|
||||
WriteLock.Wait();
|
||||
if (WriteConnection is not null)
|
||||
{
|
||||
return new ManagedConnection(WriteConnection, WriteLock);
|
||||
}
|
||||
=> readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
|
||||
|
||||
WriteConnection = SQLite3.Open(
|
||||
protected SQLiteDatabaseConnection CreateWriteConnection()
|
||||
{
|
||||
var writeConnection = SQLite3.Open(
|
||||
DbFilePath,
|
||||
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
|
||||
null);
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
}
|
||||
|
||||
if (JournalSizeLimit.HasValue)
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA journal_size_limit=" + (int)JournalSizeLimit.Value);
|
||||
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
||||
}
|
||||
|
||||
if (Synchronous.HasValue)
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
}
|
||||
|
||||
if (PageSize.HasValue)
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
}
|
||||
|
||||
WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
||||
WriteConnection.Execute("VACUUM");
|
||||
return writeConnection;
|
||||
}
|
||||
|
||||
return new ManagedConnection(WriteConnection, WriteLock);
|
||||
protected SQLiteDatabaseConnection CreateReadConnection()
|
||||
{
|
||||
var connection = SQLite3.Open(
|
||||
DbFilePath,
|
||||
DefaultConnectionFlags | ConnectionFlags.ReadOnly,
|
||||
null);
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
||||
{
|
||||
connection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||
{
|
||||
connection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
}
|
||||
|
||||
if (JournalSizeLimit.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
||||
}
|
||||
|
||||
if (Synchronous.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
}
|
||||
|
||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
public IStatement PrepareStatement(ManagedConnection connection, string sql)
|
||||
|
@ -166,18 +219,6 @@ namespace Emby.Server.Implementations.Data
|
|||
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
|
||||
=> connection.PrepareStatement(sql);
|
||||
|
||||
public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql)
|
||||
{
|
||||
int len = sql.Count;
|
||||
IStatement[] statements = new IStatement[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
statements[i] = connection.PrepareStatement(sql[i]);
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
protected bool TableExists(ManagedConnection connection, string name)
|
||||
{
|
||||
return connection.RunInTransaction(
|
||||
|
@ -252,22 +293,10 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
if (dispose)
|
||||
{
|
||||
WriteLock.Wait();
|
||||
try
|
||||
{
|
||||
WriteConnection?.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
WriteLock.Release();
|
||||
}
|
||||
|
||||
WriteLock.Dispose();
|
||||
WriteConnections.Dispose();
|
||||
ReadConnections.Dispose();
|
||||
}
|
||||
|
||||
WriteConnection = null;
|
||||
WriteLock = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data;
|
||||
|
||||
/// <summary>
|
||||
/// A pool of SQLite Database connections.
|
||||
/// </summary>
|
||||
public sealed class ConnectionPool : IDisposable
|
||||
{
|
||||
private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionPool" /> class.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of database connection to create.</param>
|
||||
/// <param name="factory">Factory function to create the database connections.</param>
|
||||
public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
_connections.Add(factory.Invoke());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a database connection from the pool if one is available, otherwise blocks.
|
||||
/// </summary>
|
||||
/// <returns>A database connection.</returns>
|
||||
public ManagedConnection GetConnection()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
ThrowObjectDisposedException();
|
||||
}
|
||||
|
||||
return new ManagedConnection(_connections.Take(), this);
|
||||
|
||||
static void ThrowObjectDisposedException()
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(ConnectionPool));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a database connection to the pool.
|
||||
/// </summary>
|
||||
/// <param name="connection">The database connection to return.</param>
|
||||
public void Return(SQLiteDatabaseConnection connection)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
connection.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
_connections.Add(connection);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var connection in _connections)
|
||||
{
|
||||
connection.Dispose();
|
||||
}
|
||||
|
||||
_connections.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
|
@ -2,23 +2,22 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public sealed class ManagedConnection : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _writeLock;
|
||||
private readonly ConnectionPool _pool;
|
||||
|
||||
private SQLiteDatabaseConnection? _db;
|
||||
private SQLiteDatabaseConnection _db;
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
|
||||
public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
|
||||
{
|
||||
_db = db;
|
||||
_writeLock = writeLock;
|
||||
_pool = pool;
|
||||
}
|
||||
|
||||
public IStatement PrepareStatement(string sql)
|
||||
|
@ -73,9 +72,9 @@ namespace Emby.Server.Implementations.Data
|
|||
return;
|
||||
}
|
||||
|
||||
_writeLock.Release();
|
||||
_pool.Return(_db);
|
||||
|
||||
_db = null; // Don't dispose it
|
||||
_db = null!; // Don't dispose it
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ using MediaBrowser.Controller.Entities;
|
|||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
|
@ -34,6 +35,7 @@ using MediaBrowser.Model.Entities;
|
|||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
|
@ -49,8 +51,8 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
private const string SaveItemCommandText =
|
||||
@"replace into TypedBaseItems
|
||||
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
|
||||
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
|
||||
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
|
||||
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
@ -110,6 +112,7 @@ namespace Emby.Server.Implementations.Data
|
|||
"PrimaryVersionId",
|
||||
"DateLastMediaAdded",
|
||||
"Album",
|
||||
"LUFS",
|
||||
"CriticRating",
|
||||
"IsVirtualItem",
|
||||
"SeriesName",
|
||||
|
@ -318,13 +321,15 @@ namespace Emby.Server.Implementations.Data
|
|||
/// <param name="logger">Instance of the <see cref="ILogger{SqliteItemRepository}"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
|
||||
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
|
||||
/// <exception cref="ArgumentNullException">config is null.</exception>
|
||||
public SqliteItemRepository(
|
||||
IServerConfigurationManager config,
|
||||
IServerApplicationHost appHost,
|
||||
ILogger<SqliteItemRepository> logger,
|
||||
ILocalizationManager localization,
|
||||
IImageProcessor imageProcessor)
|
||||
IImageProcessor imageProcessor,
|
||||
IConfiguration configuration)
|
||||
: base(logger)
|
||||
{
|
||||
_config = config;
|
||||
|
@ -336,10 +341,13 @@ namespace Emby.Server.Implementations.Data
|
|||
_jsonOptions = JsonDefaults.Options;
|
||||
|
||||
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
|
||||
|
||||
CacheSize = configuration.GetSqliteCacheSize();
|
||||
ReadConnectionsCount = Environment.ProcessorCount * 2;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override int? CacheSize => 20000;
|
||||
protected override int? CacheSize { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override TempStoreMode TempStore => TempStoreMode.Memory;
|
||||
|
@ -347,10 +355,10 @@ namespace Emby.Server.Implementations.Data
|
|||
/// <summary>
|
||||
/// Opens the connection to the database.
|
||||
/// </summary>
|
||||
/// <param name="userDataRepo">The user data repository.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
const string CreateMediaStreamsTableCommand
|
||||
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, PRIMARY KEY (ItemId, StreamIndex))";
|
||||
|
||||
|
@ -488,6 +496,7 @@ namespace Emby.Server.Implementations.Data
|
|||
AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
|
||||
AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
|
||||
AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames);
|
||||
AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
|
||||
AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
|
||||
AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
|
||||
AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
|
||||
|
@ -551,8 +560,6 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
connection.RunQueries(postQueries);
|
||||
}
|
||||
|
||||
userDataRepo.Initialize(userManager, WriteLock, WriteConnection);
|
||||
}
|
||||
|
||||
public void SaveImages(BaseItem item)
|
||||
|
@ -586,7 +593,7 @@ namespace Emby.Server.Implementations.Data
|
|||
/// <exception cref="ArgumentNullException">
|
||||
/// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
|
||||
/// </exception>
|
||||
public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
|
||||
public void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
|
||||
|
@ -594,9 +601,11 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
CheckDisposed();
|
||||
|
||||
var tuples = new List<(BaseItem, List<Guid>, BaseItem, string, List<string>)>();
|
||||
foreach (var item in items)
|
||||
var itemsLen = items.Count;
|
||||
var tuples = new ValueTuple<BaseItem, List<Guid>, BaseItem, string, List<string>>[itemsLen];
|
||||
for (int i = 0; i < itemsLen; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
var ancestorIds = item.SupportsAncestors ?
|
||||
item.GetAncestorIds().Distinct().ToList() :
|
||||
null;
|
||||
|
@ -606,7 +615,7 @@ namespace Emby.Server.Implementations.Data
|
|||
var userdataKey = item.GetUserDataKeys().FirstOrDefault();
|
||||
var inheritedTags = item.GetInheritedTags();
|
||||
|
||||
tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
|
||||
tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags);
|
||||
}
|
||||
|
||||
using (var connection = GetConnection())
|
||||
|
@ -622,14 +631,8 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
private void SaveItemsInTransaction(IDatabaseConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
|
||||
{
|
||||
var statements = PrepareAll(db, new string[]
|
||||
{
|
||||
SaveItemCommandText,
|
||||
"delete from AncestorIds where ItemId=@ItemId"
|
||||
});
|
||||
|
||||
using (var saveItemStatement = statements[0])
|
||||
using (var deleteAncestorsStatement = statements[1])
|
||||
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
|
||||
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
|
||||
{
|
||||
var requiresReset = false;
|
||||
foreach (var tuple in tuples)
|
||||
|
@ -911,6 +914,7 @@ namespace Emby.Server.Implementations.Data
|
|||
}
|
||||
|
||||
saveItemStatement.TryBind("@Album", item.Album);
|
||||
saveItemStatement.TryBind("@LUFS", item.LUFS);
|
||||
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
|
||||
|
||||
if (item is IHasSeries hasSeriesName)
|
||||
|
@ -1195,7 +1199,7 @@ namespace Emby.Server.Implementations.Data
|
|||
Path = RestorePath(path.ToString())
|
||||
};
|
||||
|
||||
if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)
|
||||
if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
|
||||
&& ticks >= DateTime.MinValue.Ticks
|
||||
&& ticks <= DateTime.MaxValue.Ticks)
|
||||
{
|
||||
|
@ -1284,15 +1288,13 @@ namespace Emby.Server.Implementations.Data
|
|||
CheckDisposed();
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
|
||||
{
|
||||
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
|
||||
{
|
||||
statement.TryBind("@guid", id);
|
||||
statement.TryBind("@guid", id);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return GetItem(row, new InternalItemsQuery());
|
||||
}
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return GetItem(row, new InternalItemsQuery());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1307,7 +1309,8 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(UserRootFolder))
|
||||
|
||||
if (type == typeof(UserRootFolder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -1317,55 +1320,68 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(MusicArtist))
|
||||
|
||||
if (type == typeof(MusicArtist))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(Person))
|
||||
|
||||
if (type == typeof(Person))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(MusicGenre))
|
||||
|
||||
if (type == typeof(MusicGenre))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(Genre))
|
||||
|
||||
if (type == typeof(Genre))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(Studio))
|
||||
|
||||
if (type == typeof(Studio))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(PlaylistsFolder))
|
||||
|
||||
if (type == typeof(PlaylistsFolder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(PhotoAlbum))
|
||||
|
||||
if (type == typeof(PhotoAlbum))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(Year))
|
||||
|
||||
if (type == typeof(Year))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(Book))
|
||||
|
||||
if (type == typeof(Book))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(LiveTvProgram))
|
||||
|
||||
if (type == typeof(LiveTvProgram))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(AudioBook))
|
||||
|
||||
if (type == typeof(AudioBook))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(Audio))
|
||||
|
||||
if (type == typeof(Audio))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (type == typeof(MusicAlbum))
|
||||
|
||||
if (type == typeof(MusicAlbum))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -1749,6 +1765,11 @@ namespace Emby.Server.Implementations.Data
|
|||
item.Album = album;
|
||||
}
|
||||
|
||||
if (reader.TryGetSingle(index++, out var lUFS))
|
||||
{
|
||||
item.LUFS = lUFS;
|
||||
}
|
||||
|
||||
if (reader.TryGetSingle(index++, out var criticRating))
|
||||
{
|
||||
item.CriticRating = criticRating;
|
||||
|
@ -1956,22 +1977,19 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
CheckDisposed();
|
||||
|
||||
var chapters = new List<ChapterInfo>();
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
|
||||
{
|
||||
var chapters = new List<ChapterInfo>();
|
||||
statement.TryBind("@ItemId", item.Id);
|
||||
|
||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
statement.TryBind("@ItemId", item.Id);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
chapters.Add(GetChapter(row, item));
|
||||
}
|
||||
chapters.Add(GetChapter(row, item));
|
||||
}
|
||||
|
||||
return chapters;
|
||||
}
|
||||
|
||||
return chapters;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -1980,16 +1998,14 @@ namespace Emby.Server.Implementations.Data
|
|||
CheckDisposed();
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
|
||||
{
|
||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
|
||||
{
|
||||
statement.TryBind("@ItemId", item.Id);
|
||||
statement.TryBind("@ChapterIndex", index);
|
||||
statement.TryBind("@ItemId", item.Id);
|
||||
statement.TryBind("@ChapterIndex", index);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return GetChapter(row, item);
|
||||
}
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return GetChapter(row, item);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2376,7 +2392,7 @@ namespace Emby.Server.Implementations.Data
|
|||
else
|
||||
{
|
||||
builder.Append(
|
||||
@"(SELECT CASE WHEN InheritedParentalRatingValue=0
|
||||
@"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0
|
||||
THEN 0
|
||||
ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue))
|
||||
END)");
|
||||
|
@ -2390,6 +2406,7 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
// genres, tags, studios, person, year?
|
||||
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))");
|
||||
builder.Append("+ (Select count(1) * 10 from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId))");
|
||||
|
||||
if (item is MusicArtist)
|
||||
{
|
||||
|
@ -2841,13 +2858,10 @@ namespace Emby.Server.Implementations.Data
|
|||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
var itemQueryStatement = PrepareStatement(db, itemQuery);
|
||||
var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery);
|
||||
|
||||
if (!isReturningZeroItems)
|
||||
{
|
||||
using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
|
||||
using (var statement = itemQueryStatement)
|
||||
using (var statement = PrepareStatement(db, itemQuery))
|
||||
{
|
||||
if (EnableJoinUserData(query))
|
||||
{
|
||||
|
@ -2882,7 +2896,7 @@ namespace Emby.Server.Implementations.Data
|
|||
if (query.EnableTotalRecordCount)
|
||||
{
|
||||
using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
|
||||
using (var statement = totalRecordCountQueryStatement)
|
||||
using (var statement = PrepareStatement(db, totalRecordCountQuery))
|
||||
{
|
||||
if (EnableJoinUserData(query))
|
||||
{
|
||||
|
@ -3202,7 +3216,8 @@ namespace Emby.Server.Implementations.Data
|
|||
return IsAlphaNumeric(value);
|
||||
}
|
||||
|
||||
private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement)
|
||||
#nullable enable
|
||||
private List<string> GetWhereClauses(InternalItemsQuery query, IStatement? statement)
|
||||
{
|
||||
if (query.IsResumable ?? false)
|
||||
{
|
||||
|
@ -3677,7 +3692,6 @@ namespace Emby.Server.Implementations.Data
|
|||
if (statement is not null)
|
||||
{
|
||||
nameContains = FixUnicodeChars(nameContains);
|
||||
|
||||
statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%");
|
||||
}
|
||||
}
|
||||
|
@ -3803,13 +3817,8 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var artistId in query.ArtistIds)
|
||||
{
|
||||
var paramName = "@ArtistIds" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
}
|
||||
|
||||
statement?.TryBind(paramName, artistId);
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -3824,13 +3833,8 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var artistId in query.AlbumArtistIds)
|
||||
{
|
||||
var paramName = "@ArtistIds" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
}
|
||||
|
||||
statement?.TryBind(paramName, artistId);
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -3845,13 +3849,8 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var artistId in query.ContributingArtistIds)
|
||||
{
|
||||
var paramName = "@ArtistIds" + index;
|
||||
|
||||
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
}
|
||||
|
||||
statement?.TryBind(paramName, artistId);
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -3866,13 +3865,8 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var albumId in query.AlbumIds)
|
||||
{
|
||||
var paramName = "@AlbumIds" + index;
|
||||
|
||||
clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind(paramName, albumId);
|
||||
}
|
||||
|
||||
statement?.TryBind(paramName, albumId);
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -3887,13 +3881,8 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var artistId in query.ExcludeArtistIds)
|
||||
{
|
||||
var paramName = "@ExcludeArtistId" + index;
|
||||
|
||||
clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
}
|
||||
|
||||
statement?.TryBind(paramName, artistId);
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -3908,13 +3897,8 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var genreId in query.GenreIds)
|
||||
{
|
||||
var paramName = "@GenreId" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind(paramName, genreId);
|
||||
}
|
||||
|
||||
statement?.TryBind(paramName, genreId);
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -3929,11 +3913,7 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var item in query.Genres)
|
||||
{
|
||||
clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@Genre" + index, GetCleanValue(item));
|
||||
}
|
||||
|
||||
statement?.TryBind("@Genre" + index, GetCleanValue(item));
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -3948,11 +3928,7 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var item in tags)
|
||||
{
|
||||
clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@Tag" + index, GetCleanValue(item));
|
||||
}
|
||||
|
||||
statement?.TryBind("@Tag" + index, GetCleanValue(item));
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -3967,11 +3943,7 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var item in excludeTags)
|
||||
{
|
||||
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
|
||||
}
|
||||
|
||||
statement?.TryBind("@ExcludeTag" + index, GetCleanValue(item));
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -3986,14 +3958,8 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var studioId in query.StudioIds)
|
||||
{
|
||||
var paramName = "@StudioId" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
|
||||
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind(paramName, studioId);
|
||||
}
|
||||
|
||||
statement?.TryBind(paramName, studioId);
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -4008,11 +3974,7 @@ namespace Emby.Server.Implementations.Data
|
|||
foreach (var item in query.OfficialRatings)
|
||||
{
|
||||
clauses.Add("OfficialRating=@OfficialRating" + index);
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@OfficialRating" + index, item);
|
||||
}
|
||||
|
||||
statement?.TryBind("@OfficialRating" + index, item);
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -4020,34 +3982,96 @@ namespace Emby.Server.Implementations.Data
|
|||
whereClauses.Add(clause);
|
||||
}
|
||||
|
||||
if (query.MinParentalRating.HasValue)
|
||||
var ratingClauseBuilder = new StringBuilder("(");
|
||||
if (query.HasParentalRating ?? false)
|
||||
{
|
||||
whereClauses.Add("InheritedParentalRatingValue>=@MinParentalRating");
|
||||
if (statement is not null)
|
||||
ratingClauseBuilder.Append("InheritedParentalRatingValue not null");
|
||||
if (query.MinParentalRating.HasValue)
|
||||
{
|
||||
statement.TryBind("@MinParentalRating", query.MinParentalRating.Value);
|
||||
ratingClauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating");
|
||||
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
|
||||
}
|
||||
|
||||
if (query.MaxParentalRating.HasValue)
|
||||
{
|
||||
ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
|
||||
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.MaxParentalRating.HasValue)
|
||||
else if (query.BlockUnratedItems.Length > 0)
|
||||
{
|
||||
whereClauses.Add("InheritedParentalRatingValue<=@MaxParentalRating");
|
||||
var paramName = "@UnratedType";
|
||||
var index = 0;
|
||||
string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++));
|
||||
ratingClauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))");
|
||||
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
|
||||
for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++)
|
||||
{
|
||||
statement.TryBind(paramName + ind, query.BlockUnratedItems[ind].ToString());
|
||||
}
|
||||
}
|
||||
|
||||
if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
|
||||
{
|
||||
ratingClauseBuilder.Append(" OR (");
|
||||
}
|
||||
|
||||
if (query.MinParentalRating.HasValue)
|
||||
{
|
||||
ratingClauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating");
|
||||
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
|
||||
}
|
||||
|
||||
if (query.MaxParentalRating.HasValue)
|
||||
{
|
||||
if (query.MinParentalRating.HasValue)
|
||||
{
|
||||
ratingClauseBuilder.Append(" AND ");
|
||||
}
|
||||
|
||||
ratingClauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating");
|
||||
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
|
||||
}
|
||||
|
||||
if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
|
||||
{
|
||||
ratingClauseBuilder.Append(")");
|
||||
}
|
||||
|
||||
if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue))
|
||||
{
|
||||
ratingClauseBuilder.Append(" OR InheritedParentalRatingValue not null");
|
||||
}
|
||||
}
|
||||
|
||||
if (query.HasParentalRating.HasValue)
|
||||
else if (query.MinParentalRating.HasValue)
|
||||
{
|
||||
if (query.HasParentalRating.Value)
|
||||
ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating");
|
||||
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
|
||||
|
||||
if (query.MaxParentalRating.HasValue)
|
||||
{
|
||||
whereClauses.Add("InheritedParentalRatingValue > 0");
|
||||
}
|
||||
else
|
||||
{
|
||||
whereClauses.Add("InheritedParentalRatingValue = 0");
|
||||
ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
|
||||
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
|
||||
}
|
||||
|
||||
ratingClauseBuilder.Append(")");
|
||||
}
|
||||
else if (query.MaxParentalRating.HasValue)
|
||||
{
|
||||
ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating");
|
||||
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
|
||||
}
|
||||
else if (!query.HasParentalRating ?? false)
|
||||
{
|
||||
ratingClauseBuilder.Append("InheritedParentalRatingValue is null");
|
||||
}
|
||||
|
||||
var ratingClauseString = ratingClauseBuilder.ToString();
|
||||
if (!string.Equals(ratingClauseString, "(", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
whereClauses.Add(ratingClauseString + ")");
|
||||
}
|
||||
|
||||
if (query.HasOfficialRating.HasValue)
|
||||
|
@ -4089,37 +4113,25 @@ namespace Emby.Server.Implementations.Data
|
|||
if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage))
|
||||
{
|
||||
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
|
||||
}
|
||||
statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage))
|
||||
{
|
||||
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
|
||||
}
|
||||
statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage))
|
||||
{
|
||||
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
|
||||
}
|
||||
statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage))
|
||||
{
|
||||
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
|
||||
}
|
||||
statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
|
||||
}
|
||||
|
||||
if (query.HasSubtitles.HasValue)
|
||||
|
@ -4169,15 +4181,11 @@ namespace Emby.Server.Implementations.Data
|
|||
if (query.Years.Length == 1)
|
||||
{
|
||||
whereClauses.Add("ProductionYear=@Years");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (query.Years.Length > 1)
|
||||
{
|
||||
var val = string.Join(',', query.Years);
|
||||
|
||||
whereClauses.Add("ProductionYear in (" + val + ")");
|
||||
}
|
||||
|
||||
|
@ -4185,10 +4193,7 @@ namespace Emby.Server.Implementations.Data
|
|||
if (isVirtualItem.HasValue)
|
||||
{
|
||||
whereClauses.Add("IsVirtualItem=@IsVirtualItem");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@IsVirtualItem", isVirtualItem.Value);
|
||||
}
|
||||
statement?.TryBind("@IsVirtualItem", isVirtualItem.Value);
|
||||
}
|
||||
|
||||
if (query.IsSpecialSeason.HasValue)
|
||||
|
@ -4219,31 +4224,22 @@ namespace Emby.Server.Implementations.Data
|
|||
if (queryMediaTypes.Length == 1)
|
||||
{
|
||||
whereClauses.Add("MediaType=@MediaTypes");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@MediaTypes", queryMediaTypes[0]);
|
||||
}
|
||||
statement?.TryBind("@MediaTypes", queryMediaTypes[0]);
|
||||
}
|
||||
else if (queryMediaTypes.Length > 1)
|
||||
{
|
||||
var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
|
||||
|
||||
whereClauses.Add("MediaType in (" + val + ")");
|
||||
}
|
||||
|
||||
if (query.ItemIds.Length > 0)
|
||||
{
|
||||
var includeIds = new List<string>();
|
||||
|
||||
var index = 0;
|
||||
foreach (var id in query.ItemIds)
|
||||
{
|
||||
includeIds.Add("Guid = @IncludeId" + index);
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@IncludeId" + index, id);
|
||||
}
|
||||
|
||||
statement?.TryBind("@IncludeId" + index, id);
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -4253,16 +4249,11 @@ namespace Emby.Server.Implementations.Data
|
|||
if (query.ExcludeItemIds.Length > 0)
|
||||
{
|
||||
var excludeIds = new List<string>();
|
||||
|
||||
var index = 0;
|
||||
foreach (var id in query.ExcludeItemIds)
|
||||
{
|
||||
excludeIds.Add("Guid <> @ExcludeId" + index);
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@ExcludeId" + index, id);
|
||||
}
|
||||
|
||||
statement?.TryBind("@ExcludeId" + index, id);
|
||||
index++;
|
||||
}
|
||||
|
||||
|
@ -4283,11 +4274,7 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
var paramName = "@ExcludeProviderId" + index;
|
||||
excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
|
||||
}
|
||||
|
||||
statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
|
||||
index++;
|
||||
|
||||
break;
|
||||
|
@ -4312,7 +4299,7 @@ namespace Emby.Server.Implementations.Data
|
|||
}
|
||||
|
||||
// TODO this seems to be an idea for a better schema where ProviderIds are their own table
|
||||
// buut this is not implemented
|
||||
// but this is not implemented
|
||||
// hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")");
|
||||
|
||||
// TODO this is a really BAD way to do it since the pair:
|
||||
|
@ -4326,11 +4313,7 @@ namespace Emby.Server.Implementations.Data
|
|||
hasProviderIds.Add("ProviderIds like " + paramName);
|
||||
|
||||
// this replaces the placeholder with a value, here: %key=val%
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
|
||||
}
|
||||
|
||||
statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
|
||||
index++;
|
||||
|
||||
break;
|
||||
|
@ -4407,11 +4390,7 @@ namespace Emby.Server.Implementations.Data
|
|||
if (query.AncestorIds.Length == 1)
|
||||
{
|
||||
whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)");
|
||||
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@AncestorId", query.AncestorIds[0]);
|
||||
}
|
||||
statement?.TryBind("@AncestorId", query.AncestorIds[0]);
|
||||
}
|
||||
|
||||
if (query.AncestorIds.Length > 1)
|
||||
|
@ -4424,39 +4403,13 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
|
||||
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
|
||||
}
|
||||
statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey))
|
||||
{
|
||||
whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey");
|
||||
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.BlockUnratedItems.Length == 1)
|
||||
{
|
||||
whereClauses.Add("(InheritedParentalRatingValue > 0 or UnratedType <> @UnratedType)");
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind("@UnratedType", query.BlockUnratedItems[0].ToString());
|
||||
}
|
||||
}
|
||||
|
||||
if (query.BlockUnratedItems.Length > 1)
|
||||
{
|
||||
var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
||||
whereClauses.Add(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
|
||||
inClause));
|
||||
statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey);
|
||||
}
|
||||
|
||||
if (query.ExcludeInheritedTags.Length > 0)
|
||||
|
@ -4605,6 +4558,7 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
return whereClauses;
|
||||
}
|
||||
#nullable disable
|
||||
|
||||
/// <summary>
|
||||
/// Formats a where clause for the specified provider.
|
||||
|
@ -4811,22 +4765,20 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
commandText.Append(" LIMIT ").Append(query.Limit);
|
||||
}
|
||||
|
||||
var list = new List<string>();
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||
{
|
||||
var list = new List<string>();
|
||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||
// Run this again to bind the params
|
||||
GetPeopleWhereClauses(query, statement);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
// Run this again to bind the params
|
||||
GetPeopleWhereClauses(query, statement);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(row.GetString(0));
|
||||
}
|
||||
list.Add(row.GetString(0));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
|
||||
|
@ -4851,23 +4803,20 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
commandText += " LIMIT " + query.Limit;
|
||||
}
|
||||
|
||||
var list = new List<PersonInfo>();
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, commandText))
|
||||
{
|
||||
var list = new List<PersonInfo>();
|
||||
// Run this again to bind the params
|
||||
GetPeopleWhereClauses(query, statement);
|
||||
|
||||
using (var statement = PrepareStatement(connection, commandText))
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
// Run this again to bind the params
|
||||
GetPeopleWhereClauses(query, statement);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(GetPerson(row));
|
||||
}
|
||||
list.Add(GetPerson(row));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement)
|
||||
|
@ -5598,7 +5547,7 @@ AND Type = @InternalPersonType)");
|
|||
|
||||
statement.TryBind("@Name" + index, person.Name);
|
||||
statement.TryBind("@Role" + index, person.Role);
|
||||
statement.TryBind("@PersonType" + index, person.Type);
|
||||
statement.TryBind("@PersonType" + index, person.Type.ToString());
|
||||
statement.TryBind("@SortOrder" + index, person.SortOrder);
|
||||
statement.TryBind("@ListOrder" + index, listIndex);
|
||||
|
||||
|
@ -5627,9 +5576,10 @@ AND Type = @InternalPersonType)");
|
|||
item.Role = role;
|
||||
}
|
||||
|
||||
if (reader.TryGetString(3, out var type))
|
||||
if (reader.TryGetString(3, out var type)
|
||||
&& Enum.TryParse(type, true, out PersonKind personKind))
|
||||
{
|
||||
item.Type = type;
|
||||
item.Type = personKind;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(4, out var sortOrder))
|
||||
|
|
|
@ -7,7 +7,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
|
@ -18,33 +18,32 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public SqliteUserDataRepository(
|
||||
ILogger<SqliteUserDataRepository> logger,
|
||||
IApplicationPaths appPaths)
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager)
|
||||
: base(logger)
|
||||
{
|
||||
DbFilePath = Path.Combine(appPaths.DataPath, "library.db");
|
||||
_userManager = userManager;
|
||||
|
||||
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database.
|
||||
/// </summary>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
/// <param name="dbLock">The lock to use for database IO.</param>
|
||||
/// <param name="dbConnection">The connection to use for database IO.</param>
|
||||
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
|
||||
public override void Initialize()
|
||||
{
|
||||
WriteLock.Dispose();
|
||||
WriteLock = dbLock;
|
||||
WriteConnection?.Dispose();
|
||||
WriteConnection = dbConnection;
|
||||
base.Initialize();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
var userDatasTableExists = TableExists(connection, "UserDatas");
|
||||
var userDataTableExists = TableExists(connection, "userdata");
|
||||
|
||||
var users = userDatasTableExists ? null : userManager.Users;
|
||||
var users = userDatasTableExists ? null : _userManager.Users;
|
||||
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
|
@ -371,20 +370,5 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
return userData;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2215
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and
|
||||
/// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>.
|
||||
/// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>.
|
||||
/// </remarks>
|
||||
protected override void Dispose(bool dispose)
|
||||
{
|
||||
// The write lock and connection for the item repository are shared with the user data repository
|
||||
// since they point to the same database. The item repo has responsibility for disposing these two objects,
|
||||
// so the user data repo should not attempt to dispose them as well
|
||||
}
|
||||
#pragma warning restore CA2215
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ using System.Collections.Generic;
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
|
@ -83,22 +82,23 @@ namespace Emby.Server.Implementations.Dto
|
|||
/// <inheritdoc />
|
||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
||||
{
|
||||
var returnItems = new BaseItemDto[items.Count];
|
||||
var programTuples = new List<(BaseItem, BaseItemDto)>();
|
||||
var channelTuples = new List<(BaseItemDto, LiveTvChannel)>();
|
||||
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
|
||||
var returnItems = new BaseItemDto[accessibleItems.Count];
|
||||
List<(BaseItem, BaseItemDto)> programTuples = null;
|
||||
List<(BaseItemDto, LiveTvChannel)> channelTuples = null;
|
||||
|
||||
for (int index = 0; index < items.Count; index++)
|
||||
for (int index = 0; index < accessibleItems.Count; index++)
|
||||
{
|
||||
var item = items[index];
|
||||
var item = accessibleItems[index];
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
channelTuples.Add((dto, tvChannel));
|
||||
(channelTuples ??= new()).Add((dto, tvChannel));
|
||||
}
|
||||
else if (item is LiveTvProgram)
|
||||
{
|
||||
programTuples.Add((item, dto));
|
||||
(programTuples ??= new()).Add((item, dto));
|
||||
}
|
||||
|
||||
if (item is IItemByName byName)
|
||||
|
@ -121,12 +121,12 @@ namespace Emby.Server.Implementations.Dto
|
|||
returnItems[index] = dto;
|
||||
}
|
||||
|
||||
if (programTuples.Count > 0)
|
||||
if (programTuples is not null)
|
||||
{
|
||||
LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (channelTuples.Count > 0)
|
||||
if (channelTuples is not null)
|
||||
{
|
||||
LivetvManager.AddChannelInfo(channelTuples, options, user);
|
||||
}
|
||||
|
@ -522,32 +522,32 @@ namespace Emby.Server.Implementations.Dto
|
|||
var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
|
||||
.ThenBy(i =>
|
||||
{
|
||||
if (i.IsType(PersonType.Actor))
|
||||
if (i.IsType(PersonKind.Actor))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (i.IsType(PersonType.GuestStar))
|
||||
if (i.IsType(PersonKind.GuestStar))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (i.IsType(PersonType.Director))
|
||||
if (i.IsType(PersonKind.Director))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (i.IsType(PersonType.Writer))
|
||||
if (i.IsType(PersonKind.Writer))
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (i.IsType(PersonType.Producer))
|
||||
if (i.IsType(PersonKind.Producer))
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (i.IsType(PersonType.Composer))
|
||||
if (i.IsType(PersonKind.Composer))
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
@ -571,9 +571,7 @@ namespace Emby.Server.Implementations.Dto
|
|||
return null;
|
||||
}
|
||||
}).Where(i => i is not null)
|
||||
.Where(i => user is null ?
|
||||
true :
|
||||
i.IsVisible(user))
|
||||
.Where(i => user is null || i.IsVisible(user))
|
||||
.DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
@ -908,6 +906,7 @@ namespace Emby.Server.Implementations.Dto
|
|||
// Add audio info
|
||||
if (item is Audio audio)
|
||||
{
|
||||
dto.LUFS = audio.LUFS;
|
||||
dto.Album = audio.Album;
|
||||
if (audio.ExtraType.HasValue)
|
||||
{
|
||||
|
|
|
@ -12,6 +12,7 @@ using System.Threading.Tasks;
|
|||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -26,12 +27,8 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
{
|
||||
public class LibraryChangedNotifier : IServerEntryPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The library update duration.
|
||||
/// </summary>
|
||||
private const int LibraryUpdateDuration = 30000;
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
@ -51,12 +48,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
public LibraryChangedNotifier(
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager configurationManager,
|
||||
ISessionManager sessionManager,
|
||||
IUserManager userManager,
|
||||
ILogger<LibraryChangedNotifier> logger,
|
||||
IProviderManager providerManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_configurationManager = configurationManager;
|
||||
_sessionManager = sessionManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
|
@ -196,12 +195,12 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
LibraryUpdateTimer = new Timer(
|
||||
LibraryUpdateTimerCallback,
|
||||
null,
|
||||
LibraryUpdateDuration,
|
||||
Timeout.Infinite);
|
||||
TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration),
|
||||
Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
if (e.Item.GetParent() is Folder parent)
|
||||
|
@ -229,11 +228,11 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
{
|
||||
if (LibraryUpdateTimer is null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
_itemsUpdated.Add(e.Item);
|
||||
|
@ -256,11 +255,11 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
{
|
||||
if (LibraryUpdateTimer is null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
if (e.Parent is Folder parent)
|
||||
|
@ -276,25 +275,31 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
/// Libraries the update timer callback.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
private void LibraryUpdateTimerCallback(object state)
|
||||
private async void LibraryUpdateTimerCallback(object state)
|
||||
{
|
||||
List<Folder> foldersAddedTo;
|
||||
List<Folder> foldersRemovedFrom;
|
||||
List<BaseItem> itemsUpdated;
|
||||
List<BaseItem> itemsAdded;
|
||||
List<BaseItem> itemsRemoved;
|
||||
lock (_libraryChangedSyncLock)
|
||||
{
|
||||
// Remove dupes in case some were saved multiple times
|
||||
var foldersAddedTo = _foldersAddedTo
|
||||
foldersAddedTo = _foldersAddedTo
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
var foldersRemovedFrom = _foldersRemovedFrom
|
||||
foldersRemovedFrom = _foldersRemovedFrom
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
var itemsUpdated = _itemsUpdated
|
||||
itemsUpdated = _itemsUpdated
|
||||
.Where(i => !_itemsAdded.Contains(i))
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
|
||||
itemsAdded = _itemsAdded.ToList();
|
||||
itemsRemoved = _itemsRemoved.ToList();
|
||||
|
||||
if (LibraryUpdateTimer is not null)
|
||||
{
|
||||
|
@ -308,6 +313,8 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
_foldersAddedTo.Clear();
|
||||
_foldersRemovedFrom.Clear();
|
||||
}
|
||||
|
||||
await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -87,29 +87,30 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
}
|
||||
}
|
||||
|
||||
private void UpdateTimerCallback(object? state)
|
||||
private async void UpdateTimerCallback(object? state)
|
||||
{
|
||||
List<KeyValuePair<Guid, List<BaseItem>>> changes;
|
||||
lock (_syncLock)
|
||||
{
|
||||
// Remove dupes in case some were saved multiple times
|
||||
var changes = _changedItems.ToList();
|
||||
changes = _changedItems.ToList();
|
||||
_changedItems.Clear();
|
||||
|
||||
SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
if (_updateTimer is not null)
|
||||
{
|
||||
_updateTimer.Dispose();
|
||||
_updateTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var pair in changes)
|
||||
foreach ((var key, var value) in changes)
|
||||
{
|
||||
await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false);
|
||||
await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -85,14 +86,15 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
/// <value>The state.</value>
|
||||
public WebSocketState State => _socket.State;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the message.</typeparam>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
||||
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
||||
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
|
||||
|
@ -171,7 +173,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
return;
|
||||
}
|
||||
|
||||
WebSocketMessage<object>? stub;
|
||||
InboundWebSocketMessage<object>? stub;
|
||||
long bytesConsumed;
|
||||
try
|
||||
{
|
||||
|
@ -212,10 +214,10 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
}
|
||||
}
|
||||
|
||||
internal WebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed)
|
||||
internal InboundWebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed)
|
||||
{
|
||||
var jsonReader = new Utf8JsonReader(bytes);
|
||||
var ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions);
|
||||
var ret = JsonSerializer.Deserialize<InboundWebSocketMessage<object>>(ref jsonReader, _jsonOptions);
|
||||
bytesConsumed = jsonReader.BytesConsumed;
|
||||
return ret;
|
||||
}
|
||||
|
@ -224,11 +226,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
{
|
||||
LastKeepAliveDate = DateTime.UtcNow;
|
||||
return SendAsync(
|
||||
new WebSocketMessage<string>
|
||||
{
|
||||
MessageId = Guid.NewGuid(),
|
||||
MessageType = SessionMessageType.KeepAlive
|
||||
},
|
||||
new OutboundKeepAliveMessage(),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
public class ExtendedFileSystemInfo
|
||||
{
|
||||
public bool IsHidden { get; set; }
|
||||
|
||||
public bool IsReadOnly { get; set; }
|
||||
|
||||
public bool Exists { get; set; }
|
||||
}
|
||||
}
|
|
@ -20,6 +20,14 @@ namespace Emby.Server.Implementations.IO
|
|||
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
|
||||
private readonly string _tempPath;
|
||||
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
|
||||
private static readonly char[] _invalidPathCharacters =
|
||||
{
|
||||
'\"', '<', '>', '|', '\0',
|
||||
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
|
||||
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
|
||||
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
|
||||
(char)31, ':', '*', '?', '\\', '/'
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
|
||||
|
@ -267,25 +275,6 @@ namespace Emby.Server.Implementations.IO
|
|||
return result;
|
||||
}
|
||||
|
||||
private static ExtendedFileSystemInfo GetExtendedFileSystemInfo(string path)
|
||||
{
|
||||
var result = new ExtendedFileSystemInfo();
|
||||
|
||||
var info = new FileInfo(path);
|
||||
|
||||
if (info.Exists)
|
||||
{
|
||||
result.Exists = true;
|
||||
|
||||
var attributes = info.Attributes;
|
||||
|
||||
result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
||||
result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a filename and removes invalid characters.
|
||||
/// </summary>
|
||||
|
@ -294,8 +283,7 @@ namespace Emby.Server.Implementations.IO
|
|||
/// <exception cref="ArgumentNullException">The filename is null.</exception>
|
||||
public string GetValidFilename(string filename)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
var first = filename.IndexOfAny(invalid);
|
||||
var first = filename.IndexOfAny(_invalidPathCharacters);
|
||||
if (first == -1)
|
||||
{
|
||||
// Fast path for clean strings
|
||||
|
@ -304,7 +292,7 @@ namespace Emby.Server.Implementations.IO
|
|||
|
||||
return string.Create(
|
||||
filename.Length,
|
||||
(filename, invalid, first),
|
||||
(filename, _invalidPathCharacters, first),
|
||||
(chars, state) =>
|
||||
{
|
||||
state.filename.AsSpan().CopyTo(chars);
|
||||
|
@ -312,7 +300,7 @@ namespace Emby.Server.Implementations.IO
|
|||
chars[state.first++] = ' ';
|
||||
|
||||
var len = chars.Length;
|
||||
foreach (var c in state.invalid)
|
||||
foreach (var c in state._invalidPathCharacters)
|
||||
{
|
||||
for (int i = state.first; i < len; i++)
|
||||
{
|
||||
|
@ -403,19 +391,18 @@ namespace Emby.Server.Implementations.IO
|
|||
return;
|
||||
}
|
||||
|
||||
var info = GetExtendedFileSystemInfo(path);
|
||||
var info = new FileInfo(path);
|
||||
|
||||
if (info.Exists && info.IsHidden != isHidden)
|
||||
if (info.Exists &&
|
||||
((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
|
||||
{
|
||||
if (isHidden)
|
||||
{
|
||||
File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden);
|
||||
File.SetAttributes(path, info.Attributes | FileAttributes.Hidden);
|
||||
}
|
||||
else
|
||||
{
|
||||
var attributes = File.GetAttributes(path);
|
||||
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
|
||||
File.SetAttributes(path, attributes);
|
||||
File.SetAttributes(path, info.Attributes & ~FileAttributes.Hidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -428,19 +415,20 @@ namespace Emby.Server.Implementations.IO
|
|||
return;
|
||||
}
|
||||
|
||||
var info = GetExtendedFileSystemInfo(path);
|
||||
var info = new FileInfo(path);
|
||||
|
||||
if (!info.Exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
|
||||
if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
|
||||
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var attributes = File.GetAttributes(path);
|
||||
var attributes = info.Attributes;
|
||||
|
||||
if (readOnly)
|
||||
{
|
||||
|
@ -448,7 +436,7 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
else
|
||||
{
|
||||
attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
|
||||
attributes &= ~FileAttributes.ReadOnly;
|
||||
}
|
||||
|
||||
if (isHidden)
|
||||
|
@ -457,17 +445,12 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
else
|
||||
{
|
||||
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
|
||||
attributes &= ~FileAttributes.Hidden;
|
||||
}
|
||||
|
||||
File.SetAttributes(path, attributes);
|
||||
}
|
||||
|
||||
private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
|
||||
{
|
||||
return attributes & ~attributesToRemove;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the files.
|
||||
/// </summary>
|
||||
|
|
|
@ -113,6 +113,7 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public LibraryManager(
|
||||
IServerApplicationHost appHost,
|
||||
ILoggerFactory loggerFactory,
|
||||
|
@ -128,7 +129,8 @@ namespace Emby.Server.Implementations.Library
|
|||
IItemRepository itemRepository,
|
||||
IImageProcessor imageProcessor,
|
||||
IMemoryCache memoryCache,
|
||||
NamingOptions namingOptions)
|
||||
NamingOptions namingOptions,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_logger = loggerFactory.CreateLogger<LibraryManager>();
|
||||
|
@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.Library
|
|||
_memoryCache = memoryCache;
|
||||
_namingOptions = namingOptions;
|
||||
|
||||
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions);
|
||||
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
|
||||
|
||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||
|
||||
|
@ -356,8 +358,8 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
var children = item.IsFolder
|
||||
? ((Folder)item).GetRecursiveChildren(false).ToList()
|
||||
: new List<BaseItem>();
|
||||
? ((Folder)item).GetRecursiveChildren(false)
|
||||
: Enumerable.Empty<BaseItem>();
|
||||
|
||||
foreach (var metadataPath in GetMetadataPaths(item, children))
|
||||
{
|
||||
|
@ -537,7 +539,7 @@ namespace Emby.Server.Implementations.Library
|
|||
collectionType = GetContentTypeOverride(fullPath, true);
|
||||
}
|
||||
|
||||
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
|
||||
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
|
||||
{
|
||||
Parent = parent,
|
||||
FileInfo = fileInfo,
|
||||
|
@ -1253,7 +1255,7 @@ namespace Emby.Server.Implementations.Library
|
|||
var parent = GetItemById(query.ParentId);
|
||||
if (parent is not null)
|
||||
{
|
||||
SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
|
||||
SetTopParentIdsOrAncestors(query, new[] { parent });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1262,7 +1264,14 @@ namespace Emby.Server.Implementations.Library
|
|||
AddUserToQuery(query, query.User, allowExternalContent);
|
||||
}
|
||||
|
||||
return _itemRepository.GetItemList(query);
|
||||
var itemList = _itemRepository.GetItemList(query);
|
||||
var user = query.User;
|
||||
if (user is not null)
|
||||
{
|
||||
return itemList.Where(i => i.IsVisible(user)).ToList();
|
||||
}
|
||||
|
||||
return itemList;
|
||||
}
|
||||
|
||||
public List<BaseItem> GetItemList(InternalItemsQuery query)
|
||||
|
@ -1277,7 +1286,7 @@ namespace Emby.Server.Implementations.Library
|
|||
var parent = GetItemById(query.ParentId);
|
||||
if (parent is not null)
|
||||
{
|
||||
SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
|
||||
SetTopParentIdsOrAncestors(query, new[] { parent });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1435,7 +1444,7 @@ namespace Emby.Server.Implementations.Library
|
|||
var parent = GetItemById(query.ParentId);
|
||||
if (parent is not null)
|
||||
{
|
||||
SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
|
||||
SetTopParentIdsOrAncestors(query, new[] { parent });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1455,7 +1464,7 @@ namespace Emby.Server.Implementations.Library
|
|||
_itemRepository.GetItemList(query));
|
||||
}
|
||||
|
||||
private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents)
|
||||
private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
|
||||
{
|
||||
if (parents.All(i => i is ICollectionFolder || i is UserView))
|
||||
{
|
||||
|
@ -1501,6 +1510,12 @@ namespace Emby.Server.Implementations.Library
|
|||
});
|
||||
|
||||
query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
|
||||
|
||||
// Prevent searching in all libraries due to empty filter
|
||||
if (query.TopParentIds.Length == 0)
|
||||
{
|
||||
query.TopParentIds = new[] { Guid.NewGuid() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1602,7 +1617,7 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
_logger.LogError(ex, "Error getting intros");
|
||||
|
||||
return new List<IntroInfo>();
|
||||
return Enumerable.Empty<IntroInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1877,7 +1892,7 @@ namespace Emby.Server.Implementations.Library
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
||||
size = new ImageDimensions(0, 0);
|
||||
size = default;
|
||||
image.Width = 0;
|
||||
image.Height = 0;
|
||||
}
|
||||
|
@ -2054,7 +2069,9 @@ namespace Emby.Server.Implementations.Library
|
|||
.Find(folder => folder is CollectionFolder) as CollectionFolder;
|
||||
}
|
||||
|
||||
return collectionFolder is null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
|
||||
return collectionFolder is null
|
||||
? new LibraryOptions()
|
||||
: collectionFolder.GetLibraryOptions();
|
||||
}
|
||||
|
||||
public string GetContentType(BaseItem item)
|
||||
|
@ -2741,9 +2758,7 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
})
|
||||
.Where(i => i is not null)
|
||||
.Where(i => query.User is null ?
|
||||
true :
|
||||
i.IsVisible(query.User))
|
||||
.Where(i => query.User is null || i.IsVisible(query.User))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
@ -2876,7 +2891,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
|
||||
{
|
||||
var personsToSave = new List<BaseItem>();
|
||||
List<BaseItem> personsToSave = null;
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
|
@ -2918,12 +2933,12 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (saveEntity)
|
||||
{
|
||||
personsToSave.Add(personEntity);
|
||||
(personsToSave ??= new()).Add(personEntity);
|
||||
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (personsToSave.Count > 0)
|
||||
if (personsToSave is not null)
|
||||
{
|
||||
CreateItems(personsToSave, null, CancellationToken.None);
|
||||
}
|
||||
|
@ -3085,22 +3100,19 @@ namespace Emby.Server.Implementations.Library
|
|||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
var removeList = new List<NameValuePair>();
|
||||
List<NameValuePair> removeList = null;
|
||||
|
||||
foreach (var contentType in _configurationManager.Configuration.ContentTypes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contentType.Name))
|
||||
{
|
||||
removeList.Add(contentType);
|
||||
}
|
||||
else if (_fileSystem.AreEqual(path, contentType.Name)
|
||||
if (string.IsNullOrWhiteSpace(contentType.Name)
|
||||
|| _fileSystem.AreEqual(path, contentType.Name)
|
||||
|| _fileSystem.ContainsSubPath(path, contentType.Name))
|
||||
{
|
||||
removeList.Add(contentType);
|
||||
(removeList ??= new()).Add(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
if (removeList.Count > 0)
|
||||
if (removeList is not null)
|
||||
{
|
||||
_configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
|
||||
.Except(removeList)
|
||||
|
|
|
@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Library
|
|||
// If file is strm or main media stream is missing, force a metadata refresh with remote probing
|
||||
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
|
||||
&& (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
|
||||
|| (item.MediaType == MediaType.Video && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Video))
|
||||
|| (item.MediaType == MediaType.Audio && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio))))
|
||||
|| (item.MediaType == MediaType.Video && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Video))
|
||||
|| (item.MediaType == MediaType.Audio && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Audio))))
|
||||
{
|
||||
await item.RefreshMetadata(
|
||||
new MetadataRefreshOptions(_directoryService)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using MediaBrowser.Common.Providers;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
|
@ -86,24 +87,8 @@ namespace Emby.Server.Implementations.Library
|
|||
return false;
|
||||
}
|
||||
|
||||
char oldDirectorySeparatorChar;
|
||||
char newDirectorySeparatorChar;
|
||||
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
|
||||
// The reasoning behind this is that a forward slash likely means it's a Linux path and
|
||||
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
|
||||
if (newSubPath.Contains('/', StringComparison.Ordinal))
|
||||
{
|
||||
oldDirectorySeparatorChar = '\\';
|
||||
newDirectorySeparatorChar = '/';
|
||||
}
|
||||
else
|
||||
{
|
||||
oldDirectorySeparatorChar = '/';
|
||||
newDirectorySeparatorChar = '\\';
|
||||
}
|
||||
|
||||
path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
|
||||
subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
|
||||
subPath = subPath.NormalizePath(out var newDirectorySeparatorChar);
|
||||
path = path.NormalizePath(newDirectorySeparatorChar);
|
||||
|
||||
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
|
||||
// when the sub path matches a similar but in-complete subpath
|
||||
|
@ -127,5 +112,82 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the full resolved path and normalizes path separators to the <see cref="Path.DirectorySeparatorChar"/>.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to canonicalize.</param>
|
||||
/// <returns>The fully expanded, normalized path.</returns>
|
||||
public static string Canonicalize(this string path)
|
||||
{
|
||||
return Path.GetFullPath(path).NormalizePath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the path's directory separator character to the currently defined <see cref="Path.DirectorySeparatorChar"/>.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to normalize.</param>
|
||||
/// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
|
||||
[return: NotNullIfNotNull(nameof(path))]
|
||||
public static string? NormalizePath(this string? path)
|
||||
{
|
||||
return path.NormalizePath(Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the path's directory separator character.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to normalize.</param>
|
||||
/// <param name="separator">The separator character the path now uses or <see langword="null"/>.</param>
|
||||
/// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
|
||||
[return: NotNullIfNotNull(nameof(path))]
|
||||
public static string? NormalizePath(this string? path, out char separator)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
separator = default;
|
||||
return path;
|
||||
}
|
||||
|
||||
var newSeparator = '\\';
|
||||
|
||||
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
|
||||
// The reasoning behind this is that a forward slash likely means it's a Linux path and
|
||||
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
|
||||
if (path.Contains('/', StringComparison.Ordinal))
|
||||
{
|
||||
newSeparator = '/';
|
||||
}
|
||||
|
||||
separator = newSeparator;
|
||||
|
||||
return path.NormalizePath(newSeparator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the path's directory separator character to the specified character.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to normalize.</param>
|
||||
/// <param name="newSeparator">The replacement directory separator character. Must be a valid directory separator.</param>
|
||||
/// <returns>The normalized path.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the new separator character is not a directory separator.</exception>
|
||||
[return: NotNullIfNotNull(nameof(path))]
|
||||
public static string? NormalizePath(this string? path, char newSeparator)
|
||||
{
|
||||
const char Bs = '\\';
|
||||
const char Fs = '/';
|
||||
|
||||
if (!(newSeparator == Bs || newSeparator == Fs))
|
||||
{
|
||||
throw new ArgumentException("The character must be a directory separator.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return newSeparator == Bs ? path.Replace(Fs, newSeparator) : path.Replace(Bs, newSeparator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -158,7 +158,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
|
||||
{
|
||||
var files = new List<FileSystemMetadata>();
|
||||
var items = new List<BaseItem>();
|
||||
var leftOver = new List<FileSystemMetadata>();
|
||||
|
||||
// Loop through each child file/folder and see if we find a video
|
||||
|
@ -180,7 +179,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
var result = new MultiItemResolverResult
|
||||
{
|
||||
ExtraFiles = leftOver,
|
||||
Items = items
|
||||
Items = new List<BaseItem>()
|
||||
};
|
||||
|
||||
var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent);
|
||||
|
@ -193,7 +192,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
continue;
|
||||
}
|
||||
|
||||
if (resolvedItem.Files.Count == 0)
|
||||
// Until multi-part books are handled letting files stack hides them from browsing in the client
|
||||
if (resolvedItem.Files.Count == 0 || resolvedItem.Extras.Count > 0 || resolvedItem.AlternateVersions.Count > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -25,16 +25,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
{
|
||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_namingOptions = namingOptions;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -109,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
}
|
||||
|
||||
// If args contains music it's a music album
|
||||
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
|
||||
if (ContainsMusic(args.FileSystemChildren, true, _directoryService))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -18,19 +19,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
public class MusicArtistResolver : ItemResolver<MusicArtist>
|
||||
{
|
||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||
private NamingOptions _namingOptions;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
|
||||
/// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public MusicArtistResolver(
|
||||
ILogger<MusicAlbumResolver> logger,
|
||||
NamingOptions namingOptions)
|
||||
NamingOptions namingOptions,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_namingOptions = namingOptions;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -78,9 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
return null;
|
||||
}
|
||||
|
||||
var directoryService = args.DirectoryService;
|
||||
|
||||
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
|
||||
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
|
||||
|
||||
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
|
||||
|
||||
|
@ -97,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
}
|
||||
|
||||
// If we contain a music album assume we are an artist folder
|
||||
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
|
||||
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
|
||||
{
|
||||
// Stop once we see a music album
|
||||
state.Stop();
|
||||
|
|
|
@ -25,14 +25,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions)
|
||||
protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
NamingOptions = namingOptions;
|
||||
DirectoryService = directoryService;
|
||||
}
|
||||
|
||||
protected NamingOptions NamingOptions { get; }
|
||||
|
||||
protected IDirectoryService DirectoryService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
|
@ -65,13 +68,26 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
var filename = child.Name;
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
|
||||
if (IsDvdDirectory(child.FullName, filename, DirectoryService))
|
||||
{
|
||||
videoType = VideoType.Dvd;
|
||||
var videoTmp = new TVideoType
|
||||
{
|
||||
Path = args.Path,
|
||||
VideoType = VideoType.Dvd
|
||||
};
|
||||
Set3DFormat(videoTmp);
|
||||
return videoTmp;
|
||||
}
|
||||
else if (IsBluRayDirectory(filename))
|
||||
|
||||
if (IsBluRayDirectory(filename))
|
||||
{
|
||||
videoType = VideoType.BluRay;
|
||||
var videoTmp = new TVideoType
|
||||
{
|
||||
Path = args.Path,
|
||||
VideoType = VideoType.BluRay
|
||||
};
|
||||
Set3DFormat(videoTmp);
|
||||
return videoTmp;
|
||||
}
|
||||
}
|
||||
else if (IsDvdFile(filename))
|
||||
|
|
|
@ -4,6 +4,8 @@ using System.IO;
|
|||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -14,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
/// <summary>
|
||||
/// Resolves a Path into a Video or Video subclass.
|
||||
/// </summary>
|
||||
internal class ExtraResolver
|
||||
internal class ExtraResolver : BaseVideoResolver<Video>
|
||||
{
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IItemResolver[] _trailerResolvers;
|
||||
|
@ -25,11 +27,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
|
||||
public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions)
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
_namingOptions = namingOptions;
|
||||
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) };
|
||||
_videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) };
|
||||
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
|
||||
_videoResolvers = new IItemResolver[] { this };
|
||||
}
|
||||
|
||||
protected override Video Resolve(ItemResolveArgs args)
|
||||
{
|
||||
return ResolveVideo<Video>(args, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
|
@ -18,8 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions)
|
||||
: base(logger, namingOptions)
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,8 +43,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions)
|
||||
: base(logger, namingOptions)
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
}
|
||||
|
@ -97,12 +98,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
|
||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||
movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
|
@ -118,12 +119,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
return null;
|
||||
}
|
||||
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
|
||||
}
|
||||
|
||||
// ignore extras
|
||||
|
@ -313,13 +314,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
return result;
|
||||
}
|
||||
|
||||
private static bool IsIgnored(string filename)
|
||||
{
|
||||
// Ignore samples
|
||||
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
return m.Success;
|
||||
}
|
||||
private static bool IsIgnored(ReadOnlySpan<char> filename)
|
||||
=> Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
|
||||
{
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
@ -12,15 +10,20 @@ using Jellyfin.Extensions;
|
|||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class PhotoResolver.
|
||||
/// </summary>
|
||||
public class PhotoResolver : ItemResolver<Photo>
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
|
@ -35,10 +38,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
"default"
|
||||
};
|
||||
|
||||
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PhotoResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
_namingOptions = namingOptions;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -61,7 +71,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
var filename = Path.GetFileNameWithoutExtension(args.Path);
|
||||
|
||||
// Make sure the image doesn't belong to a video file
|
||||
var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path));
|
||||
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
{
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
// It's a boxset if the path is a directory with [playlist] in it's the name
|
||||
// It's a boxset if the path is a directory with [playlist] in its name
|
||||
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
|
@ -42,7 +42,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
return new Playlist
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
|
||||
Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim(),
|
||||
OpenAccess = true
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -53,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
return new Playlist
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = filename
|
||||
Name = filename,
|
||||
OpenAccess = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +72,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
Path = args.Path,
|
||||
Name = Path.GetFileNameWithoutExtension(args.Path),
|
||||
IsInMixedFolder = true,
|
||||
PlaylistMediaType = MediaType.Audio
|
||||
PlaylistMediaType = MediaType.Audio,
|
||||
OpenAccess = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -20,8 +21,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions)
|
||||
: base(logger, namingOptions)
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -81,14 +81,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
if (season.IndexNumber.HasValue)
|
||||
{
|
||||
var seasonNumber = season.IndexNumber.Value;
|
||||
|
||||
season.Name = seasonNumber == 0 ?
|
||||
args.LibraryOptions.SeasonZeroDisplayName :
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("NameSeasonNumber"),
|
||||
seasonNumber,
|
||||
args.LibraryOptions.PreferredMetadataLanguage);
|
||||
if (string.IsNullOrEmpty(season.Name))
|
||||
{
|
||||
var seasonNames = series.SeasonNames;
|
||||
if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
|
||||
{
|
||||
season.Name = seasonName;
|
||||
}
|
||||
else
|
||||
{
|
||||
season.Name = seasonNumber == 0 ?
|
||||
args.LibraryOptions.SeasonZeroDisplayName :
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("NameSeasonNumber"),
|
||||
seasonNumber,
|
||||
args.LibraryOptions.PreferredMetadataLanguage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return season;
|
||||
|
|
|
@ -184,6 +184,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
{
|
||||
var justName = Path.GetFileName(path.AsSpan());
|
||||
|
||||
var imdbId = justName.GetAttributeValue("imdbid");
|
||||
if (!string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.Imdb, imdbId);
|
||||
}
|
||||
|
||||
var tvdbId = justName.GetAttributeValue("tvdbid");
|
||||
if (!string.IsNullOrEmpty(tvdbId))
|
||||
{
|
||||
|
|
|
@ -46,10 +46,9 @@ namespace Emby.Server.Implementations.Library
|
|||
public Folder[] GetUserViews(UserViewQuery query)
|
||||
{
|
||||
var user = _userManager.GetUserById(query.UserId);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
throw new ArgumentException("User Id specified in the query does not exist.", nameof(query));
|
||||
throw new ArgumentException("User id specified in the query does not exist.", nameof(query));
|
||||
}
|
||||
|
||||
var folders = _libraryManager.GetUserRootFolder()
|
||||
|
@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.Library
|
|||
.ToList();
|
||||
|
||||
var groupedFolders = new List<ICollectionFolder>();
|
||||
|
||||
var list = new List<Folder>();
|
||||
|
||||
foreach (var folder in folders)
|
||||
|
@ -66,6 +64,20 @@ namespace Emby.Server.Implementations.Library
|
|||
var collectionFolder = folder as ICollectionFolder;
|
||||
var folderViewType = collectionFolder?.CollectionType;
|
||||
|
||||
// Playlist library requires special handling because the folder only refrences user playlists
|
||||
if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var items = folder.GetItemList(new InternalItemsQuery(user)
|
||||
{
|
||||
ParentId = folder.ParentId
|
||||
});
|
||||
|
||||
if (!items.Any(item => item.IsVisible(user)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (UserView.IsUserSpecific(folder))
|
||||
{
|
||||
list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null));
|
||||
|
@ -111,10 +123,10 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (query.IncludeExternalContent)
|
||||
{
|
||||
var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery
|
||||
var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
|
||||
{
|
||||
UserId = query.UserId
|
||||
});
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
var channels = channelResult.Items;
|
||||
|
||||
|
@ -132,14 +144,12 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
|
||||
|
||||
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
|
||||
|
||||
return list
|
||||
.OrderBy(i =>
|
||||
{
|
||||
var index = Array.IndexOf(orders, i.Id);
|
||||
|
||||
if (index == -1
|
||||
&& i is UserView view
|
||||
&& !view.DisplayParentId.Equals(default))
|
||||
|
@ -286,7 +296,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (parents.Count == 0)
|
||||
{
|
||||
return new List<BaseItem>();
|
||||
return Array.Empty<BaseItem>();
|
||||
}
|
||||
|
||||
if (includeItemTypes.Length == 0)
|
||||
|
|
|
@ -627,10 +627,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
_timerProvider.Update(existingTimer);
|
||||
return Task.FromResult(existingTimer.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("A scheduled recording already exists for this program.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("A scheduled recording already exists for this program.");
|
||||
}
|
||||
|
||||
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
@ -1866,8 +1864,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
{
|
||||
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
|
||||
await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
|
||||
string id;
|
||||
if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out id))
|
||||
if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var id))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "id", null, id).ConfigureAwait(false);
|
||||
}
|
||||
|
@ -2032,7 +2029,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
|
||||
|
||||
var directors = people
|
||||
.Where(i => IsPersonType(i, PersonType.Director))
|
||||
.Where(i => i.IsType(PersonKind.Director))
|
||||
.Select(i => i.Name)
|
||||
.ToList();
|
||||
|
||||
|
@ -2042,7 +2039,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
}
|
||||
|
||||
var writers = people
|
||||
.Where(i => IsPersonType(i, PersonType.Writer))
|
||||
.Where(i => i.IsType(PersonKind.Writer))
|
||||
.Select(i => i.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
@ -2122,10 +2119,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
}
|
||||
}
|
||||
|
||||
private static bool IsPersonType(PersonInfo person, string type)
|
||||
=> string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private LiveTvProgram GetProgramInfoFromCache(string programId)
|
||||
{
|
||||
var query = new InternalItemsQuery
|
||||
|
|
|
@ -415,14 +415,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
{
|
||||
return null;
|
||||
}
|
||||
else if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
|
||||
if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
else
|
||||
{
|
||||
return apiUrl + "/image/" + uri + "?token=" + token;
|
||||
}
|
||||
|
||||
return apiUrl + "/image/" + uri + "?token=" + token;
|
||||
}
|
||||
|
||||
private static double GetAspectRatio(ImageDataDto i)
|
||||
|
@ -463,10 +462,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
}
|
||||
|
||||
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
|
||||
foreach (ReadOnlySpan<char> i in programIds)
|
||||
foreach (var i in programIds)
|
||||
{
|
||||
str.Append('"')
|
||||
.Append(i.Slice(0, 10))
|
||||
.Append(i[..10])
|
||||
.Append("\",");
|
||||
}
|
||||
|
||||
|
@ -570,15 +569,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
_tokens.TryAdd(username, savedToken);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(savedToken.Name) && !string.IsNullOrEmpty(savedToken.Value))
|
||||
if (!string.IsNullOrEmpty(savedToken.Name)
|
||||
&& long.TryParse(savedToken.Value, CultureInfo.InvariantCulture, out long ticks))
|
||||
{
|
||||
if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out long ticks))
|
||||
// If it's under 24 hours old we can still use it
|
||||
if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks)
|
||||
{
|
||||
// If it's under 24 hours old we can still use it
|
||||
if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks)
|
||||
{
|
||||
return savedToken.Name;
|
||||
}
|
||||
return savedToken.Name;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1312,20 +1312,19 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
return 7;
|
||||
}
|
||||
|
||||
private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user)
|
||||
private async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user)
|
||||
{
|
||||
if (user is null)
|
||||
{
|
||||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
|
||||
var folderIds = GetRecordingFolders(user, true)
|
||||
.Select(i => i.Id)
|
||||
.ToList();
|
||||
var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false);
|
||||
var folderIds = Array.ConvertAll(folders, x => x.Id);
|
||||
|
||||
var excludeItemTypes = new List<BaseItemKind>();
|
||||
|
||||
if (folderIds.Count == 0)
|
||||
if (folderIds.Length == 0)
|
||||
{
|
||||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
|
@ -1392,7 +1391,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
{
|
||||
MediaTypes = new[] { MediaType.Video },
|
||||
Recursive = true,
|
||||
AncestorIds = folderIds.ToArray(),
|
||||
AncestorIds = folderIds,
|
||||
IsFolder = false,
|
||||
IsVirtualItem = false,
|
||||
Limit = limit,
|
||||
|
@ -1528,7 +1527,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
}
|
||||
}
|
||||
|
||||
public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options)
|
||||
public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options)
|
||||
{
|
||||
var user = query.UserId.Equals(default)
|
||||
? null
|
||||
|
@ -1536,7 +1535,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
RemoveFields(options);
|
||||
|
||||
var internalResult = GetEmbyRecordings(query, options, user);
|
||||
var internalResult = await GetEmbyRecordingsAsync(query, options, user).ConfigureAwait(false);
|
||||
|
||||
var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user);
|
||||
|
||||
|
@ -2379,12 +2378,11 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
return _tvDtoService.GetInternalProgramId(externalId);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetRecordingFolders(User user)
|
||||
{
|
||||
return GetRecordingFolders(user, false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public Task<BaseItem[]> GetRecordingFoldersAsync(User user)
|
||||
=> GetRecordingFoldersAsync(user, false);
|
||||
|
||||
private List<BaseItem> GetRecordingFolders(User user, bool refreshChannels)
|
||||
private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels)
|
||||
{
|
||||
var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
|
||||
.SelectMany(i => i.Locations)
|
||||
|
@ -2396,14 +2394,16 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
.OrderBy(i => i.SortName)
|
||||
.ToList();
|
||||
|
||||
folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
|
||||
var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery
|
||||
{
|
||||
UserId = user.Id,
|
||||
IsRecordingsFolder = true,
|
||||
RefreshLatestChannelItems = refreshChannels
|
||||
}).Items);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return folders.Cast<BaseItem>().ToList();
|
||||
folders.AddRange(channels.Items);
|
||||
|
||||
return folders.Cast<BaseItem>().ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false);
|
||||
await client.ConnectAsync(remoteIp, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var stream = client.GetStream();
|
||||
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
|
||||
|
|
|
@ -13,8 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
public LegacyHdHomerunChannelCommands(string url)
|
||||
{
|
||||
// parse url for channel and program
|
||||
var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
|
||||
var match = regExp.Match(url);
|
||||
var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)");
|
||||
if (match.Success)
|
||||
{
|
||||
_channel = match.Groups[1].Value;
|
||||
|
|
|
@ -30,12 +30,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
|
||||
{
|
||||
private static readonly string[] _disallowedSharedStreamExtensions =
|
||||
private static readonly string[] _disallowedMimeTypes =
|
||||
{
|
||||
".mkv",
|
||||
".mp4",
|
||||
".m3u8",
|
||||
".mpd"
|
||||
"video/x-matroska",
|
||||
"video/mp4",
|
||||
"application/vnd.apple.mpegurl",
|
||||
"application/mpegurl",
|
||||
"application/x-mpegurl",
|
||||
"video/vnd.mpeg.dash.mpd"
|
||||
};
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
@ -118,9 +120,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
|
||||
{
|
||||
var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty;
|
||||
using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(message, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
|
||||
}
|
||||
|
|
|
@ -122,9 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
var attributes = ParseExtInf(extInf, out string remaining);
|
||||
extInf = remaining;
|
||||
|
||||
if (attributes.TryGetValue("tvg-logo", out string value))
|
||||
if (attributes.TryGetValue("tvg-logo", out string tvgLogo))
|
||||
{
|
||||
channel.ImageUrl = value;
|
||||
channel.ImageUrl = tvgLogo;
|
||||
}
|
||||
else if (attributes.TryGetValue("logo", out string logo))
|
||||
{
|
||||
channel.ImageUrl = logo;
|
||||
}
|
||||
|
||||
if (attributes.TryGetValue("group-title", out string groupTitle))
|
||||
|
@ -166,30 +170,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
|
||||
|
||||
string numberString = null;
|
||||
string attributeValue;
|
||||
|
||||
if (attributes.TryGetValue("tvg-chno", out attributeValue))
|
||||
if (attributes.TryGetValue("tvg-chno", out var attributeValue)
|
||||
&& double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
numberString = attributeValue;
|
||||
}
|
||||
numberString = attributeValue;
|
||||
}
|
||||
|
||||
if (!IsValidChannelNumber(numberString))
|
||||
{
|
||||
if (attributes.TryGetValue("tvg-id", out attributeValue))
|
||||
{
|
||||
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||
if (double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
numberString = attributeValue;
|
||||
}
|
||||
else if (attributes.TryGetValue("channel-id", out attributeValue))
|
||||
else if (attributes.TryGetValue("channel-id", out attributeValue)
|
||||
&& double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
numberString = attributeValue;
|
||||
}
|
||||
numberString = attributeValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,7 +206,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' });
|
||||
|
||||
if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||
if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
numberString = numberPart.ToString();
|
||||
}
|
||||
|
@ -255,19 +254,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
private static bool IsValidChannelNumber(string numberString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(numberString) ||
|
||||
string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.IsNullOrWhiteSpace(numberString)
|
||||
|| string.Equals(numberString, "-1", StringComparison.Ordinal)
|
||||
|| string.Equals(numberString, "0", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return double.TryParse(numberString, CultureInfo.InvariantCulture, out _);
|
||||
}
|
||||
|
||||
private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
|
||||
|
@ -285,7 +279,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
|
||||
|
||||
if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||
if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
// channel.Number = number.ToString();
|
||||
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
|
||||
|
@ -317,8 +311,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
|
||||
var matches = reg.Matches(line);
|
||||
var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
|
||||
|
||||
remaining = line;
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
_httpClientFactory = httpClientFactory;
|
||||
_appHost = appHost;
|
||||
OriginalStreamId = originalStreamId;
|
||||
EnableStreamSharing = true;
|
||||
}
|
||||
|
||||
public override async Task Open(CancellationToken openCancellationToken)
|
||||
|
@ -59,39 +58,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
|
||||
if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase)
|
||||
|| contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase)
|
||||
|| contentType.Contains("dash", StringComparison.OrdinalIgnoreCase)
|
||||
|| contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase)
|
||||
|| contentType.Contains("text/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Close the stream without any sharing features
|
||||
response.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
SetTempFilePath("ts");
|
||||
|
||||
var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
|
||||
|
||||
// OpenedMediaSource.Protocol = MediaProtocol.File;
|
||||
// OpenedMediaSource.Path = tempFile;
|
||||
// OpenedMediaSource.ReadAtNativeFramerate = true;
|
||||
|
||||
MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
||||
MediaSource.Protocol = MediaProtocol.Http;
|
||||
|
||||
// OpenedMediaSource.Path = TempFilePath;
|
||||
// OpenedMediaSource.Protocol = MediaProtocol.File;
|
||||
|
||||
// OpenedMediaSource.Path = _tempFilePath;
|
||||
// OpenedMediaSource.Protocol = MediaProtocol.File;
|
||||
// OpenedMediaSource.SupportsDirectPlay = false;
|
||||
// OpenedMediaSource.SupportsDirectStream = true;
|
||||
// OpenedMediaSource.SupportsTranscoding = true;
|
||||
var res = await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
if (!res)
|
||||
{
|
||||
|
@ -108,15 +81,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
try
|
||||
{
|
||||
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
|
||||
using var message = response;
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
await StreamHelper.CopyToAsync(
|
||||
stream,
|
||||
fileStream,
|
||||
IODefaults.CopyToBufferSize,
|
||||
() => Resolve(openTaskCompletionSource),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
using (response)
|
||||
{
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
await StreamHelper.CopyToAsync(
|
||||
stream,
|
||||
fileStream,
|
||||
IODefaults.CopyToBufferSize,
|
||||
() => Resolve(openTaskCompletionSource),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
{
|
||||
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
|
||||
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
|
||||
"Collections": "সংগ্রহ",
|
||||
"Collections": "সংগ্রহশালা",
|
||||
"ChapterNameValue": "অধ্যায় {0}",
|
||||
"Channels": "চ্যানেল",
|
||||
"Channels": "চ্যানেলসমূহ",
|
||||
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
|
||||
"Books": "বই",
|
||||
"Books": "পুস্তকসমূহ",
|
||||
"AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
|
||||
"Artists": "শিল্পীরা",
|
||||
"Artists": "শিল্পীগণ",
|
||||
"Application": "অ্যাপ্লিকেশন",
|
||||
"Albums": "অ্যালবামগুলো",
|
||||
"Albums": "অ্যালবামসমূহ",
|
||||
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
|
||||
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
|
||||
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
|
||||
"HeaderContinueWatching": "দেখতে থাকুন",
|
||||
"HeaderAlbumArtists": "এলবাম শিল্পীবৃন্দ",
|
||||
"Genres": "শৈলী",
|
||||
"Folders": "ফোল্ডারগুলো",
|
||||
"HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
|
||||
"Genres": "শৈলীধারাসমূহ",
|
||||
"Folders": "ফোল্ডারসমূহ",
|
||||
"Favorites": "পছন্দসমূহ",
|
||||
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
|
||||
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
|
||||
"VersionNumber": "সংস্করণ {0}",
|
||||
"ValueSpecialEpisodeName": "বিশেষ - {0}",
|
||||
"ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
|
||||
"UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
|
||||
"UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
|
||||
|
@ -36,10 +36,10 @@
|
|||
"User": "ব্যবহারকারী",
|
||||
"TvShows": "টিভি শোগুলো",
|
||||
"System": "সিস্টেম",
|
||||
"Sync": "সিংক",
|
||||
"Sync": "সমলয় স্থাপন",
|
||||
"SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
|
||||
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
|
||||
"Songs": "গানগুলো",
|
||||
"Songs": "সঙ্গীতসমূহ",
|
||||
"Shows": "টিভি পর্ব",
|
||||
"ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন",
|
||||
"ScheduledTaskStartedWithName": "{0} শুরু হয়েছে",
|
||||
|
@ -49,8 +49,8 @@
|
|||
"PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
|
||||
"PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
|
||||
"Plugin": "প্লাগিন",
|
||||
"Playlists": "প্লেলিস্ট",
|
||||
"Photos": "ছবিগুলো",
|
||||
"Playlists": "প্লে লিস্ট সমূহ",
|
||||
"Photos": "চিত্রসমূহ",
|
||||
"NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
|
||||
"NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
|
||||
"NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
|
||||
|
@ -71,9 +71,9 @@
|
|||
"NameSeasonUnknown": "সিজন অজানা",
|
||||
"NameSeasonNumber": "সিজন {0}",
|
||||
"NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
|
||||
"MusicVideos": "গানের ভিডিও",
|
||||
"MusicVideos": "সঙ্গীত ভিডিয়ো সমূহ",
|
||||
"Music": "গান",
|
||||
"Movies": "চলচ্চিত্র",
|
||||
"Movies": "চলচ্চিত্রসমূহ",
|
||||
"MixedContent": "মিশ্র কন্টেন্ট",
|
||||
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
|
||||
"HeaderRecordingGroups": "রেকর্ডিং দল",
|
||||
|
@ -117,5 +117,11 @@
|
|||
"Forced": "জোরকরে",
|
||||
"TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
|
||||
"TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
|
||||
"Default": "প্রাথমিক"
|
||||
"Default": "ডিফল্ট",
|
||||
"HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
|
||||
"TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
|
||||
"External": "বাহ্যিক",
|
||||
"TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
|
||||
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
|
||||
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।"
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"Artists": "Artistes",
|
||||
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
|
||||
"Books": "Llibres",
|
||||
"CameraImageUploadedFrom": "S'ha pujat una nova imatge des de la camera desde {0}",
|
||||
"CameraImageUploadedFrom": "S'ha pujat una nova imatge de càmera des de {0}",
|
||||
"Channels": "Canals",
|
||||
"ChapterNameValue": "Capítol {0}",
|
||||
"Collections": "Col·leccions",
|
||||
|
@ -16,65 +16,65 @@
|
|||
"Folders": "Carpetes",
|
||||
"Genres": "Gèneres",
|
||||
"HeaderAlbumArtists": "Artistes de l'àlbum",
|
||||
"HeaderContinueWatching": "Continua Veient",
|
||||
"HeaderFavoriteAlbums": "Àlbums Preferits",
|
||||
"HeaderFavoriteArtists": "Artistes Predilectes",
|
||||
"HeaderFavoriteEpisodes": "Episodis Predilectes",
|
||||
"HeaderFavoriteShows": "Sèries Predilectes",
|
||||
"HeaderFavoriteSongs": "Cançons Predilectes",
|
||||
"HeaderLiveTV": "TV en Directe",
|
||||
"HeaderContinueWatching": "Continuar veient",
|
||||
"HeaderFavoriteAlbums": "Àlbums preferits",
|
||||
"HeaderFavoriteArtists": "Artistes preferits",
|
||||
"HeaderFavoriteEpisodes": "Episodis preferits",
|
||||
"HeaderFavoriteShows": "Sèries preferides",
|
||||
"HeaderFavoriteSongs": "Cançons preferides",
|
||||
"HeaderLiveTV": "TV en directe",
|
||||
"HeaderNextUp": "A continuació",
|
||||
"HeaderRecordingGroups": "Grups d'Enregistrament",
|
||||
"HomeVideos": "Vídeos Domèstics",
|
||||
"HeaderRecordingGroups": "Grups d'enregistrament",
|
||||
"HomeVideos": "Vídeos domèstics",
|
||||
"Inherit": "Hereta",
|
||||
"ItemAddedWithName": "{0} ha estat afegit a la biblioteca",
|
||||
"ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca",
|
||||
"ItemAddedWithName": "{0} ha sigut afegit a la biblioteca",
|
||||
"ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca",
|
||||
"LabelIpAddressValue": "Adreça IP: {0}",
|
||||
"LabelRunningTimeValue": "Temps en funcionament: {0}",
|
||||
"Latest": "Darreres",
|
||||
"MessageApplicationUpdated": "El Servidor de Jellyfin ha estat actualitzat",
|
||||
"MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}",
|
||||
"Latest": "Darrers",
|
||||
"MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
|
||||
"MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
|
||||
"MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
|
||||
"MixedContent": "Contingut barrejat",
|
||||
"Movies": "Pel·lícules",
|
||||
"Music": "Música",
|
||||
"MusicVideos": "Vídeos Musicals",
|
||||
"MusicVideos": "Videoclips",
|
||||
"NameInstallFailed": "{0} instal·lació fallida",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameSeasonUnknown": "Temporada Desconeguda",
|
||||
"NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualització d'aplicació disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualització d'aplicació instal·lada",
|
||||
"NameSeasonUnknown": "Temporada desconeguda",
|
||||
"NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada",
|
||||
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
|
||||
"NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
|
||||
"NotificationOptionInstallationFailed": "Instal·lació fallida",
|
||||
"NotificationOptionNewLibraryContent": "Nou contingut afegit",
|
||||
"NotificationOptionPluginError": "Un connector ha fallat",
|
||||
"NotificationOptionPluginInstalled": "Connector instal·lat",
|
||||
"NotificationOptionPluginUninstalled": "Connector desinstal·lat",
|
||||
"NotificationOptionPluginUpdateInstalled": "Actualització de connector instal·lada",
|
||||
"NotificationOptionPluginError": "Un complement ha fallat",
|
||||
"NotificationOptionPluginInstalled": "Complement instal·lat",
|
||||
"NotificationOptionPluginUninstalled": "Complement desinstal·lat",
|
||||
"NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada",
|
||||
"NotificationOptionServerRestartRequired": "Reinici del servidor requerit",
|
||||
"NotificationOptionTaskFailed": "Tasca programada fallida",
|
||||
"NotificationOptionUserLockedOut": "Usuari tancat",
|
||||
"NotificationOptionVideoPlayback": "Reproducció de video iniciada",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reproducció de video aturada",
|
||||
"NotificationOptionUserLockedOut": "Usuari expulsat",
|
||||
"NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reproducció de vídeo aturada",
|
||||
"Photos": "Fotos",
|
||||
"Playlists": "Llistes de reproducció",
|
||||
"Plugin": "Connector",
|
||||
"Plugin": "Complement",
|
||||
"PluginInstalledWithName": "{0} ha estat instal·lat",
|
||||
"PluginUninstalledWithName": "{0} ha estat desinstal·lat",
|
||||
"PluginUpdatedWithName": "{0} ha estat actualitzat",
|
||||
"ProviderValue": "Proveïdor: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} ha fallat",
|
||||
"ScheduledTaskStartedWithName": "{0} iniciat",
|
||||
"ScheduledTaskStartedWithName": "{0} s'ha iniciat",
|
||||
"ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
|
||||
"Shows": "Sèries",
|
||||
"Songs": "Cançons",
|
||||
"StartupEmbyServerIsLoading": "El Servidor de Jellyfin està carregant. Si et plau, prova de nou ben aviat.",
|
||||
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho altre cop aviat.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
|
||||
"Sync": "Sincronitzar",
|
||||
"System": "Sistema",
|
||||
"TvShows": "Sèries de TV",
|
||||
|
@ -82,11 +82,11 @@
|
|||
"UserCreatedWithName": "S'ha creat l'usuari {0}",
|
||||
"UserDeletedWithName": "L'usuari {0} ha estat eliminat",
|
||||
"UserDownloadingItemWithValues": "{0} està descarregant {1}",
|
||||
"UserLockedOutWithName": "L'usuari {0} ha sigut tancat",
|
||||
"UserLockedOutWithName": "L'usuari {0} ha sigut expulsat",
|
||||
"UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
|
||||
"UserOnlineFromDevice": "{0} està connectat des de {1}",
|
||||
"UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}",
|
||||
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per {0}",
|
||||
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
|
||||
|
@ -94,14 +94,14 @@
|
|||
"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",
|
||||
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'Internet.",
|
||||
"TaskRefreshChannels": "Actualitza Canals",
|
||||
"TaskCleanTranscodeDescription": "Elimina els arxius temporals de transcodificacions que tinguin més d'un dia.",
|
||||
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
|
||||
"TaskRefreshChannels": "Actualitza els canals",
|
||||
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
|
||||
"TaskCleanTranscode": "Neteja les transcodificacions",
|
||||
"TaskUpdatePluginsDescription": "Actualitza les extensions que estan configurades per actualitzar-se automàticament.",
|
||||
"TaskUpdatePlugins": "Actualitza les extensions",
|
||||
"TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.",
|
||||
"TaskUpdatePlugins": "Actualitza els connectors",
|
||||
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.",
|
||||
"TaskRefreshPeople": "Actualitza Persones",
|
||||
"TaskRefreshPeople": "Actualitza les persones",
|
||||
"TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
|
||||
"TaskCleanLogs": "Neteja els registres",
|
||||
"TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
|
||||
|
@ -110,12 +110,12 @@
|
|||
"TaskRefreshChapterImages": "Extreure les imatges dels capítols",
|
||||
"TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
|
||||
"TaskCleanCache": "Elimina arxius temporals",
|
||||
"TasksChannelsCategory": "Canals d'Internet",
|
||||
"TasksChannelsCategory": "Canals d'internet",
|
||||
"TasksApplicationCategory": "Aplicació",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksMaintenanceCategory": "Manteniment",
|
||||
"TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
|
||||
"TaskCleanActivityLog": "Buidar Registre d'Activitat",
|
||||
"TaskCleanActivityLog": "Buidar el registre d'activitat",
|
||||
"Undefined": "Indefinit",
|
||||
"Forced": "Forçat",
|
||||
"Default": "Per defecte",
|
||||
|
@ -124,5 +124,5 @@
|
|||
"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",
|
||||
"External": "Extern",
|
||||
"HearingImpaired": "Discapacitat Auditiva"
|
||||
"HearingImpaired": "Discapacitat auditiva"
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"NameSeasonNumber": "Tymor {0}",
|
||||
"MusicVideos": "Fideos Cerddoriaeth",
|
||||
"MixedContent": "Cynnwys amrywiol",
|
||||
"HomeVideos": "Fideos Cartref",
|
||||
"HomeVideos": "Genres",
|
||||
"HeaderNextUp": "Nesaf i Fyny",
|
||||
"HeaderFavoriteArtists": "Ffefryn Artistiaid",
|
||||
"HeaderFavoriteAlbums": "Ffefryn Albwmau",
|
||||
|
@ -122,5 +122,6 @@
|
|||
"TaskRefreshChapterImagesDescription": "Creu mân-luniau ar gyfer fideos sydd â phenodau.",
|
||||
"TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
|
||||
"TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
|
||||
"TaskCleanCache": "Gwaghau Ffolder Cache"
|
||||
"TaskCleanCache": "Gwaghau Ffolder Cache",
|
||||
"HearingImpaired": "Nam ar y clyw"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Albums": "Albummer",
|
||||
"Albums": "Album",
|
||||
"AppDeviceValues": "App: {0}, Enhed: {1}",
|
||||
"Application": "Applikation",
|
||||
"Artists": "Kunstnere",
|
||||
"AuthenticationSucceededWithUserName": "{0} succesfuldt autentificeret",
|
||||
"AuthenticationSucceededWithUserName": "{0} er logget ind",
|
||||
"Books": "Bøger",
|
||||
"CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}",
|
||||
"Channels": "Kanaler",
|
||||
|
@ -11,17 +11,17 @@
|
|||
"Collections": "Samlinger",
|
||||
"DeviceOfflineWithName": "{0} har afbrudt forbindelsen",
|
||||
"DeviceOnlineWithName": "{0} er forbundet",
|
||||
"FailedLoginAttemptWithUserName": "Fejlet loginforsøg fra {0}",
|
||||
"FailedLoginAttemptWithUserName": "Mislykket loginforsøg fra {0}",
|
||||
"Favorites": "Favoritter",
|
||||
"Folders": "Mapper",
|
||||
"Genres": "Genrer",
|
||||
"HeaderAlbumArtists": "Albumkunstner",
|
||||
"HeaderAlbumArtists": "Albums kunstnere",
|
||||
"HeaderContinueWatching": "Fortsæt afspilning",
|
||||
"HeaderFavoriteAlbums": "Favoritalbummer",
|
||||
"HeaderFavoriteArtists": "Favoritkunstnere",
|
||||
"HeaderFavoriteEpisodes": "Favoritepisoder",
|
||||
"HeaderFavoriteShows": "Favoritserier",
|
||||
"HeaderFavoriteSongs": "Favoritsange",
|
||||
"HeaderFavoriteAlbums": "Favorit albummer",
|
||||
"HeaderFavoriteArtists": "Favorit kunstnere",
|
||||
"HeaderFavoriteEpisodes": "Favorit afsnit",
|
||||
"HeaderFavoriteShows": "Favorit serier",
|
||||
"HeaderFavoriteSongs": "Favorit sange",
|
||||
"HeaderLiveTV": "Live-TV",
|
||||
"HeaderNextUp": "Næste",
|
||||
"HeaderRecordingGroups": "Optagelsesgrupper",
|
||||
|
@ -39,90 +39,90 @@
|
|||
"MixedContent": "Blandet indhold",
|
||||
"Movies": "Film",
|
||||
"Music": "Musik",
|
||||
"MusicVideos": "Musik videoer",
|
||||
"MusicVideos": "Musikvideoer",
|
||||
"NameInstallFailed": "{0} installationen mislykkedes",
|
||||
"NameSeasonNumber": "Sæson {0}",
|
||||
"NameSeasonUnknown": "Ukendt sæson",
|
||||
"NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig til download.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Opdatering til applikation tilgængelig",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Opdatering til applikation installeret",
|
||||
"NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Opdatering til applikationen er tilgængelig",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Opdatering til applikationen blev installeret",
|
||||
"NotificationOptionAudioPlayback": "Lydafspilning påbegyndt",
|
||||
"NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet",
|
||||
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
|
||||
"NotificationOptionInstallationFailed": "Installationen fejlede",
|
||||
"NotificationOptionInstallationFailed": "Installationen mislykkedes",
|
||||
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
|
||||
"NotificationOptionPluginError": "Pluginfejl",
|
||||
"NotificationOptionPluginInstalled": "Plugin installeret",
|
||||
"NotificationOptionPluginUninstalled": "Plugin afinstalleret",
|
||||
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin installeret",
|
||||
"NotificationOptionServerRestartRequired": "Genstart af server påkrævet",
|
||||
"NotificationOptionTaskFailed": "Planlagt opgave fejlet",
|
||||
"NotificationOptionUserLockedOut": "Bruger låst ude",
|
||||
"NotificationOptionPluginError": "Plugin fejl",
|
||||
"NotificationOptionPluginInstalled": "Plugin blev installeret",
|
||||
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
|
||||
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
|
||||
"NotificationOptionServerRestartRequired": "Genstart af serveren er påkrævet",
|
||||
"NotificationOptionTaskFailed": "Planlagt opgave er fejlet",
|
||||
"NotificationOptionUserLockedOut": "Bruger er låst ude",
|
||||
"NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
|
||||
"NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet",
|
||||
"Photos": "Fotoer",
|
||||
"NotificationOptionVideoPlaybackStopped": "Videoafspilning blev stoppet",
|
||||
"Photos": "Fotos",
|
||||
"Playlists": "Afspilningslister",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} blev installeret",
|
||||
"PluginUninstalledWithName": "{0} blev afinstalleret",
|
||||
"PluginUpdatedWithName": "{0} blev opdateret",
|
||||
"ProviderValue": "Udbyder: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} fejlet",
|
||||
"ScheduledTaskStartedWithName": "{0} påbegyndt",
|
||||
"ScheduledTaskFailedWithName": "{0} mislykkedes",
|
||||
"ScheduledTaskStartedWithName": "{0} påbegyndte",
|
||||
"ServerNameNeedsToBeRestarted": "{0} skal genstartes",
|
||||
"Shows": "Serier",
|
||||
"Songs": "Sange",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte op. Prøv venligst igen om lidt.",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte. Forsøg igen om et øjeblik.",
|
||||
"SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke downloades fra {0} til {1}",
|
||||
"Sync": "Synk",
|
||||
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
|
||||
"Sync": "Synkroniser",
|
||||
"System": "System",
|
||||
"TvShows": "Tv-serier",
|
||||
"TvShows": "TV-serier",
|
||||
"User": "Bruger",
|
||||
"UserCreatedWithName": "Bruger {0} er blevet oprettet",
|
||||
"UserDeletedWithName": "Brugeren {0} er blevet slettet",
|
||||
"UserDownloadingItemWithValues": "{0} downloader {1}",
|
||||
"UserDeletedWithName": "Brugeren {0} er nu slettet",
|
||||
"UserDownloadingItemWithValues": "{0} henter {1}",
|
||||
"UserLockedOutWithName": "Brugeren {0} er blevet låst ude",
|
||||
"UserOfflineFromDevice": "{0} har afbrudt fra {1}",
|
||||
"UserOnlineFromDevice": "{0} er online fra {1}",
|
||||
"UserPasswordChangedWithName": "Adgangskode er ændret for bruger {0}",
|
||||
"UserPolicyUpdatedWithName": "Brugerpolitik er blevet opdateret for {0}",
|
||||
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
|
||||
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
||||
"ValueSpecialEpisodeName": "Special - {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfiguration.",
|
||||
"TaskDownloadMissingSubtitles": "Download manglende undertekster",
|
||||
"TaskUpdatePluginsDescription": "Downloader og installere opdateringer for plugins som er konfigureret til at opdatere automatisk.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
|
||||
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
|
||||
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
|
||||
"TaskUpdatePlugins": "Opdater Plugins",
|
||||
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gammle.",
|
||||
"TaskCleanLogs": "Ryd Log Mappe",
|
||||
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdaterer metadata.",
|
||||
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
|
||||
"TaskCleanLogs": "Ryd Log mappe",
|
||||
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
|
||||
"TaskRefreshLibrary": "Scan Medie Bibliotek",
|
||||
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke har brug for længere.",
|
||||
"TaskCleanCache": "Ryd Cache Mappe",
|
||||
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
|
||||
"TaskCleanCache": "Ryd Cache mappe",
|
||||
"TasksChannelsCategory": "Internet Kanaler",
|
||||
"TasksApplicationCategory": "Applikation",
|
||||
"TasksLibraryCategory": "Bibliotek",
|
||||
"TasksMaintenanceCategory": "Vedligeholdelse",
|
||||
"TaskRefreshChapterImages": "Udtræk Kapitel billeder",
|
||||
"TaskRefreshChapterImages": "Udtræk kapitel billeder",
|
||||
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
|
||||
"TaskRefreshChannelsDescription": "Genopfrisker internet kanal information.",
|
||||
"TaskRefreshChannels": "Genopfrisk Kanaler",
|
||||
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.",
|
||||
"TaskCleanTranscode": "Rengør Transcode Mappen",
|
||||
"TaskRefreshPeople": "Genopfrisk Personer",
|
||||
"TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek.",
|
||||
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigureret alder.",
|
||||
"TaskRefreshChannelsDescription": "Opdater internet kanal information.",
|
||||
"TaskRefreshChannels": "Opdater Kanaler",
|
||||
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
|
||||
"TaskCleanTranscode": "Tøm Transcode mappen",
|
||||
"TaskRefreshPeople": "Opdater Personer",
|
||||
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
|
||||
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
|
||||
"TaskCleanActivityLog": "Ryd Aktivitetslog",
|
||||
"Undefined": "Udefineret",
|
||||
"Forced": "Tvunget",
|
||||
"Default": "Standard",
|
||||
"TaskOptimizeDatabaseDescription": "Kompakter database og forkorter fri plads. Ved at køre denne proces efter at scanne biblioteket eller efter at ændre noget som kunne have indflydelse på databasen, kan forbedre ydeevne.",
|
||||
"TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
|
||||
"TaskOptimizeDatabase": "Optimér database",
|
||||
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan godt tage lang tid.",
|
||||
"TaskKeyframeExtractor": "Billedramme udtrækker",
|
||||
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
|
||||
"TaskKeyframeExtractor": "Nøglebillede udtræk",
|
||||
"External": "Ekstern",
|
||||
"HearingImpaired": "Hørehæmmet"
|
||||
}
|
||||
|
|
|
@ -118,11 +118,11 @@
|
|||
"TaskCleanActivityLog": "Borrar log de actividades",
|
||||
"Undefined": "Indefinido",
|
||||
"Forced": "Forzado",
|
||||
"Default": "Por Defecto",
|
||||
"Default": "Predeterminado",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.",
|
||||
"TaskOptimizeDatabase": "Optimización de base de datos",
|
||||
"External": "Externo",
|
||||
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
||||
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
|
||||
"HearingImpaired": "Personas con discapacidad auditiva"
|
||||
"HearingImpaired": "Discapacidad Auditiva"
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
|
||||
"LabelIpAddressValue": "Dirección IP: {0}",
|
||||
"LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
|
||||
"Latest": "Últimos",
|
||||
"Latest": "Últimas",
|
||||
"MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
|
||||
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada",
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
"TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.",
|
||||
"TaskCleanActivityLog": "Tyhjennä toimintahistoria",
|
||||
"Undefined": "Määrittelemätön",
|
||||
"TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastojen skannauksen tai muiden tietokantaan liittyvien muutoksien jälkeen voi parantaa suorituskykyä.",
|
||||
"TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastopäivityksen tai muiden mahdollisten tietokantamuutosten jälkeen voi parantaa suorituskykyä.",
|
||||
"TaskOptimizeDatabase": "Optimoi tietokanta",
|
||||
"TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
|
||||
"TaskKeyframeExtractor": "Avainkuvien purkain",
|
||||
|
|
|
@ -119,5 +119,9 @@
|
|||
"Undefined": "Hindi tiyak",
|
||||
"Forced": "Sapilitan",
|
||||
"TaskOptimizeDatabaseDescription": "Iko-compact ang database at ita-truncate ang free space. Ang pagpapatakbo ng gawaing ito pagkatapos ng pag-scan sa library o paggawa ng iba pang mga pagbabago na nagpapahiwatig ng mga pagbabago sa database ay maaaring magpa-improve ng performance.",
|
||||
"TaskOptimizeDatabase": "I-optimize ang database"
|
||||
"TaskOptimizeDatabase": "I-optimize ang database",
|
||||
"HearingImpaired": "Bingi",
|
||||
"TaskKeyframeExtractor": "Tagabunot ng Keyframe",
|
||||
"TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.",
|
||||
"External": "External"
|
||||
}
|
||||
|
|
|
@ -67,5 +67,61 @@
|
|||
"Plugin": "प्लग-इन",
|
||||
"Playlists": "प्लेलिस्ट",
|
||||
"Photos": "तस्वीरें",
|
||||
"External": "बाहरी"
|
||||
"External": "बाहरी",
|
||||
"PluginUpdatedWithName": "{0} अपडेट हुए",
|
||||
"ScheduledTaskStartedWithName": "{0} शुरू हुए",
|
||||
"Songs": "गाने",
|
||||
"UserStartedPlayingItemWithValues": "{0} {2} पर {1} खेल रहे हैं",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ने {2} पर {1} खेलना खत्म किया",
|
||||
"StartupEmbyServerIsLoading": "जेलीफ़िन सर्वर लोड हो रहा है। कृपया शीघ्र ही पुन: प्रयास करें।",
|
||||
"ServerNameNeedsToBeRestarted": "{0} रीस्टार्ट करने की आवश्यकता है",
|
||||
"UserCreatedWithName": "उपयोगकर्ता {0} बनाया गया",
|
||||
"UserDownloadingItemWithValues": "{0} डाउनलोड हो रहा है",
|
||||
"UserOfflineFromDevice": "{0} {1} से डिस्कनेक्ट हो गया है",
|
||||
"Undefined": "अनिर्धारित",
|
||||
"UserOnlineFromDevice": "{0} {1} से ऑनलाइन है",
|
||||
"Shows": "शो",
|
||||
"UserPasswordChangedWithName": "उपयोगकर्ता {0} के लिए पासवर्ड बदल दिया गया है",
|
||||
"UserDeletedWithName": "उपयोगकर्ता {0} हटा दिया गया",
|
||||
"UserPolicyUpdatedWithName": "{0} के लिए उपयोगकर्ता नीति अपडेट कर दी गई है",
|
||||
"User": "उपयोगकर्ता",
|
||||
"SubtitleDownloadFailureFromForItem": "{1} के लिए {0} से उपशीर्षक डाउनलोड करने में विफल",
|
||||
"ProviderValue": "प्रदाता: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0}असफल",
|
||||
"UserLockedOutWithName": "उपयोगकर्ता {0} को लॉक आउट कर दिया गया है",
|
||||
"System": "प्रणाली",
|
||||
"TvShows": "टीवी शो",
|
||||
"HearingImpaired": "मूक बधिर",
|
||||
"ValueSpecialEpisodeName": "विशेष - {0}",
|
||||
"TasksMaintenanceCategory": "रखरखाव",
|
||||
"Sync": "समाकलयति",
|
||||
"VersionNumber": "{0} पाठान्तर",
|
||||
"ValueHasBeenAddedToLibrary": "{0} आपके माध्यम ग्रन्थालय में उपजात हो गया हैं",
|
||||
"TasksLibraryCategory": "संग्रहालय",
|
||||
"TaskOptimizeDatabase": "जानकारी प्रवृद्धि",
|
||||
"TaskDownloadMissingSubtitles": "असमेत अनुलेख को अवाहरति करें",
|
||||
"TaskRefreshLibrary": "माध्यम संग्राहत को छाने",
|
||||
"TaskCleanActivityLog": "क्रियाकलाप लॉग साफ करें",
|
||||
"TasksChannelsCategory": "इंटरनेट प्रणाली",
|
||||
"TasksApplicationCategory": "अनुप्रयोग",
|
||||
"TaskRefreshPeople": "लोगोकी जानकारी ताज़ी करें",
|
||||
"TaskKeyframeExtractor": "कीफ़्रेम एक्सट्रैक्टर",
|
||||
"TaskCleanActivityLogDescription": "कॉन्फ़िगर की गई आयु से पुरानी गतिविधि लॉग प्रविष्टियां हटाता है।",
|
||||
"TaskRefreshChapterImagesDescription": "अध्याय वाले वीडियो के लिए थंबनेल बनाता है।",
|
||||
"TaskRefreshLibraryDescription": "नई फ़ाइलों के लिए आपकी मीडिया लाइब्रेरी को स्कैन करता है और मेटाडेटा को ताज़ा करता है।",
|
||||
"TaskCleanLogs": "स्वच्छ लॉग निर्देशिका",
|
||||
"TaskUpdatePluginsDescription": "प्लगइन्स के लिए अपडेट डाउनलोड और इंस्टॉल करें जो स्वचालित रूप से अपडेट करने के लिए कॉन्फ़िगर किए गए हैं।",
|
||||
"TaskCleanTranscode": "स्वच्छ ट्रांसकोड निर्देशिका",
|
||||
"TaskCleanTranscodeDescription": "एक दिन से अधिक पुरानी ट्रांसकोड फ़ाइलें हटाता है.",
|
||||
"TaskRefreshChannelsDescription": "इंटरनेट चैनल की जानकारी को ताज़ा करता है।",
|
||||
"TaskOptimizeDatabaseDescription": "डेटाबेस को कॉम्पैक्ट करता है और मुक्त स्थान को छोटा करता है। लाइब्रेरी को स्कैन करने के बाद इस कार्य को चलाने या अन्य परिवर्तन करने से जो डेटाबेस संशोधनों को लागू करते हैं, प्रदर्शन में सुधार कर सकते हैं।",
|
||||
"TaskRefreshChannels": "इंटरनेट चैनल की जानकारी को ताज़ा करता है",
|
||||
"TaskRefreshChapterImages": "अध्याय छवियाँ निकालें",
|
||||
"TaskCleanLogsDescription": "{0} दिन से अधिक पुरानी लॉग फ़ाइलें हटाता है।",
|
||||
"TaskCleanCacheDescription": "उन कैश फ़ाइलों को हटाता है जिनकी अब सिस्टम को आवश्यकता नहीं है।",
|
||||
"TaskUpdatePlugins": "अद्यतन प्लगइन्स",
|
||||
"TaskRefreshPeopleDescription": "आपकी मीडिया लाइब्रेरी में अभिनेताओं और निर्देशकों के लिए मेटाडेटा अपडेट करता है।",
|
||||
"TaskCleanCache": "स्वच्छ कैश निर्देशिका",
|
||||
"TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कॉन्फ़िगरेशन के आधार पर लापता उपशीर्षक के लिए इंटरनेट खोजता है।",
|
||||
"TaskKeyframeExtractorDescription": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।"
|
||||
}
|
||||
|
|
|
@ -107,5 +107,14 @@
|
|||
"TasksApplicationCategory": "Forrit",
|
||||
"TasksLibraryCategory": "Miðlasafn",
|
||||
"TasksMaintenanceCategory": "Viðhald",
|
||||
"Default": "Sjálfgefið"
|
||||
"Default": "Sjálfgefið",
|
||||
"TaskCleanActivityLog": "Hreinsa athafnaskrá",
|
||||
"TaskRefreshPeople": "Endurnýja fólk",
|
||||
"TaskDownloadMissingSubtitles": "Sækja texta sem vantar",
|
||||
"TaskOptimizeDatabase": "Fínstilla gagnagrunn",
|
||||
"Undefined": "Óskilgreint",
|
||||
"TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
|
||||
"TaskCleanLogs": "Hreinsa færslu skrá",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
|
||||
"HearingImpaired": "Heyrnarskertur"
|
||||
}
|
||||
|
|
|
@ -37,8 +37,8 @@
|
|||
"MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました",
|
||||
"MessageServerConfigurationUpdated": "サーバー設定が更新されました",
|
||||
"MixedContent": "ミックスコンテンツ",
|
||||
"Movies": "ムービー",
|
||||
"Music": "ミュージック",
|
||||
"Movies": "映画",
|
||||
"Music": "音楽",
|
||||
"MusicVideos": "ミュージックビデオ",
|
||||
"NameInstallFailed": "{0}のインストールに失敗しました",
|
||||
"NameSeasonNumber": "シーズン {0}",
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
"HeaderFavoriteAlbums": "Mėgstami Albumai",
|
||||
"HeaderFavoriteArtists": "Mėgstami Atlikėjai",
|
||||
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
|
||||
"HeaderFavoriteShows": "Mėgstamiausi serialai",
|
||||
"HeaderFavoriteSongs": "Mėgstamos dainos",
|
||||
"HeaderLiveTV": "TV gyvai",
|
||||
"HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
|
||||
"HeaderFavoriteSongs": "Mėgstamos Dainos",
|
||||
"HeaderLiveTV": "Tiesioginė TV",
|
||||
"HeaderNextUp": "Toliau eilėje",
|
||||
"HeaderRecordingGroups": "Įrašų grupės",
|
||||
"HomeVideos": "Namų vaizdo įrašai",
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
"CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
|
||||
"Books": "Grāmatas",
|
||||
"Artists": "Izpildītāji",
|
||||
"Albums": "Albūmi",
|
||||
"Albums": "Albumi",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"HeaderFavoriteSongs": "Dziesmu Favorīti",
|
||||
"HeaderFavoriteShows": "Raidījumu Favorīti",
|
||||
|
@ -120,5 +120,8 @@
|
|||
"Default": "Noklusējuma",
|
||||
"TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
|
||||
"TaskOptimizeDatabase": "Optimizēt datubāzi",
|
||||
"External": "Ārējais"
|
||||
"External": "Ārējais",
|
||||
"HearingImpaired": "Ar dzirdes traucējumiem",
|
||||
"TaskKeyframeExtractor": "Atslēgkadru Ekstraktors",
|
||||
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"Albums": "辑册",
|
||||
"Artists": "艺人",
|
||||
"AuthenticationSucceededWithUserName": "{0} 授之权矣",
|
||||
"Books": "册"
|
||||
}
|
|
@ -119,5 +119,7 @@
|
|||
"Genres": "വിഭാഗങ്ങൾ",
|
||||
"Channels": "ചാനലുകൾ",
|
||||
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
|
||||
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക"
|
||||
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
|
||||
"HearingImpaired": "കേൾവി തകരാറുകൾ",
|
||||
"External": "പുറമേയുള്ള"
|
||||
}
|
||||
|
|
|
@ -122,5 +122,6 @@
|
|||
"External": "बाहेरचा",
|
||||
"DeviceOnlineWithName": "{0} कनेक्ट झाले",
|
||||
"DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे",
|
||||
"AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत"
|
||||
"AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत",
|
||||
"HearingImpaired": "कर्णबधीर"
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"MixedContent": "Kandungan campuran",
|
||||
"Movies": "Filem-filem",
|
||||
"Music": "Muzik",
|
||||
"MusicVideos": "Video muzik",
|
||||
"MusicVideos": "Video Muzik",
|
||||
"NameInstallFailed": "{0} pemasangan gagal",
|
||||
"NameSeasonNumber": "Musim {0}",
|
||||
"NameSeasonUnknown": "Musim Tidak Diketahui",
|
||||
|
@ -55,7 +55,7 @@
|
|||
"NotificationOptionPluginInstalled": "Plugin telah dipasang",
|
||||
"NotificationOptionPluginUninstalled": "Plugin telah dinyahpasang",
|
||||
"NotificationOptionPluginUpdateInstalled": "Kemaskini plugin telah dipasang",
|
||||
"NotificationOptionServerRestartRequired": "",
|
||||
"NotificationOptionServerRestartRequired": "Perlu mulakan semula server",
|
||||
"NotificationOptionTaskFailed": "Kegagalan tugas berjadual",
|
||||
"NotificationOptionUserLockedOut": "Pengguna telah dikunci",
|
||||
"NotificationOptionVideoPlayback": "Ulangmain video bermula",
|
||||
|
@ -109,5 +109,20 @@
|
|||
"TaskRefreshLibrary": "Imbas Perpustakaan Media",
|
||||
"TaskRefreshChapterImagesDescription": "Membuat gambaran kecil untuk video yang mempunyai bab.",
|
||||
"TaskRefreshChapterImages": "Ekstrak Gambar-gambar Bab",
|
||||
"TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem."
|
||||
"TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem.",
|
||||
"HearingImpaired": "Lemah Pendengaran",
|
||||
"TaskRefreshPeopleDescription": "Kemas kini metadata untuk pelakon dan pengarah di dalam perpustakaan media.",
|
||||
"TaskUpdatePluginsDescription": "Muat turun dan kemas kini plugin yang dikonfigurasi secara automatik.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Cari sari kata yang hilang di internet, berdasarkan konfigurasi metadata.",
|
||||
"TaskOptimizeDatabaseDescription": "Mampatkan pangkalan data dan potong ruang kosong. Pelaksanaan tugas ini selepas pengimbasan perpustakaan boleh membantu membaiki prestasi.",
|
||||
"TaskRefreshChannels": "Segarkan Saluran-saluran",
|
||||
"TaskUpdatePlugins": "Kemas kini plugin",
|
||||
"TaskDownloadMissingSubtitles": "Muat turn sari kata yang tiada",
|
||||
"TaskCleanTranscodeDescription": "Padam fail transkod yang lebih lama dari satu hari.",
|
||||
"TaskRefreshChannelsDescription": "Segarkan maklumat saluran internet.",
|
||||
"TaskCleanTranscode": "Bersihkan direktori transkod",
|
||||
"External": "Luaran",
|
||||
"TaskOptimizeDatabase": "Optimumkan pangkalan data",
|
||||
"TaskKeyframeExtractor": "Ekstrak bingkai kunci",
|
||||
"TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang."
|
||||
}
|
||||
|
|
|
@ -109,5 +109,19 @@
|
|||
"Sync": "समकालीन",
|
||||
"SubtitleDownloadFailureFromForItem": "उपशीर्षकहरू {0} बाट {1} को लागि डाउनलोड गर्न असफल",
|
||||
"PluginUpdatedWithName": "{0} अद्यावधिक गरिएको थियो",
|
||||
"PluginUninstalledWithName": "{0} को स्थापना रद्द गरिएको थियो"
|
||||
"PluginUninstalledWithName": "{0} को स्थापना रद्द गरिएको थियो",
|
||||
"HearingImpaired": "सुन्न नसक्ने",
|
||||
"TaskUpdatePluginsDescription": "स्वचालित रूपमा अद्यावधिक गर्न कन्फिगर गरिएका प्लगइनहरूका लागि अद्यावधिकहरू डाउनलोड र स्थापना गर्दछ।",
|
||||
"TaskCleanTranscode": "सफा ट्रान्सकोड निर्देशिका",
|
||||
"TaskCleanTranscodeDescription": "एक दिन भन्दा पुराना ट्रान्सकोड फाइलहरू मेटाउँछ।",
|
||||
"TaskRefreshChannels": "च्यानलहरू ताजा गर्नुहोस्",
|
||||
"TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कन्फिगरेसनमा आधारित हराइरहेको उपशीर्षकहरूको लागि इन्टरनेट खोज्छ।",
|
||||
"TaskOptimizeDatabase": "डेटाबेस अप्टिमाइज गर्नुहोस्",
|
||||
"TaskOptimizeDatabaseDescription": "डाटाबेस कम्प्याक्ट र खाली ठाउँ काट्छ। पुस्तकालय स्क्यान गरेपछि वा डाटाबेस परिमार्जनलाई संकेत गर्ने अन्य परिवर्तनहरू गरेपछि यो कार्य चलाउँदा कार्यसम्पादनमा सुधार हुन सक्छ।",
|
||||
"TaskKeyframeExtractorDescription": "थप सटीक एचएलएस प्लेलिस्टहरू सिर्जना गर्न भिडियो फाइलहरूबाट कीफ्रेमहरू निकाल्छ। यो कार्य लामो समय सम्म चल्न सक्छ।",
|
||||
"TaskUpdatePlugins": "प्लगइनहरू अपडेट गर्नुहोस्",
|
||||
"TaskRefreshPeopleDescription": "तपाईंको मिडिया लाइब्रेरीमा अभिनेता र निर्देशकहरूको लागि मेटाडेटा अपडेट गर्दछ।",
|
||||
"TaskRefreshChannelsDescription": "इन्टरनेट च्यानल जानकारी ताजा गर्दछ।",
|
||||
"TaskDownloadMissingSubtitles": "छुटेका उपशीर्षकहरू डाउनलोड गर्नुहोस्",
|
||||
"TaskKeyframeExtractor": "कीफ्रेम एक्स्ट्रक्टर"
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
|
||||
"Channels": "Kanalen",
|
||||
"ChapterNameValue": "Hoofdstuk {0}",
|
||||
"Collections": "Verzamelingen",
|
||||
"Collections": "Collecties",
|
||||
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
|
||||
"DeviceOnlineWithName": "{0} is verbonden",
|
||||
"FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}",
|
||||
|
@ -95,13 +95,13 @@
|
|||
"TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.",
|
||||
"TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden",
|
||||
"TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.",
|
||||
"TaskRefreshChannels": "Vernieuw kanalen",
|
||||
"TaskRefreshChannels": "Kanalen vernieuwen",
|
||||
"TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.",
|
||||
"TaskCleanLogs": "Logboekmap opschonen",
|
||||
"TaskCleanTranscode": "Transcoderingsmap opschonen",
|
||||
"TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.",
|
||||
"TaskUpdatePlugins": "Plug-ins bijwerken",
|
||||
"TaskRefreshPeopleDescription": "Update metadata voor acteurs en regisseurs in de media bibliotheek.",
|
||||
"TaskRefreshPeopleDescription": "Updatet metadata voor acteurs en regisseurs in je mediabibliotheek.",
|
||||
"TaskRefreshPeople": "Personen vernieuwen",
|
||||
"TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.",
|
||||
"TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.",
|
||||
|
@ -114,7 +114,7 @@
|
|||
"TasksApplicationCategory": "Toepassing",
|
||||
"TasksLibraryCategory": "Bibliotheek",
|
||||
"TasksMaintenanceCategory": "Onderhoud",
|
||||
"TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde leeftijd.",
|
||||
"TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.",
|
||||
"TaskCleanActivityLog": "Activiteitenlogboek legen",
|
||||
"Undefined": "Niet gedefinieerd",
|
||||
"Forced": "Geforceerd",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"External": "ବହିଃସ୍ଥ",
|
||||
"Genres": "ଧରଣ"
|
||||
}
|
|
@ -28,22 +28,22 @@
|
|||
"ValueHasBeenAddedToLibrary": "{0} ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ਨੇ {2} 'ਤੇ {1} ਖੇਡਣਾ ਪੂਰਾ ਕਰ ਲਿਆ ਹੈ",
|
||||
"UserStartedPlayingItemWithValues": "{0} {2} 'ਤੇ {1} ਖੇਡ ਰਿਹਾ ਹੈ",
|
||||
"UserPolicyUpdatedWithName": "ਉਪਭੋਗਤਾ ਨੀਤੀ ਨੂੰ {0} ਲਈ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
|
||||
"UserPasswordChangedWithName": "ਪਾਸਵਰਡ ਯੂਜ਼ਰ ਲਈ ਬਦਲਿਆ ਗਿਆ ਹੈ {0}",
|
||||
"UserOnlineFromDevice": "{0} ਤੋਂ isਨਲਾਈਨ ਹੈ {1}",
|
||||
"UserPolicyUpdatedWithName": "ਵਰਤੋਂਕਾਰ ਨੀਤੀ ਨੂੰ {0} ਲਈ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
|
||||
"UserPasswordChangedWithName": "{0} ਵਰਤੋਂਕਾਰ ਲਈ ਪਾਸਵਰਡ ਬਦਲਿਆ ਗਿਆ ਸੀ",
|
||||
"UserOnlineFromDevice": "{0} ਨੂੰ {1} ਤੋਂ ਆਨਲਾਈਨ ਹੈ",
|
||||
"UserOfflineFromDevice": "{0} ਤੋਂ ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ {1}",
|
||||
"UserLockedOutWithName": "ਯੂਜ਼ਰ {0} ਨੂੰ ਲਾਕ ਆਉਟ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ",
|
||||
"UserDownloadingItemWithValues": "{0} ਡਾ{ਨਲੋਡ ਕਰ ਰਿਹਾ ਹੈ {1}",
|
||||
"UserDeletedWithName": "ਯੂਜ਼ਰ {0} ਨੂੰ ਮਿਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ",
|
||||
"UserCreatedWithName": "ਯੂਜ਼ਰ {0} ਬਣਾਇਆ ਗਿਆ ਹੈ",
|
||||
"User": "ਯੂਜ਼ਰ",
|
||||
"UserLockedOutWithName": "ਵਰਤੋਂਕਾਰ {0} ਨੂੰ ਲਾਕ ਕੀਤਾ ਗਿਆ ਹੈ",
|
||||
"UserDownloadingItemWithValues": "{0} {1} ਨੂੰ ਡਾਊਨਲੋਡ ਕਰ ਰਿਹਾ ਹੈ",
|
||||
"UserDeletedWithName": "ਵਰਤੋਂਕਾਰ {0} ਨੂੰ ਹਟਾਇਆ ਗਿਆ",
|
||||
"UserCreatedWithName": "ਵਰਤੋਂਕਾਰ {0} ਬਣਾਇਆ ਗਿਆ ਹੈ",
|
||||
"User": "ਵਰਤੋਂਕਾਰ",
|
||||
"Undefined": "ਪਰਿਭਾਸ਼ਤ",
|
||||
"TvShows": "ਟੀਵੀ ਸ਼ੋਅਜ਼",
|
||||
"TvShows": "ਟੀਵੀ ਸ਼ੋਅ",
|
||||
"System": "ਸਿਸਟਮ",
|
||||
"Sync": "ਸਿੰਕ",
|
||||
"SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾ toਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ",
|
||||
"StartupEmbyServerIsLoading": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ. ਕਿਰਪਾ ਕਰਕੇ ਜਲਦੀ ਹੀ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ.",
|
||||
"Songs": "ਗਾਣੇਂ",
|
||||
"SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾਊਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ। ਛੇਤੀ ਹੀ ਫ਼ੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।",
|
||||
"Songs": "ਗਾਣੇ",
|
||||
"Shows": "ਸ਼ੋਅ",
|
||||
"ServerNameNeedsToBeRestarted": "{0} ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ",
|
||||
"ScheduledTaskStartedWithName": "{0} ਸ਼ੁਰੂ ਹੋਇਆ",
|
||||
|
@ -57,12 +57,12 @@
|
|||
"Photos": "ਫੋਟੋਆਂ",
|
||||
"NotificationOptionVideoPlaybackStopped": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਰੋਕਿਆ ਗਿਆ",
|
||||
"NotificationOptionVideoPlayback": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ",
|
||||
"NotificationOptionUserLockedOut": "ਉਪਭੋਗਤਾ ਨੂੰ ਲਾਕ ਆਉਟ ਕੀਤਾ ਗਿਆ",
|
||||
"NotificationOptionUserLockedOut": "ਵਰਤੋਂਕਾਰ ਨੂੰ ਲਾਕ ਕੀਤਾ",
|
||||
"NotificationOptionTaskFailed": "ਨਿਰਧਾਰਤ ਕਾਰਜ ਅਸਫਲਤਾ",
|
||||
"NotificationOptionServerRestartRequired": "ਸਰਵਰ ਨੂੰ ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ",
|
||||
"NotificationOptionPluginUpdateInstalled": "ਪਲੱਗਇਨ ਅਪਡੇਟ ਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ",
|
||||
"NotificationOptionPluginUninstalled": "ਪਲੱਗਇਨ ਅਣਇੰਸਟੌਲ ਕੀਤਾ",
|
||||
"NotificationOptionPluginInstalled": "ਪਲੱਗਇਨ ਸਥਾਪਿਤ ਕੀਤਾ",
|
||||
"NotificationOptionPluginInstalled": "ਪਲੱਗਇਨ ਇੰਸਟਾਲ ਕੀਤੀ",
|
||||
"NotificationOptionPluginError": "ਪਲੱਗਇਨ ਅਸਫਲ",
|
||||
"NotificationOptionNewLibraryContent": "ਨਵੀਂ ਸਮੱਗਰੀ ਸ਼ਾਮਲ ਕੀਤੀ ਗਈ",
|
||||
"NotificationOptionInstallationFailed": "ਇੰਸਟਾਲੇਸ਼ਨ ਅਸਫਲ",
|
||||
|
@ -92,7 +92,7 @@
|
|||
"HomeVideos": "ਘਰੇਲੂ ਵੀਡੀਓ",
|
||||
"HeaderRecordingGroups": "ਰਿਕਾਰਡਿੰਗ ਸਮੂਹ",
|
||||
"HeaderNextUp": "ਅੱਗੇ",
|
||||
"HeaderLiveTV": "ਲਾਈਵ ਟੀ",
|
||||
"HeaderLiveTV": "ਲਾਈਵ ਟੀਵੀ",
|
||||
"HeaderFavoriteSongs": "ਮਨਪਸੰਦ ਗਾਣੇ",
|
||||
"HeaderFavoriteShows": "ਮਨਪਸੰਦ ਸ਼ੋਅ",
|
||||
"HeaderFavoriteEpisodes": "ਮਨਪਸੰਦ ਐਪੀਸੋਡ",
|
||||
|
@ -102,20 +102,22 @@
|
|||
"HeaderAlbumArtists": "ਐਲਬਮ ਕਲਾਕਾਰ",
|
||||
"Genres": "ਸ਼ੈਲੀਆਂ",
|
||||
"Forced": "ਮਜਬੂਰ",
|
||||
"Folders": "ਫੋਲਡਰਸ",
|
||||
"Folders": "ਫੋਲਡਰ",
|
||||
"Favorites": "ਮਨਪਸੰਦ",
|
||||
"FailedLoginAttemptWithUserName": "ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ {0}",
|
||||
"FailedLoginAttemptWithUserName": "{0} ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ",
|
||||
"DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ",
|
||||
"DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ",
|
||||
"Default": "ਡਿਫੌਲਟ",
|
||||
"Collections": "ਸੰਗ੍ਰਹਿਣ",
|
||||
"ChapterNameValue": "ਅਧਿਆਇ {0}",
|
||||
"ChapterNameValue": "ਚੈਪਟਰ {0}",
|
||||
"Channels": "ਚੈਨਲ",
|
||||
"CameraImageUploadedFrom": "ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ {0}",
|
||||
"CameraImageUploadedFrom": "{0} ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ",
|
||||
"Books": "ਕਿਤਾਬਾਂ",
|
||||
"AuthenticationSucceededWithUserName": "{0} ਸਫਲਤਾਪੂਰਕ ਪ੍ਰਮਾਣਿਤ",
|
||||
"Artists": "ਕਲਾਕਾਰ",
|
||||
"Application": "ਐਪਲੀਕੇਸ਼ਨ",
|
||||
"AppDeviceValues": "ਐਪ: {0}, ਜੰਤਰ: {1}",
|
||||
"Albums": "ਐਲਬਮਾਂ"
|
||||
"Albums": "ਐਲਬਮਾਂ",
|
||||
"TaskOptimizeDatabase": "ਡਾਟਾਬੇਸ ਅਨੁਕੂਲ ਬਣਾਓ",
|
||||
"External": "ਬਾਹਰੀ"
|
||||
}
|
||||
|
|
|
@ -121,5 +121,7 @@
|
|||
"TaskOptimizeDatabase": "Otimizar base de dados",
|
||||
"TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
|
||||
"External": "Externo",
|
||||
"HearingImpaired": "Problemas auditivos"
|
||||
"HearingImpaired": "Problemas auditivos",
|
||||
"TaskKeyframeExtractor": "Extrator de quadro-chave",
|
||||
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo."
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"Folders": "Папки",
|
||||
"Genres": "Жанры",
|
||||
"HeaderAlbumArtists": "Исполнители альбома",
|
||||
"HeaderContinueWatching": "Продолжение просмотра",
|
||||
"HeaderContinueWatching": "Продолжить просмотр",
|
||||
"HeaderFavoriteAlbums": "Избранные альбомы",
|
||||
"HeaderFavoriteArtists": "Избранные исполнители",
|
||||
"HeaderFavoriteEpisodes": "Избранные эпизоды",
|
||||
|
@ -42,7 +42,7 @@
|
|||
"MusicVideos": "Муз. видео",
|
||||
"NameInstallFailed": "Установка {0} неудачна",
|
||||
"NameSeasonNumber": "Сезон {0}",
|
||||
"NameSeasonUnknown": "Сезон неопознан",
|
||||
"NameSeasonUnknown": "Сезон не опознан",
|
||||
"NewVersionIsAvailable": "Новая версия Jellyfin Server доступна для загрузки.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Имеется обновление приложения",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Обновление приложения установлено",
|
||||
|
@ -96,7 +96,7 @@
|
|||
"TaskRefreshChannels": "Обновление каналов",
|
||||
"TaskCleanTranscode": "Очистка каталога перекодировки",
|
||||
"TaskUpdatePlugins": "Обновление плагинов",
|
||||
"TaskRefreshPeople": "Подновление людей",
|
||||
"TaskRefreshPeople": "Обновление информации о персонах",
|
||||
"TaskCleanLogs": "Очистка каталога журналов",
|
||||
"TaskRefreshLibrary": "Сканирование медиатеки",
|
||||
"TaskRefreshChapterImages": "Извлечение изображений сцен",
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"HeaderAlbumArtists": "Vaimbi vemadambarefu",
|
||||
"HeaderContinueWatching": "Simudzira kuona",
|
||||
"HeaderFavoriteSongs": "Nziyo dzaunofarira",
|
||||
"Albums": "Dambarefu",
|
||||
"AppDeviceValues": "Apu: {0}, Dhivhaisi: {1}",
|
||||
"Application": "Purogiramu",
|
||||
"Artists": "Vaimbi",
|
||||
"AuthenticationSucceededWithUserName": "apinda",
|
||||
"Books": "Mabhuku",
|
||||
"CameraImageUploadedFrom": "Mufananidzo mutsva vabva pakamera {0}",
|
||||
"Channels": "Machanewo",
|
||||
"ChapterNameValue": "Chikamu {0}",
|
||||
"Collections": "Akafanana",
|
||||
"Default": "Zvakasarudzwa Kare",
|
||||
"DeviceOfflineWithName": "{0} haasisipo",
|
||||
"DeviceOnlineWithName": "{0} aripo",
|
||||
"External": "Zvekunze",
|
||||
"FailedLoginAttemptWithUserName": "Vatadza kuloga chimboedza kushandisa {0}",
|
||||
"Favorites": "Zvaunofarira",
|
||||
"Folders": "Mafoodha",
|
||||
"Forced": "Zvekumanikidzira",
|
||||
"Genres": "Mhando",
|
||||
"HeaderFavoriteAlbums": "Madambarefu aunofarira",
|
||||
"HeaderFavoriteArtists": "Vaimbi vaunofarira",
|
||||
"HeaderFavoriteEpisodes": "Maepisodhi aunofarira",
|
||||
"HeaderFavoriteShows": "Masirisi aunofarira"
|
||||
}
|
|
@ -19,5 +19,24 @@
|
|||
"Channels": "ఛానెల్లు",
|
||||
"Books": "పుస్తకాలు",
|
||||
"Artists": "కళాకారులు",
|
||||
"Albums": "ఆల్బమ్లు"
|
||||
"Albums": "ఆల్బమ్లు",
|
||||
"HearingImpaired": "వినికిడి లోపం",
|
||||
"HomeVideos": "హోమ్ వీడియోలు",
|
||||
"AppDeviceValues": "అప్లికేషన్ : {0}, పరికరం: {1}",
|
||||
"Application": "అప్లికేషన్",
|
||||
"AuthenticationSucceededWithUserName": "విజయవంతంగా ఆమోదించబడింది",
|
||||
"CameraImageUploadedFrom": "{0} నుండి కొత్త కెమెరా చిత్రం అప్లోడ్ చేయబడింది",
|
||||
"ChapterNameValue": "అధ్యాయం",
|
||||
"DeviceOfflineWithName": "{0} డిస్కనెక్ట్ చేయబడింది",
|
||||
"DeviceOnlineWithName": "{0} కనెక్ట్ చేయబడింది",
|
||||
"External": "బాహ్య",
|
||||
"FailedLoginAttemptWithUserName": "{0} నుండి విఫలమైన లాగిన్ ప్రయత్నం",
|
||||
"HeaderFavoriteAlbums": "ఇష్టమైన ఆల్బమ్లు",
|
||||
"HeaderFavoriteArtists": "ఇష్టమైన కళాకారులు",
|
||||
"HeaderFavoriteEpisodes": "ఇష్టమైన ఎపిసోడ్లు",
|
||||
"HeaderFavoriteShows": "ఇష్టమైన ప్రదర్శనలు",
|
||||
"HeaderFavoriteSongs": "ఇష్టమైన పాటలు",
|
||||
"HeaderLiveTV": "ప్రత్యక్ష TV",
|
||||
"HeaderNextUp": "తదుపరి",
|
||||
"HeaderRecordingGroups": "రికార్డింగ్ గుంపులు"
|
||||
}
|
||||
|
|
|
@ -123,5 +123,6 @@
|
|||
"TaskOptimizeDatabase": "Veritabanını optimize et",
|
||||
"TaskKeyframeExtractorDescription": "Daha hassas HLS çalma listeleri oluşturmak için video dosyalarından kareleri çıkarır. Bu görev uzun bir süre çalışabilir.",
|
||||
"TaskKeyframeExtractor": "Kare Ayırt Edici",
|
||||
"External": "Harici"
|
||||
"External": "Harici",
|
||||
"HearingImpaired": "Duyma engelli"
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
"Shows": "Шоу",
|
||||
"ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
|
||||
"ScheduledTaskStartedWithName": "{0} розпочато",
|
||||
"ScheduledTaskFailedWithName": "Помилка {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} незавершено, збій",
|
||||
"ProviderValue": "Постачальник: {0}",
|
||||
"PluginUpdatedWithName": "{0} оновлено",
|
||||
"PluginUninstalledWithName": "{0} видалено",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue