Merge branch 'jellyfin:master' into master

This commit is contained in:
Ronan Charles-Lorel 2023-06-29 15:08:52 +02:00 committed by GitHub
commit e108183b13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
683 changed files with 35168 additions and 29307 deletions

View File

@ -47,7 +47,7 @@ jobs:
displayName: Set release version (stable) displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') 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' 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)' - 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)'

View File

@ -30,9 +30,9 @@ body:
label: Jellyfin Version label: Jellyfin Version
description: What version of Jellyfin are you running? description: What version of Jellyfin are you running?
options: options:
- 10.8.0 - 10.8.z
- 10.8.9
- 10.7.7 - 10.7.7
- 10.7.z
- 10.6.4 - 10.6.4
- Other - Other
validations: validations:
@ -47,13 +47,15 @@ body:
label: Environment label: Environment
description: | description: |
Examples: 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] - **Virtualization**: [e.g. Docker, KVM, LXC]
- **Clients**: [Browser, Android, Fire Stick, etc.] - **Clients**: [Browser, Android, Fire Stick, etc.]
- **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13] - **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] - **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.] - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.] - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.] - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example] - **Base URL**: [e.g. none, yes: /example]
@ -61,12 +63,14 @@ body:
- **Storage**: [e.g. local, NFS, cloud] - **Storage**: [e.g. local, NFS, cloud]
value: | value: |
- OS: - OS:
- Linux Kernel:
- Virtualization: - Virtualization:
- Clients: - Clients:
- Browser: - Browser:
- FFmpeg Version: - FFmpeg Version:
- Playback Method: - Playback Method:
- Hardware Acceleration: - Hardware Acceleration:
- GPU Model:
- Plugins: - Plugins:
- Reverse Proxy: - Reverse Proxy:
- Base URL: - Base URL:
@ -84,8 +88,8 @@ body:
id: ffmpeg-logs id: ffmpeg-logs
attributes: attributes:
label: FFmpeg logs label: FFmpeg logs
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs. description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log.
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. 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 render: shell
- type: textarea - type: textarea
id: browserlogs id: browserlogs

View File

@ -19,6 +19,7 @@ jobs:
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with: with:
dirtyLabel: 'merge conflict' 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 }} repoToken: ${{ secrets.JF_BOT_TOKEN }}
project: project:

View File

@ -20,18 +20,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with: with:
dotnet-version: '7.0.x' dotnet-version: '7.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 uses: github/codeql-action/analyze@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1

View File

@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - name: Notify as seen
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }} comment-id: ${{ github.event.comment.id }}
reactions: '+1' reactions: '+1'
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - name: Notify as seen
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null }} if: ${{ github.event.comment != null }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -51,14 +51,14 @@ jobs:
reactions: eyes reactions: eyes
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
- name: Notify as running - name: Notify as running
id: comment_running id: comment_running
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null }} if: ${{ github.event.comment != null }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -93,7 +93,7 @@ jobs:
exit ${retcode} exit ${retcode}
- name: Notify with result success - name: Notify with result success
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null && success() }} if: ${{ github.event.comment != null && success() }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -108,7 +108,7 @@ jobs:
reactions: hooray reactions: hooray
- name: Notify with result failure - name: Notify with result failure
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null && failure() }} if: ${{ github.event.comment != null && failure() }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}

View File

@ -14,18 +14,18 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with: with:
dotnet-version: '7.0.x' dotnet-version: '7.0.x'
- name: Generate openapi.json - 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" 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 - name: Upload openapi.json
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3 uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with: with:
name: openapi-head name: openapi-head
retention-days: 14 retention-days: 14
@ -39,25 +39,27 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0 fetch-depth: 0
- name: Checkout common ancestor - name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
run: | run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }} 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/* 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 git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with: with:
dotnet-version: '7.0.x' dotnet-version: '7.0.x'
- name: Generate openapi.json - 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" 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 - name: Upload openapi.json
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3 uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with: with:
name: openapi-base name: openapi-base
retention-days: 14 retention-days: 14
@ -76,12 +78,12 @@ jobs:
- openapi-base - openapi-base
steps: steps:
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
- name: Download openapi-base - name: Download openapi-base
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with: with:
name: openapi-base name: openapi-base
path: openapi-base path: openapi-base
@ -103,14 +105,14 @@ jobs:
body="${body//$'\r'/'%0D'}" body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body echo ::set-output name=body::$body
- name: Find difference comment - name: Find difference comment
uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2 uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0
id: find-comment id: find-comment
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
direction: last direction: last
body-includes: openapi-diff-workflow-comment body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed) - name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ steps.read-diff.outputs.body != '' }} if: ${{ steps.read-diff.outputs.body != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@ -125,7 +127,7 @@ jobs:
</details> </details>
- name: Edit difference comment (unchanged) - name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}

View File

@ -1,4 +1,4 @@
name: Issue Stale Check name: Stale Check
on: on:
schedule: schedule:
@ -7,12 +7,15 @@ on:
permissions: permissions:
issues: write issues: write
pull-requests: write
jobs: jobs:
stale: issues:
name: Check issues
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
- uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7 - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with: with:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120 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. 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). 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.

3
.npmrc
View File

@ -1,3 +0,0 @@
registry=https://registry.npmjs.org/
@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
always-auth=true

View File

@ -58,6 +58,7 @@
- [HelloWorld017](https://github.com/HelloWorld017) - [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog) - [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga) - [jftuga](https://github.com/jftuga)
- [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h) - [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface) - [joshuaboniface](https://github.com/joshuaboniface)
- [JustAMan](https://github.com/JustAMan) - [JustAMan](https://github.com/JustAMan)
@ -125,6 +126,7 @@
- [SuperSandro2000](https://github.com/SuperSandro2000) - [SuperSandro2000](https://github.com/SuperSandro2000)
- [tbraeutigam](https://github.com/tbraeutigam) - [tbraeutigam](https://github.com/tbraeutigam)
- [teacupx](https://github.com/teacupx) - [teacupx](https://github.com/teacupx)
- [TelepathicWalrus](https://github.com/TelepathicWalrus)
- [Terror-Gene](https://github.com/Terror-Gene) - [Terror-Gene](https://github.com/Terror-Gene)
- [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu) - [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
- [ThibaultNocchi](https://github.com/ThibaultNocchi) - [ThibaultNocchi](https://github.com/ThibaultNocchi)
@ -162,6 +164,8 @@
- [vgambier](https://github.com/vgambier) - [vgambier](https://github.com/vgambier)
- [MinecraftPlaye](https://github.com/MinecraftPlaye) - [MinecraftPlaye](https://github.com/MinecraftPlaye)
- [RealGreenDragon](https://github.com/RealGreenDragon) - [RealGreenDragon](https://github.com/RealGreenDragon)
- [ipitio](https://github.com/ipitio)
- [TheTyrius](https://github.com/TheTyrius)
# Emby Contributors # Emby Contributors
@ -231,3 +235,4 @@
- [Matthew Jones](https://github.com/matthew-jones-uk) - [Matthew Jones](https://github.com/matthew-jones-uk)
- [Jakob Kukla](https://github.com/jakobkukla) - [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir) - [Utku Özdemir](https://github.com/utkuozdemir)
- [JPUC1143](https://github.com/Jpuc1143/)

92
Directory.Packages.props Normal file
View File

@ -0,0 +1,92 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<!-- 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.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="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.9.2" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<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.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.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.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.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.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.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.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="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="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="3.0.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.0" />
<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.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.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" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.4.2" />
</ItemGroup>
</Project>

View File

@ -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 - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \ && npm ci --no-audit --unsafe-perm \
&& npm run build:production \
&& mv dist /dist && mv dist /dist
FROM debian:stable-slim as app FROM debian:stable-slim as app
@ -37,7 +38,7 @@ RUN apt-get update \
&& apt-get update \ && apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \ && apt-get install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers \ mesa-va-drivers \
jellyfin-ffmpeg \ jellyfin-ffmpeg5 \
openssl \ openssl \
locales \ locales \
# Intel VAAPI Tone mapping dependencies: # Intel VAAPI Tone mapping dependencies:

View File

@ -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 - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \ && npm ci --no-audit --unsafe-perm \
&& npm run build:production \
&& mv dist /dist && mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu FROM multiarch/qemu-user-static:x86_64-arm as qemu

View File

@ -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 - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \ && npm ci --no-audit --unsafe-perm \
&& npm run build:production \
&& mv dist /dist && mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu

View File

@ -27,7 +27,7 @@ namespace Emby.Dlna.ConnectionManager
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns> /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables() private static IEnumerable<StateVariable> GetStateVariables()
{ {
var list = new List<StateVariable> return new StateVariable[]
{ {
new StateVariable new StateVariable
{ {
@ -114,8 +114,6 @@ namespace Emby.Dlna.ConnectionManager
SendsEvents = false SendsEvents = false
} }
}; };
return list;
} }
} }
} }

View File

@ -27,7 +27,7 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns> /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables() private static IEnumerable<StateVariable> GetStateVariables()
{ {
var list = new List<StateVariable> return new StateVariable[]
{ {
new StateVariable new StateVariable
{ {
@ -154,8 +154,6 @@ namespace Emby.Dlna.ContentDirectory
SendsEvents = false SendsEvents = false
} }
}; };
return list;
} }
} }
} }

View File

@ -10,6 +10,7 @@ using System.Text;
using System.Xml; using System.Xml;
using Emby.Dlna.ContentDirectory; using Emby.Dlna.ContentDirectory;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -870,11 +871,11 @@ namespace Emby.Dlna.Didl
var types = new[] var types = new[]
{ {
PersonType.Director, PersonKind.Director,
PersonType.Writer, PersonKind.Writer,
PersonType.Producer, PersonKind.Producer,
PersonType.Composer, PersonKind.Composer,
"creator" PersonKind.Creator
}; };
// Seeing some LG models locking up due content with large lists of people // 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) foreach (var actor in people)
{ {
var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) var type = types.FirstOrDefault(i => i == actor.Type || string.Equals(actor.Role, i.ToString(), StringComparison.OrdinalIgnoreCase));
?? PersonType.Actor; if (type == PersonKind.Unknown)
{
type = PersonKind.Actor;
}
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp); AddValue(writer, "upnp", type.ToString().ToLowerInvariant(), actor.Name, NsUpnp);
} }
} }

View File

@ -28,13 +28,13 @@
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4"> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -80,7 +80,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -164,7 +164,7 @@ namespace Emby.Dlna.Eventing
try try
{ {
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)

View File

@ -7,7 +7,6 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.PlayTo; using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;

View File

@ -2,9 +2,11 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
@ -15,7 +17,10 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo 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 ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
@ -44,25 +49,44 @@ namespace Emby.Dlna.PlayTo
private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) 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(); 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 try
{ {
return await XDocument.LoadAsync( return await XDocument.LoadAsync(
stream, ms,
LoadOptions.None, LoadOptions.None,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }
catch (XmlException ex) catch (XmlException)
{ {
_logger.LogError(ex, "Failed to parse response"); // try correcting the Xml response with common errors
if (_logger.IsEnabled(LogLevel.Debug)) ms.Position = 0;
{ using StreamReader sr = new StreamReader(ms);
_logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)); var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
}
return null; // find and replace unescaped ampersands (&)
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
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 // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false); 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();
} }
} }

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -66,7 +64,8 @@ namespace Emby.Dlna.PlayTo
IUserDataManager userDataManager, IUserDataManager userDataManager,
ILocalizationManager localization, ILocalizationManager localization,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder) IMediaEncoder mediaEncoder,
Device device)
{ {
_session = session; _session = session;
_sessionManager = sessionManager; _sessionManager = sessionManager;
@ -82,14 +81,7 @@ namespace Emby.Dlna.PlayTo
_localization = localization; _localization = localization;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
}
public bool IsSessionActive => !_disposed && _device is not null;
public bool SupportsMediaControl => IsSessionActive;
public void Init(Device device)
{
_device = device; _device = device;
_device.OnDeviceUnavailable = OnDeviceUnavailable; _device.OnDeviceUnavailable = OnDeviceUnavailable;
_device.PlaybackStart += OnDevicePlaybackStart; _device.PlaybackStart += OnDevicePlaybackStart;
@ -102,6 +94,10 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft; _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. * 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; var info = e.Argument;
if (!_disposed 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(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
&& (usn.IndexOf("MediaRenderer:", 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))) && nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
{ {
OnDeviceUnavailable(); OnDeviceUnavailable();
} }
} }
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e) private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
{ {
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url)) 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) 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) 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) if (_disposed)
{ {
@ -486,9 +482,9 @@ namespace Emby.Dlna.PlayTo
private PlaylistItem CreatePlaylistItem( private PlaylistItem CreatePlaylistItem(
BaseItem item, BaseItem item,
User user, User? user,
long startPostionTicks, long startPostionTicks,
string mediaSourceId, string? mediaSourceId,
int? audioStreamIndex, int? audioStreamIndex,
int? subtitleStreamIndex) int? subtitleStreamIndex)
{ {
@ -525,7 +521,7 @@ namespace Emby.Dlna.PlayTo
return playlistItem; return playlistItem;
} }
private string GetDlnaHeaders(PlaylistItem item) private string? GetDlnaHeaders(PlaylistItem item)
{ {
var profile = item.Profile; var profile = item.Profile;
var streamInfo = item.StreamInfo; var streamInfo = item.StreamInfo;
@ -579,7 +575,7 @@ namespace Emby.Dlna.PlayTo
return null; 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)) if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{ {
@ -696,7 +692,6 @@ namespace Emby.Dlna.PlayTo
_device.MediaChanged -= OnDeviceMediaChanged; _device.MediaChanged -= OnDeviceMediaChanged;
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
_device.OnDeviceUnavailable = null; _device.OnDeviceUnavailable = null;
_device = null;
_disposed = true; _disposed = true;
} }
@ -716,7 +711,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.ToggleMute: case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken); return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex: 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)) 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"); throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
case GeneralCommandType.SetVolume: 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)) if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{ {
@ -865,34 +860,19 @@ namespace Emby.Dlna.PlayTo
throw new ObjectDisposedException(GetType().Name); throw new ObjectDisposedException(GetType().Name);
} }
if (_device is null) return name switch
{ {
return Task.CompletedTask; SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
} SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
if (name == SessionMessageType.Play) _ => Task.CompletedTask // Not supported or needed right now
{ };
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;
} }
private class StreamParams private class StreamParams
{ {
private MediaSourceInfo _mediaSource; private MediaSourceInfo? _mediaSource;
private IMediaSourceManager _mediaSourceManager; private IMediaSourceManager? _mediaSourceManager;
public Guid ItemId { get; set; } public Guid ItemId { get; set; }
@ -904,17 +884,17 @@ namespace Emby.Dlna.PlayTo
public int? SubtitleStreamIndex { get; set; } 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) if (_mediaSource is not null)
{ {
@ -944,8 +924,8 @@ namespace Emby.Dlna.PlayTo
{ {
var part = parts[i]; var part = parts[i];
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) || if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase)) || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
{ {
if (Guid.TryParse(parts[i + 1], out var result)) if (Guid.TryParse(parts[i + 1], out var result))
{ {

View File

@ -205,12 +205,11 @@ namespace Emby.Dlna.PlayTo
_userDataManager, _userDataManager,
_localization, _localization,
_mediaSourceManager, _mediaSourceManager,
_mediaEncoder); _mediaEncoder,
device);
sessionInfo.AddController(controller); sessionInfo.AddController(controller);
controller.Init(device);
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ?? var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
_dlnaManager.GetDefaultProfile(); _dlnaManager.GetDefaultProfile();

View File

@ -116,7 +116,7 @@ namespace Emby.Dlna.PlayTo
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); 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; 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; var stateString = string.Empty;
@ -150,9 +150,9 @@ namespace Emby.Dlna.PlayTo
{ {
stateString += BuildArgumentXml(arg, "0"); 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 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 = "") private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")

View File

@ -147,11 +147,16 @@ namespace Emby.Dlna.Server
} }
} }
private string GetFriendlyName() internal string GetFriendlyName()
{ {
if (string.IsNullOrEmpty(_profile.FriendlyName)) 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>(); 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); return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
return name ?? string.Empty;
} }
private void AppendIconList(StringBuilder builder) private void AppendIconList(StringBuilder builder)

View File

@ -3,6 +3,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Emby.Naming.Common; using Emby.Naming.Common;
using Jellyfin.Extensions;
namespace Emby.Naming.Audio namespace Emby.Naming.Audio
{ {
@ -58,13 +59,7 @@ namespace Emby.Naming.Audio
var tmp = trimmedFilename.Slice(prefix.Length).Trim(); var tmp = trimmedFilename.Slice(prefix.Length).Trim();
int index = tmp.IndexOf(' '); if (int.TryParse(tmp.LeftPart(' '), CultureInfo.InvariantCulture, out _))
if (index != -1)
{
tmp = tmp.Slice(0, index);
}
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{ {
return true; return true;
} }

View File

@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook
var fileName = Path.GetFileNameWithoutExtension(path); var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions) 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 (match.Success)
{ {
if (!result.ChapterNumber.HasValue) if (!result.ChapterNumber.HasValue)
@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["chapter"]; var value = match.Groups["chapter"];
if (value.Success) 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; result.ChapterNumber = intValue;
} }
@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["part"]; var value = match.Groups["part"];
if (value.Success) 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; result.PartNumber = intValue;
} }

View File

@ -79,25 +79,25 @@ namespace Emby.Naming.AudioBook
{ {
if (group.Count() > 1 || haveChaptersOrPages) if (group.Count() > 1 || haveChaptersOrPages)
{ {
var ex = new List<AudioBookFileInfo>(); List<AudioBookFileInfo>? ex = null;
var alt = new List<AudioBookFileInfo>(); List<AudioBookFileInfo>? alt = null;
foreach (var audioFile in group) foreach (var audioFile in group)
{ {
var name = Path.GetFileNameWithoutExtension(audioFile.Path); var name = Path.GetFileNameWithoutExtension(audioFile.Path.AsSpan());
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) || if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase)
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) || || name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase)
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase)) || name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
{ {
alt.Add(audioFile); (alt ??= new()).Add(audioFile);
} }
else else
{ {
ex.Add(audioFile); (ex ??= new()).Add(audioFile);
} }
} }
if (ex.Count > 0) if (ex is not null)
{ {
var extra = ex var extra = ex
.OrderBy(x => x.Container) .OrderBy(x => x.Container)
@ -108,7 +108,7 @@ namespace Emby.Naming.AudioBook
extras.AddRange(extra); extras.AddRange(extra);
} }
if (alt.Count > 0) if (alt is not null)
{ {
var alternatives = alt var alternatives = alt
.OrderBy(x => x.Container) .OrderBy(x => x.Container)

View File

@ -30,7 +30,7 @@ namespace Emby.Naming.AudioBook
AudioBookNameParserResult result = default; AudioBookNameParserResult result = default;
foreach (var expression in _options.AudioBookNamesExpressions) 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 (match.Success)
{ {
if (result.Name is null) if (result.Name is null)
@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["year"]; var value = match.Groups["year"];
if (value.Success) 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; result.Year = intValue;
} }

View File

@ -141,8 +141,7 @@ namespace Emby.Naming.Common
VideoFileStackingRules = new[] 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>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false), new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
}; };
CleanDateTimes = new[] CleanDateTimes = new[]
@ -157,7 +156,8 @@ namespace Emby.Naming.Common
@"^(?<cleaned>.+?)(\[.*\])", @"^(?<cleaned>.+?)(\[.*\])",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)", @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)", @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$" @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
@"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
}; };
SubtitleFileExtensions = new[] SubtitleFileExtensions = new[]
@ -270,7 +270,6 @@ namespace Emby.Naming.Common
".sfx", ".sfx",
".shn", ".shn",
".sid", ".sid",
".spc",
".stm", ".stm",
".strm", ".strm",
".ult", ".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. // so we make sure this one gets tested first.
// "Foo Bar 889" // "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$") 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[] VideoExtraRules = new[]
{ {
new ExtraRule( new ExtraRule(
@ -797,16 +794,6 @@ namespace Emby.Naming.Common
/// </summary> /// </summary>
public EpisodeExpression[] EpisodeExpressions { get; set; } 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> /// <summary>
/// Gets or sets list of video file extensions. /// Gets or sets list of video file extensions.
/// </summary> /// </summary>
@ -877,16 +864,6 @@ namespace Emby.Naming.Common
/// </summary> /// </summary>
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>(); 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> /// <summary>
/// Compiles raw regex strings into regexes. /// Compiles raw regex strings into regexes.
/// </summary> /// </summary>
@ -894,8 +871,6 @@ namespace Emby.Naming.Common
{ {
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray(); CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray(); CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray();
} }
private Regex Compile(string exp) private Regex Compile(string exp)

View File

@ -42,18 +42,18 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4"> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -113,7 +113,7 @@ namespace Emby.Naming.TV
if (expression.DateTimeFormats.Length > 0) if (expression.DateTimeFormats.Length > 0)
{ {
if (DateTime.TryParseExact( if (DateTime.TryParseExact(
match.Groups[0].Value, match.Groups[0].ValueSpan,
expression.DateTimeFormats, expression.DateTimeFormats,
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
DateTimeStyles.None, DateTimeStyles.None,
@ -125,7 +125,7 @@ namespace Emby.Naming.TV
result.Success = true; 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.Year = date.Year;
result.Month = date.Month; result.Month = date.Month;
@ -138,12 +138,12 @@ namespace Emby.Naming.TV
} }
else if (expression.IsNamed) 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; 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; result.EpisodeNumber = num;
} }
@ -158,7 +158,7 @@ namespace Emby.Naming.TV
if (nextIndex >= name.Length if (nextIndex >= name.Length
|| !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal)) || !"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; result.EndingEpisodeNumber = num;
} }
@ -170,12 +170,12 @@ namespace Emby.Naming.TV
} }
else 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; 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; result.EpisodeNumber = num;
} }

View File

@ -14,7 +14,7 @@ namespace Emby.Naming.TV
/// Used for removing separators between words, i.e turns "The_show" into "The show" while /// Used for removing separators between words, i.e turns "The_show" into "The show" while
/// preserving namings like "S.H.O.W". /// preserving namings like "S.H.O.W".
/// </summary> /// </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> /// <summary>
/// Resolve information about series from path. /// Resolve information about series from path.

View File

@ -43,7 +43,7 @@ namespace Emby.Naming.Video
&& match.Groups.Count == 5 && match.Groups.Count == 5
&& match.Groups[1].Success && match.Groups[1].Success
&& match.Groups[2].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); result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
return true; return true;

View File

@ -56,7 +56,7 @@ namespace Emby.Naming.Video
} }
else if (rule.RuleType == ExtraRuleType.Regex) 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); var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);

View File

@ -17,7 +17,7 @@ public class FileStackRule
/// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param> /// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
public FileStackRule(string token, bool isNumerical) public FileStackRule(string token, bool isNumerical)
{ {
_tokenRegex = new Regex(token, RegexOptions.IgnoreCase); _tokenRegex = new Regex(token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
IsNumerical = isNumerical; IsNumerical = isNumerical;
} }

View File

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Emby.Naming.Common; using Emby.Naming.Common;
using Jellyfin.Extensions;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
namespace Emby.Naming.Video namespace Emby.Naming.Video
@ -13,6 +14,8 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
public static class VideoListResolver public static class VideoListResolver
{ {
private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary> /// <summary>
/// Resolves alternative versions and extras from list of video files. /// Resolves alternative versions and extras from list of video files.
/// </summary> /// </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] // 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++) for (var i = 0; i < videos.Count; i++)
{ {
var video = videos[i]; var video = videos[i];
@ -114,29 +118,43 @@ namespace Emby.Naming.Video
continue; continue;
} }
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions)) if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
{ {
return videos; 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 if (videos.Count > 1)
videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); {
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> var list = new List<VideoInfo>
{ {
videos[0] primary
}; };
var alternateVersionsLen = videos.Count - 1; list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
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].Name = folderName.ToString(); list[0].Name = folderName.ToString();
return list; return list;
@ -161,9 +179,8 @@ namespace Emby.Naming.Video
return true; 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)) if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{ {
return false; return false;
@ -176,16 +193,15 @@ namespace Emby.Naming.Video
} }
// There are no span overloads for regex unfortunately // There are no span overloads for regex unfortunately
var tmpTestFilename = testFilename.ToString(); if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{ {
tmpTestFilename = cleanName.Trim(); testFilename = cleanName.AsSpan().Trim();
} }
// The CleanStringParser should have removed common keywords etc. // The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(tmpTestFilename) return testFilename.IsEmpty
|| testFilename[0] == '-' || testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
} }
} }
} }

View File

@ -87,8 +87,7 @@ namespace Emby.Naming.Video
name = cleanDateTimeResult.Name; name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year; year = cleanDateTimeResult.Year;
if (extraResult.ExtraType is null if (TryCleanString(name, namingOptions, out var newName))
&& TryCleanString(name, namingOptions, out var newName))
{ {
name = newName; name = newName;
} }

View File

@ -15,7 +15,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="TagLibSharp" Version="2.3.0" /> <PackageReference Include="TagLibSharp" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@ -26,13 +26,13 @@
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4"> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -33,15 +31,10 @@ namespace Emby.Server.Implementations.AppBase
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
/// <summary>
/// The _configuration loaded.
/// </summary>
private bool _configurationLoaded;
/// <summary> /// <summary>
/// The _configuration. /// The _configuration.
/// </summary> /// </summary>
private BaseApplicationConfiguration _configuration; private BaseApplicationConfiguration? _configuration;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class. /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase
/// <summary> /// <summary>
/// Occurs when [configuration updated]. /// Occurs when [configuration updated].
/// </summary> /// </summary>
public event EventHandler<EventArgs> ConfigurationUpdated; public event EventHandler<EventArgs>? ConfigurationUpdated;
/// <summary> /// <summary>
/// Occurs when [configuration updating]. /// Occurs when [configuration updating].
/// </summary> /// </summary>
public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating; public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
/// <summary> /// <summary>
/// Occurs when [named configuration updated]. /// Occurs when [named configuration updated].
/// </summary> /// </summary>
public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated; public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
/// <summary> /// <summary>
/// Gets the type of the configuration. /// Gets the type of the configuration.
@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase
{ {
get get
{ {
if (_configurationLoaded) if (_configuration is not null)
{ {
return _configuration; return _configuration;
} }
lock (_configurationSyncLock) lock (_configurationSyncLock)
{ {
if (_configurationLoaded) if (_configuration is not null)
{ {
return _configuration; return _configuration;
} }
_configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer); return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
_configurationLoaded = true;
return _configuration;
} }
} }
protected set protected set
{ {
_configuration = value; _configuration = value;
_configurationLoaded = value is not null;
} }
} }
@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase
Logger.LogInformation("Saving system configuration"); Logger.LogInformation("Saving system configuration");
var path = CommonApplicationPaths.SystemConfigurationFilePath; var path = CommonApplicationPaths.SystemConfigurationFilePath;
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
lock (_configurationSyncLock) lock (_configurationSyncLock)
{ {
@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase
private object LoadConfiguration(string path, Type configurationType) private object LoadConfiguration(string path, Type configurationType)
{ {
if (!File.Exists(path))
{
return Activator.CreateInstance(configurationType);
}
try try
{ {
return XmlSerializer.DeserializeFromFile(configurationType, path); if (File.Exists(path))
{
return XmlSerializer.DeserializeFromFile(configurationType, path);
}
} }
catch (IOException) catch (Exception ex) when (ex is not IOException)
{
return Activator.CreateInstance(configurationType);
}
catch (Exception ex)
{ {
Logger.LogError(ex, "Error loading configuration file: {Path}", path); Logger.LogError(ex, "Error loading configuration file: {Path}", path);
return Activator.CreateInstance(configurationType);
} }
return Activator.CreateInstance(configurationType)
?? throw new InvalidOperationException("Configuration type can't be Nullable<T>.");
} }
/// <inheritdoc /> /// <inheritdoc />
@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration); _configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
var path = GetConfigurationFile(key); var path = GetConfigurationFile(key);
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
lock (_configurationSyncLock) lock (_configurationSyncLock)
{ {

View File

@ -11,7 +11,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -81,11 +80,13 @@ using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV; using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
@ -113,15 +114,11 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
{ {
/// <summary>
/// The environment variable prefixes to log at server startup.
/// </summary>
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
/// <summary> /// <summary>
/// The disposable parts. /// The disposable parts.
/// </summary> /// </summary>
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new(); private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
private readonly DeviceId _deviceId;
private readonly IFileSystem _fileSystemManager; private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig; private readonly IConfiguration _startupConfig;
@ -130,7 +127,6 @@ namespace Emby.Server.Implementations
private readonly IPluginManager _pluginManager; private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances; private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
/// <summary> /// <summary>
@ -139,8 +135,6 @@ namespace Emby.Server.Implementations
/// <value>All concrete types.</value> /// <value>All concrete types.</value>
private Type[] _allConcreteTypes; private Type[] _allConcreteTypes;
private DeviceId _deviceId;
private bool _disposed = false; private bool _disposed = false;
/// <summary> /// <summary>
@ -164,6 +158,7 @@ namespace Emby.Server.Implementations
Logger = LoggerFactory.CreateLogger<ApplicationHost>(); Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager)); _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3); ApplicationVersionString = ApplicationVersion.ToString(3);
@ -191,23 +186,9 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; } public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
{ && !_startupOptions.IsService
get && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
{
if (!Environment.UserInteractive)
{
return false;
}
if (_startupOptions.IsService)
{
return false;
}
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
}
}
/// <summary> /// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance. /// Gets the <see cref="INetworkManager"/> singleton instance.
@ -284,15 +265,7 @@ namespace Emby.Server.Implementations
/// <value>The application name.</value> /// <value>The application name.</value>
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName; public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
public string SystemId public string SystemId => _deviceId.Value;
{
get
{
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public string Name => ApplicationProductName; public string Name => ApplicationProductName;
@ -445,7 +418,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated; ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
_mediaEncoder.SetFFmpegPath(); Resolve<IMediaEncoder>().SetFFmpegPath();
Logger.LogInformation("ServerId: {ServerId}", SystemId); Logger.LogInformation("ServerId: {ServerId}", SystemId);
@ -558,6 +531,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
@ -652,50 +627,19 @@ namespace Emby.Server.Implementations
} }
} }
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false); await localizationManager.LoadAll().ConfigureAwait(false);
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>(); _sessionManager = Resolve<ISessionManager>();
SetStaticProperties(); SetStaticProperties();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
FindParts(); FindParts();
} }
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
{
// Distinct these to prevent users from reporting problems that aren't actually problems
var commandLineArgs = Environment
.GetCommandLineArgs()
.Distinct();
// Get all relevant environment variables
var allEnvVars = Environment.GetEnvironmentVariables();
var relevantEnvVars = new Dictionary<object, object>();
foreach (var key in allEnvVars.Keys)
{
if (_relevantEnvVarPrefixes.Any(prefix => key.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
{
relevantEnvVars.Add(key, allEnvVars[key]);
}
}
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
logger.LogInformation("Arguments: {Args}", commandLineArgs);
logger.LogInformation("Operating system: {OS}", RuntimeInformation.OSDescription);
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
}
private X509Certificate2 GetCertificate(string path, string password) private X509Certificate2 GetCertificate(string path, string password)
{ {
if (string.IsNullOrWhiteSpace(path)) if (string.IsNullOrWhiteSpace(path))
@ -782,10 +726,6 @@ namespace Emby.Server.Implementations
Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
Resolve<ISubtitleManager>().AddParts(GetExports<ISubtitleProvider>());
Resolve<IChannelManager>().AddParts(GetExports<IChannel>());
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
} }
@ -1248,10 +1188,13 @@ namespace Emby.Server.Implementations
} }
} }
// used for closing websockets if (_sessionManager != null)
foreach (var session in _sessionManager.Sessions)
{ {
await session.DisposeAsync().ConfigureAwait(false); // used for closing websockets
foreach (var session in _sessionManager.Sessions)
{
await session.DisposeAsync().ConfigureAwait(false);
}
} }
} }
} }

View File

@ -66,6 +66,7 @@ namespace Emby.Server.Implementations.Channels
/// <param name="userDataManager">The user data manager.</param> /// <param name="userDataManager">The user data manager.</param>
/// <param name="providerManager">The provider manager.</param> /// <param name="providerManager">The provider manager.</param>
/// <param name="memoryCache">The memory cache.</param> /// <param name="memoryCache">The memory cache.</param>
/// <param name="channels">The channels.</param>
public ChannelManager( public ChannelManager(
IUserManager userManager, IUserManager userManager,
IDtoService dtoService, IDtoService dtoService,
@ -75,7 +76,8 @@ namespace Emby.Server.Implementations.Channels
IFileSystem fileSystem, IFileSystem fileSystem,
IUserDataManager userDataManager, IUserDataManager userDataManager,
IProviderManager providerManager, IProviderManager providerManager,
IMemoryCache memoryCache) IMemoryCache memoryCache,
IEnumerable<IChannel> channels)
{ {
_userManager = userManager; _userManager = userManager;
_dtoService = dtoService; _dtoService = dtoService;
@ -86,18 +88,13 @@ namespace Emby.Server.Implementations.Channels
_userDataManager = userDataManager; _userDataManager = userDataManager;
_providerManager = providerManager; _providerManager = providerManager;
_memoryCache = memoryCache; _memoryCache = memoryCache;
}
internal IChannel[] Channels { get; private set; }
private static TimeSpan CacheLength => TimeSpan.FromHours(3);
/// <inheritdoc />
public void AddParts(IEnumerable<IChannel> channels)
{
Channels = channels.ToArray(); Channels = channels.ToArray();
} }
internal IChannel[] Channels { get; }
private static TimeSpan CacheLength => TimeSpan.FromHours(3);
/// <inheritdoc /> /// <inheritdoc />
public bool EnableMediaSourceDisplay(BaseItem item) public bool EnableMediaSourceDisplay(BaseItem item)
{ {
@ -160,16 +157,16 @@ namespace Emby.Server.Implementations.Channels
} }
/// <inheritdoc /> /// <inheritdoc />
public QueryResult<Channel> GetChannelsInternal(ChannelQuery query) public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
{ {
var user = query.UserId.Equals(default) var user = query.UserId.Equals(default)
? null ? null
: _userManager.GetUserById(query.UserId); : _userManager.GetUserById(query.UserId);
var channels = GetAllChannels() var channels = await GetAllChannelEntitiesAsync()
.Select(GetChannelEntity)
.OrderBy(i => i.SortName) .OrderBy(i => i.SortName)
.ToList(); .ToListAsync()
.ConfigureAwait(false);
if (query.IsRecordingsFolder.HasValue) if (query.IsRecordingsFolder.HasValue)
{ {
@ -229,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
if (user is not null) if (user is not null)
{ {
var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
channels = channels.Where(i => channels = channels.Where(i =>
{ {
if (!i.IsVisible(user)) if (!i.IsVisible(user))
@ -238,7 +236,7 @@ namespace Emby.Server.Implementations.Channels
try try
{ {
return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture)); return GetChannelProvider(i).IsEnabledFor(userId);
} }
catch catch
{ {
@ -261,7 +259,7 @@ namespace Emby.Server.Implementations.Channels
{ {
foreach (var item in all) foreach (var item in all)
{ {
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult(); await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false);
} }
} }
@ -272,13 +270,13 @@ namespace Emby.Server.Implementations.Channels
} }
/// <inheritdoc /> /// <inheritdoc />
public QueryResult<BaseItemDto> GetChannels(ChannelQuery query) public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
{ {
var user = query.UserId.Equals(default) var user = query.UserId.Equals(default)
? null ? null
: _userManager.GetUserById(query.UserId); : _userManager.GetUserById(query.UserId);
var internalResult = GetChannelsInternal(query); var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions();
@ -330,9 +328,12 @@ namespace Emby.Server.Implementations.Channels
progress.Report(100); 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) private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
@ -404,7 +405,7 @@ namespace Emby.Server.Implementations.Channels
} }
else else
{ {
results = new List<MediaSourceInfo>(); results = Enumerable.Empty<MediaSourceInfo>();
} }
return results return results

View File

@ -112,7 +112,8 @@ namespace Emby.Server.Implementations.Collections
return Path.Combine(_appPaths.DataPath, "collections"); return Path.Combine(_appPaths.DataPath, "collections");
} }
private Task<Folder?> GetCollectionsFolder(bool createIfNeeded) /// <inheritdoc />
public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
{ {
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded); return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
} }
@ -206,8 +207,7 @@ namespace Emby.Server.Implementations.Collections
throw new ArgumentException("No collection exists with the supplied Id"); throw new ArgumentException("No collection exists with the supplied Id");
} }
var list = new List<LinkedChild>(); List<BaseItem>? itemList = null;
var itemList = new List<BaseItem>();
var linkedChildrenList = collection.GetLinkedChildren(); var linkedChildrenList = collection.GetLinkedChildren();
var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList(); var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList();
@ -223,18 +223,23 @@ namespace Emby.Server.Implementations.Collections
if (!currentLinkedChildrenIds.Contains(id)) if (!currentLinkedChildrenIds.Contains(id))
{ {
itemList.Add(item); (itemList ??= new()).Add(item);
list.Add(LinkedChild.Create(item));
linkedChildrenList.Add(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); 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.LinkedChildren = newChildren;
collection.UpdateRatingToItems(linkedChildrenList); collection.UpdateRatingToItems(linkedChildrenList);

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.Configuration
/// <summary> /// <summary>
/// Configuration updating event. /// Configuration updating event.
/// </summary> /// </summary>
public event EventHandler<GenericEventArgs<ServerConfiguration>> ConfigurationUpdating; public event EventHandler<GenericEventArgs<ServerConfiguration>>? ConfigurationUpdating;
/// <summary> /// <summary>
/// Gets the type of the configuration. /// Gets the type of the configuration.

View File

@ -11,14 +11,15 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Gets a new copy of the default configuration options. /// Gets a new copy of the default configuration options.
/// </summary> /// </summary>
public static Dictionary<string, string?> DefaultConfiguration => new Dictionary<string, string?> public static Dictionary<string, string?> DefaultConfiguration => new()
{ {
{ HostWebClientKey, bool.TrueString }, { HostWebClientKey, bool.TrueString },
{ DefaultRedirectKey, "web/index.html" }, { DefaultRedirectKey, "web/" },
{ FfmpegProbeSizeKey, "1G" }, { FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" }, { FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString }, { PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString } { BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" }
}; };
} }
} }

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty; using SQLitePCL.pretty;
@ -27,9 +26,19 @@ namespace Emby.Server.Implementations.Data
/// <summary> /// <summary>
/// Gets or sets the path to the DB file. /// Gets or sets the path to the DB file.
/// </summary> /// </summary>
/// <value>Path to the DB file.</value>
protected string DbFilePath { get; set; } 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> /// <summary>
/// Gets the logger. /// Gets the logger.
/// </summary> /// </summary>
@ -63,7 +72,7 @@ namespace Emby.Server.Implementations.Data
/// <summary> /// <summary>
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />. /// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
/// </summary> /// </summary>
protected virtual string LockingMode => "EXCLUSIVE"; protected virtual string LockingMode => "NORMAL";
/// <summary> /// <summary>
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />. /// 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> /// <summary>
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />. /// 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> /// </summary>
/// <value>The journal size limit.</value> /// <value>The journal size limit.</value>
protected virtual int? JournalSizeLimit => 0; protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
/// <summary> /// <summary>
/// Gets the page size. /// Gets the page size.
@ -88,7 +98,7 @@ namespace Emby.Server.Implementations.Data
/// </summary> /// </summary>
/// <value>The temp store mode.</value> /// <value>The temp store mode.</value>
/// <see cref="TempStoreMode"/> /// <see cref="TempStoreMode"/>
protected virtual TempStoreMode TempStore => TempStoreMode.Default; protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
/// <summary> /// <summary>
/// Gets the synchronous mode. /// Gets the synchronous mode.
@ -101,63 +111,106 @@ namespace Emby.Server.Implementations.Data
/// Gets or sets the write lock. /// Gets or sets the write lock.
/// </summary> /// </summary>
/// <value>The write lock.</value> /// <value>The write lock.</value>
protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1); protected ConnectionPool WriteConnections { get; set; }
/// <summary> /// <summary>
/// Gets or sets the write connection. /// Gets or sets the write connection.
/// </summary> /// </summary>
/// <value>The write connection.</value> /// <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) protected ManagedConnection GetConnection(bool readOnly = false)
{ => readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
WriteLock.Wait();
if (WriteConnection is not null)
{
return new ManagedConnection(WriteConnection, WriteLock);
}
WriteConnection = SQLite3.Open( protected SQLiteDatabaseConnection CreateWriteConnection()
{
var writeConnection = SQLite3.Open(
DbFilePath, DbFilePath,
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite, DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
null); null);
if (CacheSize.HasValue) if (CacheSize.HasValue)
{ {
WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
} }
if (!string.IsNullOrWhiteSpace(LockingMode)) if (!string.IsNullOrWhiteSpace(LockingMode))
{ {
WriteConnection.Execute("PRAGMA locking_mode=" + LockingMode); writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
} }
if (!string.IsNullOrWhiteSpace(JournalMode)) if (!string.IsNullOrWhiteSpace(JournalMode))
{ {
WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode); writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
} }
if (JournalSizeLimit.HasValue) if (JournalSizeLimit.HasValue)
{ {
WriteConnection.Execute("PRAGMA journal_size_limit=" + (int)JournalSizeLimit.Value); writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
} }
if (Synchronous.HasValue) if (Synchronous.HasValue)
{ {
WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
} }
if (PageSize.HasValue) 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. return writeConnection;
WriteConnection.Execute("VACUUM"); }
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) public IStatement PrepareStatement(ManagedConnection connection, string sql)
@ -166,18 +219,6 @@ namespace Emby.Server.Implementations.Data
public IStatement PrepareStatement(IDatabaseConnection connection, string sql) public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
=> connection.PrepareStatement(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) protected bool TableExists(ManagedConnection connection, string name)
{ {
return connection.RunInTransaction( return connection.RunInTransaction(
@ -252,22 +293,10 @@ namespace Emby.Server.Implementations.Data
if (dispose) if (dispose)
{ {
WriteLock.Wait(); WriteConnections.Dispose();
try ReadConnections.Dispose();
{
WriteConnection?.Dispose();
}
finally
{
WriteLock.Release();
}
WriteLock.Dispose();
} }
WriteConnection = null;
WriteLock = null;
_disposed = true; _disposed = true;
} }
} }

View File

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

View File

@ -2,23 +2,22 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using SQLitePCL.pretty; using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data namespace Emby.Server.Implementations.Data
{ {
public sealed class ManagedConnection : IDisposable public sealed class ManagedConnection : IDisposable
{ {
private readonly SemaphoreSlim _writeLock; private readonly ConnectionPool _pool;
private SQLiteDatabaseConnection? _db; private SQLiteDatabaseConnection _db;
private bool _disposed = false; private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock) public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
{ {
_db = db; _db = db;
_writeLock = writeLock; _pool = pool;
} }
public IStatement PrepareStatement(string sql) public IStatement PrepareStatement(string sql)
@ -73,9 +72,9 @@ namespace Emby.Server.Implementations.Data
return; return;
} }
_writeLock.Release(); _pool.Return(_db);
_db = null; // Don't dispose it _db = null!; // Don't dispose it
_disposed = true; _disposed = true;
} }
} }

View File

@ -25,6 +25,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
@ -34,6 +35,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty; using SQLitePCL.pretty;
@ -49,8 +51,8 @@ namespace Emby.Server.Implementations.Data
private const string SaveItemCommandText = private const string SaveItemCommandText =
@"replace into TypedBaseItems @"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) (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,@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 IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
@ -110,6 +112,7 @@ namespace Emby.Server.Implementations.Data
"PrimaryVersionId", "PrimaryVersionId",
"DateLastMediaAdded", "DateLastMediaAdded",
"Album", "Album",
"LUFS",
"CriticRating", "CriticRating",
"IsVirtualItem", "IsVirtualItem",
"SeriesName", "SeriesName",
@ -318,13 +321,15 @@ namespace Emby.Server.Implementations.Data
/// <param name="logger">Instance of the <see cref="ILogger{SqliteItemRepository}"/> interface.</param> /// <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="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> 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> /// <exception cref="ArgumentNullException">config is null.</exception>
public SqliteItemRepository( public SqliteItemRepository(
IServerConfigurationManager config, IServerConfigurationManager config,
IServerApplicationHost appHost, IServerApplicationHost appHost,
ILogger<SqliteItemRepository> logger, ILogger<SqliteItemRepository> logger,
ILocalizationManager localization, ILocalizationManager localization,
IImageProcessor imageProcessor) IImageProcessor imageProcessor,
IConfiguration configuration)
: base(logger) : base(logger)
{ {
_config = config; _config = config;
@ -336,10 +341,13 @@ namespace Emby.Server.Implementations.Data
_jsonOptions = JsonDefaults.Options; _jsonOptions = JsonDefaults.Options;
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
CacheSize = configuration.GetSqliteCacheSize();
ReadConnectionsCount = Environment.ProcessorCount * 2;
} }
/// <inheritdoc /> /// <inheritdoc />
protected override int? CacheSize => 20000; protected override int? CacheSize { get; }
/// <inheritdoc /> /// <inheritdoc />
protected override TempStoreMode TempStore => TempStoreMode.Memory; protected override TempStoreMode TempStore => TempStoreMode.Memory;
@ -347,10 +355,10 @@ namespace Emby.Server.Implementations.Data
/// <summary> /// <summary>
/// Opens the connection to the database. /// Opens the connection to the database.
/// </summary> /// </summary>
/// <param name="userDataRepo">The user data repository.</param> public override void Initialize()
/// <param name="userManager">The user manager.</param>
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
{ {
base.Initialize();
const string CreateMediaStreamsTableCommand 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))"; = "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", "PrimaryVersionId", "Text", existingColumnNames);
AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames);
AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
@ -551,8 +560,6 @@ namespace Emby.Server.Implementations.Data
connection.RunQueries(postQueries); connection.RunQueries(postQueries);
} }
userDataRepo.Initialize(userManager, WriteLock, WriteConnection);
} }
public void SaveImages(BaseItem item) public void SaveImages(BaseItem item)
@ -586,7 +593,7 @@ namespace Emby.Server.Implementations.Data
/// <exception cref="ArgumentNullException"> /// <exception cref="ArgumentNullException">
/// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>. /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
/// </exception> /// </exception>
public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken) public void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken)
{ {
ArgumentNullException.ThrowIfNull(items); ArgumentNullException.ThrowIfNull(items);
@ -594,9 +601,11 @@ namespace Emby.Server.Implementations.Data
CheckDisposed(); CheckDisposed();
var tuples = new List<(BaseItem, List<Guid>, BaseItem, string, List<string>)>(); var itemsLen = items.Count;
foreach (var item in items) 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 ? var ancestorIds = item.SupportsAncestors ?
item.GetAncestorIds().Distinct().ToList() : item.GetAncestorIds().Distinct().ToList() :
null; null;
@ -606,7 +615,7 @@ namespace Emby.Server.Implementations.Data
var userdataKey = item.GetUserDataKeys().FirstOrDefault(); var userdataKey = item.GetUserDataKeys().FirstOrDefault();
var inheritedTags = item.GetInheritedTags(); var inheritedTags = item.GetInheritedTags();
tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags)); tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags);
} }
using (var connection = GetConnection()) 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) 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[] using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
{ using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
SaveItemCommandText,
"delete from AncestorIds where ItemId=@ItemId"
});
using (var saveItemStatement = statements[0])
using (var deleteAncestorsStatement = statements[1])
{ {
var requiresReset = false; var requiresReset = false;
foreach (var tuple in tuples) foreach (var tuple in tuples)
@ -911,6 +914,7 @@ namespace Emby.Server.Implementations.Data
} }
saveItemStatement.TryBind("@Album", item.Album); saveItemStatement.TryBind("@Album", item.Album);
saveItemStatement.TryBind("@LUFS", item.LUFS);
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem); saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
if (item is IHasSeries hasSeriesName) if (item is IHasSeries hasSeriesName)
@ -1195,7 +1199,7 @@ namespace Emby.Server.Implementations.Data
Path = RestorePath(path.ToString()) 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.MinValue.Ticks
&& ticks <= DateTime.MaxValue.Ticks) && ticks <= DateTime.MaxValue.Ticks)
{ {
@ -1284,15 +1288,13 @@ namespace Emby.Server.Implementations.Data
CheckDisposed(); CheckDisposed();
using (var connection = GetConnection(true)) 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()) foreach (var row in statement.ExecuteQuery())
{ {
return GetItem(row, new InternalItemsQuery()); return GetItem(row, new InternalItemsQuery());
}
} }
} }
@ -1307,7 +1309,8 @@ namespace Emby.Server.Implementations.Data
{ {
return false; return false;
} }
else if (type == typeof(UserRootFolder))
if (type == typeof(UserRootFolder))
{ {
return false; return false;
} }
@ -1317,55 +1320,68 @@ namespace Emby.Server.Implementations.Data
{ {
return false; return false;
} }
else if (type == typeof(MusicArtist))
if (type == typeof(MusicArtist))
{ {
return false; return false;
} }
else if (type == typeof(Person))
if (type == typeof(Person))
{ {
return false; return false;
} }
else if (type == typeof(MusicGenre))
if (type == typeof(MusicGenre))
{ {
return false; return false;
} }
else if (type == typeof(Genre))
if (type == typeof(Genre))
{ {
return false; return false;
} }
else if (type == typeof(Studio))
if (type == typeof(Studio))
{ {
return false; return false;
} }
else if (type == typeof(PlaylistsFolder))
if (type == typeof(PlaylistsFolder))
{ {
return false; return false;
} }
else if (type == typeof(PhotoAlbum))
if (type == typeof(PhotoAlbum))
{ {
return false; return false;
} }
else if (type == typeof(Year))
if (type == typeof(Year))
{ {
return false; return false;
} }
else if (type == typeof(Book))
if (type == typeof(Book))
{ {
return false; return false;
} }
else if (type == typeof(LiveTvProgram))
if (type == typeof(LiveTvProgram))
{ {
return false; return false;
} }
else if (type == typeof(AudioBook))
if (type == typeof(AudioBook))
{ {
return false; return false;
} }
else if (type == typeof(Audio))
if (type == typeof(Audio))
{ {
return false; return false;
} }
else if (type == typeof(MusicAlbum))
if (type == typeof(MusicAlbum))
{ {
return false; return false;
} }
@ -1749,6 +1765,11 @@ namespace Emby.Server.Implementations.Data
item.Album = album; item.Album = album;
} }
if (reader.TryGetSingle(index++, out var lUFS))
{
item.LUFS = lUFS;
}
if (reader.TryGetSingle(index++, out var criticRating)) if (reader.TryGetSingle(index++, out var criticRating))
{ {
item.CriticRating = criticRating; item.CriticRating = criticRating;
@ -1956,22 +1977,19 @@ namespace Emby.Server.Implementations.Data
{ {
CheckDisposed(); CheckDisposed();
var chapters = new List<ChapterInfo>();
using (var connection = GetConnection(true)) 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); chapters.Add(GetChapter(row, item));
foreach (var row in statement.ExecuteQuery())
{
chapters.Add(GetChapter(row, item));
}
} }
return chapters;
} }
return chapters;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -1980,16 +1998,14 @@ namespace Emby.Server.Implementations.Data
CheckDisposed(); CheckDisposed();
using (var connection = GetConnection(true)) 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()) foreach (var row in statement.ExecuteQuery())
{ {
return GetChapter(row, item); return GetChapter(row, item);
}
} }
} }
@ -2376,7 +2392,7 @@ namespace Emby.Server.Implementations.Data
else else
{ {
builder.Append( builder.Append(
@"(SELECT CASE WHEN InheritedParentalRatingValue=0 @"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0
THEN 0 THEN 0
ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue)) ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue))
END)"); END)");
@ -2390,6 +2406,7 @@ namespace Emby.Server.Implementations.Data
// genres, tags, studios, person, year? // 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 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) if (item is MusicArtist)
{ {
@ -2841,13 +2858,10 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction( connection.RunInTransaction(
db => db =>
{ {
var itemQueryStatement = PrepareStatement(db, itemQuery);
var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery);
if (!isReturningZeroItems) if (!isReturningZeroItems)
{ {
using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery")) using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
using (var statement = itemQueryStatement) using (var statement = PrepareStatement(db, itemQuery))
{ {
if (EnableJoinUserData(query)) if (EnableJoinUserData(query))
{ {
@ -2882,7 +2896,7 @@ namespace Emby.Server.Implementations.Data
if (query.EnableTotalRecordCount) if (query.EnableTotalRecordCount)
{ {
using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount")) using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
using (var statement = totalRecordCountQueryStatement) using (var statement = PrepareStatement(db, totalRecordCountQuery))
{ {
if (EnableJoinUserData(query)) if (EnableJoinUserData(query))
{ {
@ -3202,7 +3216,8 @@ namespace Emby.Server.Implementations.Data
return IsAlphaNumeric(value); 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) if (query.IsResumable ?? false)
{ {
@ -3677,7 +3692,6 @@ namespace Emby.Server.Implementations.Data
if (statement is not null) if (statement is not null)
{ {
nameContains = FixUnicodeChars(nameContains); nameContains = FixUnicodeChars(nameContains);
statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%"); statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%");
} }
} }
@ -3803,13 +3817,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.ArtistIds) foreach (var artistId in query.ArtistIds)
{ {
var paramName = "@ArtistIds" + index; var paramName = "@ArtistIds" + index;
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); 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++; index++;
} }
@ -3824,13 +3833,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.AlbumArtistIds) foreach (var artistId in query.AlbumArtistIds)
{ {
var paramName = "@ArtistIds" + index; var paramName = "@ArtistIds" + index;
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))"); 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++; index++;
} }
@ -3845,13 +3849,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.ContributingArtistIds) foreach (var artistId in query.ContributingArtistIds)
{ {
var paramName = "@ArtistIds" + index; 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))"); 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++; index++;
} }
@ -3866,13 +3865,8 @@ namespace Emby.Server.Implementations.Data
foreach (var albumId in query.AlbumIds) foreach (var albumId in query.AlbumIds)
{ {
var paramName = "@AlbumIds" + index; var paramName = "@AlbumIds" + index;
clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")"); 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++; index++;
} }
@ -3887,13 +3881,8 @@ namespace Emby.Server.Implementations.Data
foreach (var artistId in query.ExcludeArtistIds) foreach (var artistId in query.ExcludeArtistIds)
{ {
var paramName = "@ExcludeArtistId" + index; 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))"); 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++; index++;
} }
@ -3908,13 +3897,8 @@ namespace Emby.Server.Implementations.Data
foreach (var genreId in query.GenreIds) foreach (var genreId in query.GenreIds)
{ {
var paramName = "@GenreId" + index; var paramName = "@GenreId" + index;
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))"); 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++; index++;
} }
@ -3929,11 +3913,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in query.Genres) foreach (var item in query.Genres)
{ {
clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)"); 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++; index++;
} }
@ -3948,11 +3928,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in tags) foreach (var item in tags)
{ {
clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)"); 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++; index++;
} }
@ -3967,11 +3943,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in excludeTags) foreach (var item in excludeTags)
{ {
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)"); 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++; index++;
} }
@ -3986,14 +3958,8 @@ namespace Emby.Server.Implementations.Data
foreach (var studioId in query.StudioIds) foreach (var studioId in query.StudioIds)
{ {
var paramName = "@StudioId" + index; var paramName = "@StudioId" + index;
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))"); clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
statement?.TryBind(paramName, studioId);
if (statement is not null)
{
statement.TryBind(paramName, studioId);
}
index++; index++;
} }
@ -4008,11 +3974,7 @@ namespace Emby.Server.Implementations.Data
foreach (var item in query.OfficialRatings) foreach (var item in query.OfficialRatings)
{ {
clauses.Add("OfficialRating=@OfficialRating" + index); clauses.Add("OfficialRating=@OfficialRating" + index);
if (statement is not null) statement?.TryBind("@OfficialRating" + index, item);
{
statement.TryBind("@OfficialRating" + index, item);
}
index++; index++;
} }
@ -4020,34 +3982,96 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause); whereClauses.Add(clause);
} }
if (query.MinParentalRating.HasValue) var ratingClauseBuilder = new StringBuilder("(");
if (query.HasParentalRating ?? false)
{ {
whereClauses.Add("InheritedParentalRatingValue>=@MinParentalRating"); ratingClauseBuilder.Append("InheritedParentalRatingValue not null");
if (statement is 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);
} }
} }
else if (query.BlockUnratedItems.Length > 0)
if (query.MaxParentalRating.HasValue)
{ {
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) 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");
} }
} }
else if (query.MinParentalRating.HasValue)
if (query.HasParentalRating.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"); ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
} statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
else
{
whereClauses.Add("InheritedParentalRatingValue = 0");
} }
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) if (query.HasOfficialRating.HasValue)
@ -4089,37 +4113,25 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage)) 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)"); 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)) 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)"); 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)) 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)"); 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)) 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)"); 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) if (query.HasSubtitles.HasValue)
@ -4169,15 +4181,11 @@ namespace Emby.Server.Implementations.Data
if (query.Years.Length == 1) if (query.Years.Length == 1)
{ {
whereClauses.Add("ProductionYear=@Years"); 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) else if (query.Years.Length > 1)
{ {
var val = string.Join(',', query.Years); var val = string.Join(',', query.Years);
whereClauses.Add("ProductionYear in (" + val + ")"); whereClauses.Add("ProductionYear in (" + val + ")");
} }
@ -4185,10 +4193,7 @@ namespace Emby.Server.Implementations.Data
if (isVirtualItem.HasValue) if (isVirtualItem.HasValue)
{ {
whereClauses.Add("IsVirtualItem=@IsVirtualItem"); whereClauses.Add("IsVirtualItem=@IsVirtualItem");
if (statement is not null) statement?.TryBind("@IsVirtualItem", isVirtualItem.Value);
{
statement.TryBind("@IsVirtualItem", isVirtualItem.Value);
}
} }
if (query.IsSpecialSeason.HasValue) if (query.IsSpecialSeason.HasValue)
@ -4219,31 +4224,22 @@ namespace Emby.Server.Implementations.Data
if (queryMediaTypes.Length == 1) if (queryMediaTypes.Length == 1)
{ {
whereClauses.Add("MediaType=@MediaTypes"); whereClauses.Add("MediaType=@MediaTypes");
if (statement is not null) statement?.TryBind("@MediaTypes", queryMediaTypes[0]);
{
statement.TryBind("@MediaTypes", queryMediaTypes[0]);
}
} }
else if (queryMediaTypes.Length > 1) else if (queryMediaTypes.Length > 1)
{ {
var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'")); var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
whereClauses.Add("MediaType in (" + val + ")"); whereClauses.Add("MediaType in (" + val + ")");
} }
if (query.ItemIds.Length > 0) if (query.ItemIds.Length > 0)
{ {
var includeIds = new List<string>(); var includeIds = new List<string>();
var index = 0; var index = 0;
foreach (var id in query.ItemIds) foreach (var id in query.ItemIds)
{ {
includeIds.Add("Guid = @IncludeId" + index); includeIds.Add("Guid = @IncludeId" + index);
if (statement is not null) statement?.TryBind("@IncludeId" + index, id);
{
statement.TryBind("@IncludeId" + index, id);
}
index++; index++;
} }
@ -4253,16 +4249,11 @@ namespace Emby.Server.Implementations.Data
if (query.ExcludeItemIds.Length > 0) if (query.ExcludeItemIds.Length > 0)
{ {
var excludeIds = new List<string>(); var excludeIds = new List<string>();
var index = 0; var index = 0;
foreach (var id in query.ExcludeItemIds) foreach (var id in query.ExcludeItemIds)
{ {
excludeIds.Add("Guid <> @ExcludeId" + index); excludeIds.Add("Guid <> @ExcludeId" + index);
if (statement is not null) statement?.TryBind("@ExcludeId" + index, id);
{
statement.TryBind("@ExcludeId" + index, id);
}
index++; index++;
} }
@ -4283,11 +4274,7 @@ namespace Emby.Server.Implementations.Data
var paramName = "@ExcludeProviderId" + index; var paramName = "@ExcludeProviderId" + index;
excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")"); 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++; index++;
break; 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 // 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 + ")"); // 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: // 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); hasProviderIds.Add("ProviderIds like " + paramName);
// this replaces the placeholder with a value, here: %key=val% // 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++; index++;
break; break;
@ -4407,11 +4390,7 @@ namespace Emby.Server.Implementations.Data
if (query.AncestorIds.Length == 1) if (query.AncestorIds.Length == 1)
{ {
whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)"); whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)");
statement?.TryBind("@AncestorId", query.AncestorIds[0]);
if (statement is not null)
{
statement.TryBind("@AncestorId", query.AncestorIds[0]);
}
} }
if (query.AncestorIds.Length > 1) if (query.AncestorIds.Length > 1)
@ -4424,39 +4403,13 @@ namespace Emby.Server.Implementations.Data
{ {
var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; 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)); 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)) if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey))
{ {
whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey"); whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey");
statement?.TryBind("@SeriesPresentationUniqueKey", query.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));
} }
if (query.ExcludeInheritedTags.Length > 0) if (query.ExcludeInheritedTags.Length > 0)
@ -4477,6 +4430,24 @@ namespace Emby.Server.Implementations.Data
} }
} }
if (query.IncludeInheritedTags.Length > 0)
{
var paramName = "@IncludeInheritedTags";
if (statement is null)
{
int index = 0;
string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
}
else
{
for (int index = 0; index < query.IncludeInheritedTags.Length; index++)
{
statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index]));
}
}
}
if (query.SeriesStatuses.Length > 0) if (query.SeriesStatuses.Length > 0)
{ {
var statuses = new List<string>(); var statuses = new List<string>();
@ -4587,6 +4558,7 @@ namespace Emby.Server.Implementations.Data
return whereClauses; return whereClauses;
} }
#nullable disable
/// <summary> /// <summary>
/// Formats a where clause for the specified provider. /// Formats a where clause for the specified provider.
@ -4793,22 +4765,20 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
commandText.Append(" LIMIT ").Append(query.Limit); commandText.Append(" LIMIT ").Append(query.Limit);
} }
var list = new List<string>();
using (var connection = GetConnection(true)) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString()))
{ {
var list = new List<string>(); // Run this again to bind the params
using (var statement = PrepareStatement(connection, commandText.ToString())) GetPeopleWhereClauses(query, statement);
foreach (var row in statement.ExecuteQuery())
{ {
// Run this again to bind the params list.Add(row.GetString(0));
GetPeopleWhereClauses(query, statement);
foreach (var row in statement.ExecuteQuery())
{
list.Add(row.GetString(0));
}
} }
return list;
} }
return list;
} }
public List<PersonInfo> GetPeople(InternalPeopleQuery query) public List<PersonInfo> GetPeople(InternalPeopleQuery query)
@ -4833,23 +4803,20 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
commandText += " LIMIT " + query.Limit; commandText += " LIMIT " + query.Limit;
} }
var list = new List<PersonInfo>();
using (var connection = GetConnection(true)) 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 list.Add(GetPerson(row));
GetPeopleWhereClauses(query, statement);
foreach (var row in statement.ExecuteQuery())
{
list.Add(GetPerson(row));
}
} }
return list;
} }
return list;
} }
private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement) private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement)
@ -5440,6 +5407,9 @@ AND Type = @InternalPersonType)");
list.AddRange(inheritedTags.Select(i => (6, i))); list.AddRange(inheritedTags.Select(i => (6, i)));
// Remove all invalid values.
list.RemoveAll(i => string.IsNullOrEmpty(i.Item2));
return list; return list;
} }
@ -5577,7 +5547,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@Name" + index, person.Name); statement.TryBind("@Name" + index, person.Name);
statement.TryBind("@Role" + index, person.Role); 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("@SortOrder" + index, person.SortOrder);
statement.TryBind("@ListOrder" + index, listIndex); statement.TryBind("@ListOrder" + index, listIndex);
@ -5606,9 +5576,10 @@ AND Type = @InternalPersonType)");
item.Role = role; 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)) if (reader.TryGetInt32(4, out var sortOrder))

View File

@ -7,7 +7,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
@ -18,33 +18,32 @@ namespace Emby.Server.Implementations.Data
{ {
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
{ {
private readonly IUserManager _userManager;
public SqliteUserDataRepository( public SqliteUserDataRepository(
ILogger<SqliteUserDataRepository> logger, ILogger<SqliteUserDataRepository> logger,
IApplicationPaths appPaths) IServerConfigurationManager config,
IUserManager userManager)
: base(logger) : base(logger)
{ {
DbFilePath = Path.Combine(appPaths.DataPath, "library.db"); _userManager = userManager;
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
} }
/// <summary> /// <summary>
/// Opens the connection to the database. /// Opens the connection to the database.
/// </summary> /// </summary>
/// <param name="userManager">The user manager.</param> public override void Initialize()
/// <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)
{ {
WriteLock.Dispose(); base.Initialize();
WriteLock = dbLock;
WriteConnection?.Dispose();
WriteConnection = dbConnection;
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
var userDatasTableExists = TableExists(connection, "UserDatas"); var userDatasTableExists = TableExists(connection, "UserDatas");
var userDataTableExists = TableExists(connection, "userdata"); var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : userManager.Users; var users = userDatasTableExists ? null : _userManager.Users;
connection.RunInTransaction( connection.RunInTransaction(
db => db =>
@ -371,20 +370,5 @@ namespace Emby.Server.Implementations.Data
return userData; 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
} }
} }

View File

@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
@ -83,22 +82,23 @@ namespace Emby.Server.Implementations.Dto
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{ {
var returnItems = new BaseItemDto[items.Count]; var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
var programTuples = new List<(BaseItem, BaseItemDto)>(); var returnItems = new BaseItemDto[accessibleItems.Count];
var channelTuples = new List<(BaseItemDto, LiveTvChannel)>(); 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); var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel) if (item is LiveTvChannel tvChannel)
{ {
channelTuples.Add((dto, tvChannel)); (channelTuples ??= new()).Add((dto, tvChannel));
} }
else if (item is LiveTvProgram) else if (item is LiveTvProgram)
{ {
programTuples.Add((item, dto)); (programTuples ??= new()).Add((item, dto));
} }
if (item is IItemByName byName) if (item is IItemByName byName)
@ -121,12 +121,12 @@ namespace Emby.Server.Implementations.Dto
returnItems[index] = dto; returnItems[index] = dto;
} }
if (programTuples.Count > 0) if (programTuples is not null)
{ {
LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult(); LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
} }
if (channelTuples.Count > 0) if (channelTuples is not null)
{ {
LivetvManager.AddChannelInfo(channelTuples, options, user); 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) var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
.ThenBy(i => .ThenBy(i =>
{ {
if (i.IsType(PersonType.Actor)) if (i.IsType(PersonKind.Actor))
{ {
return 0; return 0;
} }
if (i.IsType(PersonType.GuestStar)) if (i.IsType(PersonKind.GuestStar))
{ {
return 1; return 1;
} }
if (i.IsType(PersonType.Director)) if (i.IsType(PersonKind.Director))
{ {
return 2; return 2;
} }
if (i.IsType(PersonType.Writer)) if (i.IsType(PersonKind.Writer))
{ {
return 3; return 3;
} }
if (i.IsType(PersonType.Producer)) if (i.IsType(PersonKind.Producer))
{ {
return 4; return 4;
} }
if (i.IsType(PersonType.Composer)) if (i.IsType(PersonKind.Composer))
{ {
return 4; return 4;
} }
@ -571,9 +571,7 @@ namespace Emby.Server.Implementations.Dto
return null; return null;
} }
}).Where(i => i is not null) }).Where(i => i is not null)
.Where(i => user is null ? .Where(i => user is null || i.IsVisible(user))
true :
i.IsVisible(user))
.DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase) .DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
@ -908,6 +906,7 @@ namespace Emby.Server.Implementations.Dto
// Add audio info // Add audio info
if (item is Audio audio) if (item is Audio audio)
{ {
dto.LUFS = audio.LUFS;
dto.Album = audio.Album; dto.Album = audio.Album;
if (audio.ExtraType.HasValue) if (audio.ExtraType.HasValue)
{ {

View File

@ -22,17 +22,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" /> <PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageReference Include="Jellyfin.XmlTv" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Mono.Nat" Version="3.0.4" /> <PackageReference Include="Mono.Nat" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> <PackageReference Include="prometheus-net.DotNetRuntime" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" /> <PackageReference Include="DotNet.Glob" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -53,13 +53,13 @@
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4"> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -26,12 +27,8 @@ namespace Emby.Server.Implementations.EntryPoints
{ {
public class LibraryChangedNotifier : IServerEntryPoint public class LibraryChangedNotifier : IServerEntryPoint
{ {
/// <summary>
/// The library update duration.
/// </summary>
private const int LibraryUpdateDuration = 30000;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly IProviderManager _providerManager; private readonly IProviderManager _providerManager;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
@ -51,12 +48,14 @@ namespace Emby.Server.Implementations.EntryPoints
public LibraryChangedNotifier( public LibraryChangedNotifier(
ILibraryManager libraryManager, ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
ISessionManager sessionManager, ISessionManager sessionManager,
IUserManager userManager, IUserManager userManager,
ILogger<LibraryChangedNotifier> logger, ILogger<LibraryChangedNotifier> logger,
IProviderManager providerManager) IProviderManager providerManager)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_configurationManager = configurationManager;
_sessionManager = sessionManager; _sessionManager = sessionManager;
_userManager = userManager; _userManager = userManager;
_logger = logger; _logger = logger;
@ -196,12 +195,12 @@ namespace Emby.Server.Implementations.EntryPoints
LibraryUpdateTimer = new Timer( LibraryUpdateTimer = new Timer(
LibraryUpdateTimerCallback, LibraryUpdateTimerCallback,
null, null,
LibraryUpdateDuration, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration),
Timeout.Infinite); Timeout.InfiniteTimeSpan);
} }
else else
{ {
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
} }
if (e.Item.GetParent() is Folder parent) if (e.Item.GetParent() is Folder parent)
@ -229,11 +228,11 @@ namespace Emby.Server.Implementations.EntryPoints
{ {
if (LibraryUpdateTimer is null) 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 else
{ {
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
} }
_itemsUpdated.Add(e.Item); _itemsUpdated.Add(e.Item);
@ -256,11 +255,11 @@ namespace Emby.Server.Implementations.EntryPoints
{ {
if (LibraryUpdateTimer is null) 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 else
{ {
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
} }
if (e.Parent is Folder parent) if (e.Parent is Folder parent)
@ -276,25 +275,31 @@ namespace Emby.Server.Implementations.EntryPoints
/// Libraries the update timer callback. /// Libraries the update timer callback.
/// </summary> /// </summary>
/// <param name="state">The state.</param> /// <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) lock (_libraryChangedSyncLock)
{ {
// Remove dupes in case some were saved multiple times // Remove dupes in case some were saved multiple times
var foldersAddedTo = _foldersAddedTo foldersAddedTo = _foldersAddedTo
.DistinctBy(x => x.Id) .DistinctBy(x => x.Id)
.ToList(); .ToList();
var foldersRemovedFrom = _foldersRemovedFrom foldersRemovedFrom = _foldersRemovedFrom
.DistinctBy(x => x.Id) .DistinctBy(x => x.Id)
.ToList(); .ToList();
var itemsUpdated = _itemsUpdated itemsUpdated = _itemsUpdated
.Where(i => !_itemsAdded.Contains(i)) .Where(i => !_itemsAdded.Contains(i))
.DistinctBy(x => x.Id) .DistinctBy(x => x.Id)
.ToList(); .ToList();
SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult(); itemsAdded = _itemsAdded.ToList();
itemsRemoved = _itemsRemoved.ToList();
if (LibraryUpdateTimer is not null) if (LibraryUpdateTimer is not null)
{ {
@ -308,6 +313,8 @@ namespace Emby.Server.Implementations.EntryPoints
_foldersAddedTo.Clear(); _foldersAddedTo.Clear();
_foldersRemovedFrom.Clear(); _foldersRemovedFrom.Clear();
} }
await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
} }
/// <summary> /// <summary>

View File

@ -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) lock (_syncLock)
{ {
// Remove dupes in case some were saved multiple times // Remove dupes in case some were saved multiple times
var changes = _changedItems.ToList(); changes = _changedItems.ToList();
_changedItems.Clear(); _changedItems.Clear();
SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
if (_updateTimer is not null) if (_updateTimer is not null)
{ {
_updateTimer.Dispose(); _updateTimer.Dispose();
_updateTimer = null; _updateTimer = null;
} }
} }
await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
} }
private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken) 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);
} }
} }

View File

@ -9,7 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net; using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -85,6 +85,18 @@ namespace Emby.Server.Implementations.HttpServer
/// <value>The state.</value> /// <value>The state.</value>
public WebSocketState State => _socket.State; public WebSocketState State => _socket.State;
/// <summary>
/// Sends a message asynchronously.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task SendAsync(WebSocketMessage message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
}
/// <summary> /// <summary>
/// Sends a message asynchronously. /// Sends a message asynchronously.
/// </summary> /// </summary>
@ -224,7 +236,7 @@ namespace Emby.Server.Implementations.HttpServer
{ {
LastKeepAliveDate = DateTime.UtcNow; LastKeepAliveDate = DateTime.UtcNow;
return SendAsync( return SendAsync(
new WebSocketMessage<string> new OutboundWebSocketMessage
{ {
MessageId = Guid.NewGuid(), MessageId = Guid.NewGuid(),
MessageType = SessionMessageType.KeepAlive MessageType = SessionMessageType.KeepAlive

View File

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

View File

@ -267,25 +267,6 @@ namespace Emby.Server.Implementations.IO
return result; 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> /// <summary>
/// Takes a filename and removes invalid characters. /// Takes a filename and removes invalid characters.
/// </summary> /// </summary>
@ -405,19 +386,18 @@ namespace Emby.Server.Implementations.IO
return; 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) if (isHidden)
{ {
File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden); File.SetAttributes(path, info.Attributes | FileAttributes.Hidden);
} }
else else
{ {
var attributes = File.GetAttributes(path); File.SetAttributes(path, info.Attributes & ~FileAttributes.Hidden);
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
File.SetAttributes(path, attributes);
} }
} }
} }
@ -430,19 +410,20 @@ namespace Emby.Server.Implementations.IO
return; return;
} }
var info = GetExtendedFileSystemInfo(path); var info = new FileInfo(path);
if (!info.Exists) if (!info.Exists)
{ {
return; return;
} }
if (info.IsReadOnly == readOnly && info.IsHidden == isHidden) if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
{ {
return; return;
} }
var attributes = File.GetAttributes(path); var attributes = info.Attributes;
if (readOnly) if (readOnly)
{ {
@ -450,7 +431,7 @@ namespace Emby.Server.Implementations.IO
} }
else else
{ {
attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly); attributes &= ~FileAttributes.ReadOnly;
} }
if (isHidden) if (isHidden)
@ -459,17 +440,12 @@ namespace Emby.Server.Implementations.IO
} }
else else
{ {
attributes = RemoveAttribute(attributes, FileAttributes.Hidden); attributes &= ~FileAttributes.Hidden;
} }
File.SetAttributes(path, attributes); File.SetAttributes(path, attributes);
} }
private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
{
return attributes & ~attributesToRemove;
}
/// <summary> /// <summary>
/// Swaps the files. /// Swaps the files.
/// </summary> /// </summary>

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -113,6 +113,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
/// <param name="memoryCache">The memory cache.</param> /// <param name="memoryCache">The memory cache.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
public LibraryManager( public LibraryManager(
IServerApplicationHost appHost, IServerApplicationHost appHost,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
@ -128,7 +129,8 @@ namespace Emby.Server.Implementations.Library
IItemRepository itemRepository, IItemRepository itemRepository,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
IMemoryCache memoryCache, IMemoryCache memoryCache,
NamingOptions namingOptions) NamingOptions namingOptions,
IDirectoryService directoryService)
{ {
_appHost = appHost; _appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>(); _logger = loggerFactory.CreateLogger<LibraryManager>();
@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.Library
_memoryCache = memoryCache; _memoryCache = memoryCache;
_namingOptions = namingOptions; _namingOptions = namingOptions;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions); _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated; _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@ -356,8 +358,8 @@ namespace Emby.Server.Implementations.Library
} }
var children = item.IsFolder var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false).ToList() ? ((Folder)item).GetRecursiveChildren(false)
: new List<BaseItem>(); : Enumerable.Empty<BaseItem>();
foreach (var metadataPath in GetMetadataPaths(item, children)) foreach (var metadataPath in GetMetadataPaths(item, children))
{ {
@ -537,7 +539,7 @@ namespace Emby.Server.Implementations.Library
collectionType = GetContentTypeOverride(fullPath, true); collectionType = GetContentTypeOverride(fullPath, true);
} }
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService) var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
{ {
Parent = parent, Parent = parent,
FileInfo = fileInfo, FileInfo = fileInfo,
@ -1253,7 +1255,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId); var parent = GetItemById(query.ParentId);
if (parent is not null) 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); 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) public List<BaseItem> GetItemList(InternalItemsQuery query)
@ -1277,7 +1286,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId); var parent = GetItemById(query.ParentId);
if (parent is not null) 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); var parent = GetItemById(query.ParentId);
if (parent is not null) 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)); _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)) 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(); 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"); _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) catch (Exception ex)
{ {
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path); _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
size = new ImageDimensions(0, 0); size = default;
image.Width = 0; image.Width = 0;
image.Height = 0; image.Height = 0;
} }
@ -2741,9 +2756,7 @@ namespace Emby.Server.Implementations.Library
} }
}) })
.Where(i => i is not null) .Where(i => i is not null)
.Where(i => query.User is null ? .Where(i => query.User is null || i.IsVisible(query.User))
true :
i.IsVisible(query.User))
.ToList(); .ToList();
} }
@ -2876,7 +2889,7 @@ namespace Emby.Server.Implementations.Library
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken) private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{ {
var personsToSave = new List<BaseItem>(); List<BaseItem> personsToSave = null;
foreach (var person in people) foreach (var person in people)
{ {
@ -2918,12 +2931,12 @@ namespace Emby.Server.Implementations.Library
if (saveEntity) if (saveEntity)
{ {
personsToSave.Add(personEntity); (personsToSave ??= new()).Add(personEntity);
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
} }
} }
if (personsToSave.Count > 0) if (personsToSave is not null)
{ {
CreateItems(personsToSave, null, CancellationToken.None); CreateItems(personsToSave, null, CancellationToken.None);
} }
@ -3085,22 +3098,19 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
} }
var removeList = new List<NameValuePair>(); List<NameValuePair> removeList = null;
foreach (var contentType in _configurationManager.Configuration.ContentTypes) foreach (var contentType in _configurationManager.Configuration.ContentTypes)
{ {
if (string.IsNullOrWhiteSpace(contentType.Name)) if (string.IsNullOrWhiteSpace(contentType.Name)
{ || _fileSystem.AreEqual(path, contentType.Name)
removeList.Add(contentType);
}
else if (_fileSystem.AreEqual(path, contentType.Name)
|| _fileSystem.ContainsSubPath(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 _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
.Except(removeList) .Except(removeList)

View File

@ -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 file is strm or main media stream is missing, force a metadata refresh with remote probing
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
&& (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase) && (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
|| (item.MediaType == MediaType.Video && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Video)) || (item.MediaType == MediaType.Video && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Video))
|| (item.MediaType == MediaType.Audio && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio)))) || (item.MediaType == MediaType.Audio && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Audio))))
{ {
await item.RefreshMetadata( await item.RefreshMetadata(
new MetadataRefreshOptions(_directoryService) new MetadataRefreshOptions(_directoryService)

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO;
using MediaBrowser.Common.Providers; using MediaBrowser.Common.Providers;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
@ -86,24 +87,8 @@ namespace Emby.Server.Implementations.Library
return false; return false;
} }
char oldDirectorySeparatorChar; subPath = subPath.NormalizePath(out var newDirectorySeparatorChar);
char newDirectorySeparatorChar; path = path.NormalizePath(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);
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results // 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 // when the sub path matches a similar but in-complete subpath
@ -127,5 +112,82 @@ namespace Emby.Server.Implementations.Library
return true; 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);
}
} }
} }

View File

@ -158,7 +158,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName) private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
{ {
var files = new List<FileSystemMetadata>(); var files = new List<FileSystemMetadata>();
var items = new List<BaseItem>();
var leftOver = new List<FileSystemMetadata>(); var leftOver = new List<FileSystemMetadata>();
// Loop through each child file/folder and see if we find a video // 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 var result = new MultiItemResolverResult
{ {
ExtraFiles = leftOver, ExtraFiles = leftOver,
Items = items Items = new List<BaseItem>()
}; };
var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent); var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent);
@ -193,7 +192,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue; 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; continue;
} }

View File

@ -25,16 +25,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{ {
private readonly ILogger<MusicAlbumResolver> _logger; private readonly ILogger<MusicAlbumResolver> _logger;
private readonly NamingOptions _namingOptions; private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class. /// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</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; _logger = logger;
_namingOptions = namingOptions; _namingOptions = namingOptions;
_directoryService = directoryService;
} }
/// <summary> /// <summary>
@ -109,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
} }
// If args contains music it's a music album // If args contains music it's a music album
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService)) if (ContainsMusic(args.FileSystemChildren, true, _directoryService))
{ {
return true; return true;
} }

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Emby.Naming.Common; using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -18,19 +19,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public class MusicArtistResolver : ItemResolver<MusicArtist> public class MusicArtistResolver : ItemResolver<MusicArtist>
{ {
private readonly ILogger<MusicAlbumResolver> _logger; private readonly ILogger<MusicAlbumResolver> _logger;
private NamingOptions _namingOptions; private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class. /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
/// </summary> /// </summary>
/// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param> /// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
/// <param name="namingOptions">The <see cref="NamingOptions"/>.</param> /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
/// <param name="directoryService">The directory service.</param>
public MusicArtistResolver( public MusicArtistResolver(
ILogger<MusicAlbumResolver> logger, ILogger<MusicAlbumResolver> logger,
NamingOptions namingOptions) NamingOptions namingOptions,
IDirectoryService directoryService)
{ {
_logger = logger; _logger = logger;
_namingOptions = namingOptions; _namingOptions = namingOptions;
_directoryService = directoryService;
} }
/// <summary> /// <summary>
@ -78,9 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return null; return null;
} }
var directoryService = args.DirectoryService; var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
var directories = args.FileSystemChildren.Where(i => i.IsDirectory); 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 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 // Stop once we see a music album
state.Stop(); state.Stop();

View File

@ -25,14 +25,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions) protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
{ {
_logger = logger; _logger = logger;
NamingOptions = namingOptions; NamingOptions = namingOptions;
DirectoryService = directoryService;
} }
protected NamingOptions NamingOptions { get; } protected NamingOptions NamingOptions { get; }
protected IDirectoryService DirectoryService { get; }
/// <summary> /// <summary>
/// Resolves the specified args. /// Resolves the specified args.
/// </summary> /// </summary>
@ -65,13 +68,26 @@ namespace Emby.Server.Implementations.Library.Resolvers
var filename = child.Name; var filename = child.Name;
if (child.IsDirectory) 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)) else if (IsDvdFile(filename))

View File

@ -4,6 +4,8 @@ using System.IO;
using Emby.Naming.Common; using Emby.Naming.Common;
using Emby.Naming.Video; using Emby.Naming.Video;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -14,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary> /// <summary>
/// Resolves a Path into a Video or Video subclass. /// Resolves a Path into a Video or Video subclass.
/// </summary> /// </summary>
internal class ExtraResolver internal class ExtraResolver : BaseVideoResolver<Video>
{ {
private readonly NamingOptions _namingOptions; private readonly NamingOptions _namingOptions;
private readonly IItemResolver[] _trailerResolvers; private readonly IItemResolver[] _trailerResolvers;
@ -25,11 +27,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</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; _namingOptions = namingOptions;
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) }; _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
_videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) }; _videoResolvers = new IItemResolver[] { this };
}
protected override Video Resolve(ItemResolveArgs args)
{
return ResolveVideo<Video>(args, true);
} }
/// <summary> /// <summary>

View File

@ -2,6 +2,7 @@
using Emby.Naming.Common; using Emby.Naming.Common;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers namespace Emby.Server.Implementations.Library.Resolvers
@ -18,8 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions) /// <param name="directoryService">The directory service.</param>
: base(logger, namingOptions) public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
: base(logger, namingOptions, directoryService)
{ {
} }
} }

View File

@ -43,8 +43,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions) /// <param name="directoryService">The directory service.</param>
: base(logger, namingOptions) public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
: base(logger, namingOptions, directoryService)
{ {
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
} }
@ -97,12 +98,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) 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)) 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)) if (string.IsNullOrEmpty(collectionType))
@ -118,12 +119,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null; 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)) 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 // ignore extras
@ -313,13 +314,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result; return result;
} }
private static bool IsIgnored(string filename) private static bool IsIgnored(ReadOnlySpan<char> filename)
{ => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Ignore samples
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
return m.Success;
}
private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file) private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
{ {

View File

@ -1,7 +1,5 @@
#nullable disable #nullable disable
#pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -12,15 +10,20 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers namespace Emby.Server.Implementations.Library.Resolvers
{ {
/// <summary>
/// Class PhotoResolver.
/// </summary>
public class PhotoResolver : ItemResolver<Photo> public class PhotoResolver : ItemResolver<Photo>
{ {
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions; private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ {
@ -35,10 +38,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
"default" "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; _imageProcessor = imageProcessor;
_namingOptions = namingOptions; _namingOptions = namingOptions;
_directoryService = directoryService;
} }
/// <summary> /// <summary>
@ -61,7 +71,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
var filename = Path.GetFileNameWithoutExtension(args.Path); var filename = Path.GetFileNameWithoutExtension(args.Path);
// Make sure the image doesn't belong to a video file // 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) foreach (var file in files)
{ {

View File

@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{ {
if (args.IsDirectory) 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)); var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
if (string.IsNullOrEmpty(filename)) if (string.IsNullOrEmpty(filename))
{ {
@ -42,7 +42,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
return new Playlist return new Playlist
{ {
Path = args.Path, 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 return new Playlist
{ {
Path = args.Path, Path = args.Path,
Name = filename Name = filename,
OpenAccess = true
}; };
} }
} }
@ -70,7 +72,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
Path = args.Path, Path = args.Path,
Name = Path.GetFileNameWithoutExtension(args.Path), Name = Path.GetFileNameWithoutExtension(args.Path),
IsInMixedFolder = true, IsInMixedFolder = true,
PlaylistMediaType = MediaType.Audio PlaylistMediaType = MediaType.Audio,
OpenAccess = true
}; };
} }
} }

View File

@ -5,6 +5,7 @@ using System.Linq;
using Emby.Naming.Common; using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -20,8 +21,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) /// <param name="directoryService">The directory service.</param>
: base(logger, namingOptions) public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
: base(logger, namingOptions, directoryService)
{ {
} }

View File

@ -81,14 +81,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
if (season.IndexNumber.HasValue) if (season.IndexNumber.HasValue)
{ {
var seasonNumber = season.IndexNumber.Value; var seasonNumber = season.IndexNumber.Value;
if (string.IsNullOrEmpty(season.Name))
season.Name = seasonNumber == 0 ? {
args.LibraryOptions.SeasonZeroDisplayName : var seasonNames = series.SeasonNames;
string.Format( if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
CultureInfo.InvariantCulture, {
_localization.GetLocalizedString("NameSeasonNumber"), season.Name = seasonName;
seasonNumber, }
args.LibraryOptions.PreferredMetadataLanguage); else
{
season.Name = seasonNumber == 0 ?
args.LibraryOptions.SeasonZeroDisplayName :
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameSeasonNumber"),
seasonNumber,
args.LibraryOptions.PreferredMetadataLanguage);
}
}
} }
return season; return season;

View File

@ -184,6 +184,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{ {
var justName = Path.GetFileName(path.AsSpan()); 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"); var tvdbId = justName.GetAttributeValue("tvdbid");
if (!string.IsNullOrEmpty(tvdbId)) if (!string.IsNullOrEmpty(tvdbId))
{ {

View File

@ -46,10 +46,9 @@ namespace Emby.Server.Implementations.Library
public Folder[] GetUserViews(UserViewQuery query) public Folder[] GetUserViews(UserViewQuery query)
{ {
var user = _userManager.GetUserById(query.UserId); var user = _userManager.GetUserById(query.UserId);
if (user is null) 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() var folders = _libraryManager.GetUserRootFolder()
@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.Library
.ToList(); .ToList();
var groupedFolders = new List<ICollectionFolder>(); var groupedFolders = new List<ICollectionFolder>();
var list = new List<Folder>(); var list = new List<Folder>();
foreach (var folder in folders) foreach (var folder in folders)
@ -66,6 +64,20 @@ namespace Emby.Server.Implementations.Library
var collectionFolder = folder as ICollectionFolder; var collectionFolder = folder as ICollectionFolder;
var folderViewType = collectionFolder?.CollectionType; 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)) if (UserView.IsUserSpecific(folder))
{ {
list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null)); list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null));
@ -111,10 +123,10 @@ namespace Emby.Server.Implementations.Library
if (query.IncludeExternalContent) if (query.IncludeExternalContent)
{ {
var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
{ {
UserId = query.UserId UserId = query.UserId
}); }).GetAwaiter().GetResult();
var channels = channelResult.Items; 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 sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews); var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
return list return list
.OrderBy(i => .OrderBy(i =>
{ {
var index = Array.IndexOf(orders, i.Id); var index = Array.IndexOf(orders, i.Id);
if (index == -1 if (index == -1
&& i is UserView view && i is UserView view
&& !view.DisplayParentId.Equals(default)) && !view.DisplayParentId.Equals(default))
@ -286,7 +296,7 @@ namespace Emby.Server.Implementations.Library
if (parents.Count == 0) if (parents.Count == 0)
{ {
return new List<BaseItem>(); return Array.Empty<BaseItem>();
} }
if (includeItemTypes.Length == 0) if (includeItemTypes.Length == 0)

View File

@ -627,10 +627,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_timerProvider.Update(existingTimer); _timerProvider.Update(existingTimer);
return Task.FromResult(existingTimer.Id); 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); 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.WriteStartDocumentAsync(true).ConfigureAwait(false);
await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false); await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
string id; if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var id))
if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out id))
{ {
await writer.WriteElementStringAsync(null, "id", null, id).ConfigureAwait(false); 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 people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
var directors = people var directors = people
.Where(i => IsPersonType(i, PersonType.Director)) .Where(i => i.IsType(PersonKind.Director))
.Select(i => i.Name) .Select(i => i.Name)
.ToList(); .ToList();
@ -2042,7 +2039,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
} }
var writers = people var writers = people
.Where(i => IsPersonType(i, PersonType.Writer)) .Where(i => i.IsType(PersonKind.Writer))
.Select(i => i.Name) .Select(i => i.Name)
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(); .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) private LiveTvProgram GetProgramInfoFromCache(string programId)
{ {
var query = new InternalItemsQuery var query = new InternalItemsQuery

View File

@ -415,14 +415,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{ {
return null; return null;
} }
else if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
{ {
return uri; return uri;
} }
else
{ return apiUrl + "/image/" + uri + "?token=" + token;
return apiUrl + "/image/" + uri + "?token=" + token;
}
} }
private static double GetAspectRatio(ImageDataDto i) private static double GetAspectRatio(ImageDataDto i)
@ -463,10 +462,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
} }
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13)); StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
foreach (ReadOnlySpan<char> i in programIds) foreach (var i in programIds)
{ {
str.Append('"') str.Append('"')
.Append(i.Slice(0, 10)) .Append(i[..10])
.Append("\","); .Append("\",");
} }
@ -570,15 +569,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_tokens.TryAdd(username, savedToken); _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 return savedToken.Name;
if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks)
{
return savedToken.Name;
}
} }
} }

View File

@ -137,32 +137,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info) private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info)
{ {
string episodeTitle = program.Episode?.Title; string episodeTitle = program.Episode.Title;
var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList();
var programInfo = new ProgramInfo var programInfo = new ProgramInfo
{ {
ChannelId = program.ChannelId, ChannelId = program.ChannelId,
EndDate = program.EndDate.UtcDateTime, EndDate = program.EndDate.UtcDateTime,
EpisodeNumber = program.Episode?.Episode, EpisodeNumber = program.Episode.Episode,
EpisodeTitle = episodeTitle, EpisodeTitle = episodeTitle,
Genres = program.Categories, Genres = programCategories,
StartDate = program.StartDate.UtcDateTime, StartDate = program.StartDate.UtcDateTime,
Name = program.Title, Name = program.Title,
Overview = program.Description, Overview = program.Description,
ProductionYear = program.CopyrightDate?.Year, ProductionYear = program.CopyrightDate?.Year,
SeasonNumber = program.Episode?.Series, SeasonNumber = program.Episode.Series,
IsSeries = program.Episode is not null, IsSeries = program.Episode.Series is not null,
IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsRepeat = program.IsPreviouslyShown && !program.IsNew,
IsPremiere = program.Premiere is not null, IsPremiere = program.Premiere is not null,
IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source, ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
HasImage = !string.IsNullOrEmpty(program.Icon?.Source), HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value, OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
CommunityRating = program.StarRating, CommunityRating = program.StarRating,
SeriesId = program.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
}; };
if (string.IsNullOrWhiteSpace(program.ProgramId)) if (string.IsNullOrWhiteSpace(program.ProgramId))
@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{ {
Id = c.Id, Id = c.Id,
Name = c.DisplayName, Name = c.DisplayName,
ImageUrl = string.IsNullOrEmpty(c.Icon.Source) ? null : c.Icon.Source, ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source,
Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
}).ToList(); }).ToList();
} }

View File

@ -1312,20 +1312,19 @@ namespace Emby.Server.Implementations.LiveTv
return 7; 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) if (user is null)
{ {
return new QueryResult<BaseItem>(); return new QueryResult<BaseItem>();
} }
var folderIds = GetRecordingFolders(user, true) var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false);
.Select(i => i.Id) var folderIds = Array.ConvertAll(folders, x => x.Id);
.ToList();
var excludeItemTypes = new List<BaseItemKind>(); var excludeItemTypes = new List<BaseItemKind>();
if (folderIds.Count == 0) if (folderIds.Length == 0)
{ {
return new QueryResult<BaseItem>(); return new QueryResult<BaseItem>();
} }
@ -1392,7 +1391,7 @@ namespace Emby.Server.Implementations.LiveTv
{ {
MediaTypes = new[] { MediaType.Video }, MediaTypes = new[] { MediaType.Video },
Recursive = true, Recursive = true,
AncestorIds = folderIds.ToArray(), AncestorIds = folderIds,
IsFolder = false, IsFolder = false,
IsVirtualItem = false, IsVirtualItem = false,
Limit = limit, 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) var user = query.UserId.Equals(default)
? null ? null
@ -1536,7 +1535,7 @@ namespace Emby.Server.Implementations.LiveTv
RemoveFields(options); 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); var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user);
@ -2379,12 +2378,11 @@ namespace Emby.Server.Implementations.LiveTv
return _tvDtoService.GetInternalProgramId(externalId); return _tvDtoService.GetInternalProgramId(externalId);
} }
public List<BaseItem> GetRecordingFolders(User user) /// <inheritdoc />
{ public Task<BaseItem[]> GetRecordingFoldersAsync(User user)
return GetRecordingFolders(user, false); => 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() var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
.SelectMany(i => i.Locations) .SelectMany(i => i.Locations)
@ -2396,14 +2394,16 @@ namespace Emby.Server.Implementations.LiveTv
.OrderBy(i => i.SortName) .OrderBy(i => i.SortName)
.ToList(); .ToList();
folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery
{ {
UserId = user.Id, UserId = user.Id,
IsRecordingsFolder = true, IsRecordingsFolder = true,
RefreshLatestChannelItems = refreshChannels RefreshLatestChannelItems = refreshChannels
}).Items); }).ConfigureAwait(false);
return folders.Cast<BaseItem>().ToList(); folders.AddRange(channels.Items);
return folders.Cast<BaseItem>().ToArray();
} }
} }
} }

View File

@ -14,6 +14,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -58,7 +59,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_socketFactory = socketFactory; _socketFactory = socketFactory;
_streamHelper = streamHelper; _streamHelper = streamHelper;
_jsonOptions = JsonDefaults.Options; _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
_jsonOptions.Converters.Add(new JsonBoolNumberConverter());
} }
public string Name => "HD Homerun"; public string Name => "HD Homerun";

View File

@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
{ {
using var client = new TcpClient(); 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(); using var stream = client.GetStream();
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);

View File

@ -13,8 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public LegacyHdHomerunChannelCommands(string url) public LegacyHdHomerunChannelCommands(string url)
{ {
// parse url for channel and program // parse url for channel and program
var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)"); var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)");
var match = regExp.Match(url);
if (match.Success) if (match.Success)
{ {
_channel = match.Groups[1].Value; _channel = match.Groups[1].Value;

View File

@ -30,12 +30,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{ {
private static readonly string[] _disallowedSharedStreamExtensions = private static readonly string[] _disallowedMimeTypes =
{ {
".mkv", "video/x-matroska",
".mp4", "video/mp4",
".m3u8", "application/vnd.apple.mpegurl",
".mpd" "application/mpegurl",
"application/x-mpegurl",
"video/vnd.mpeg.dash.mpd"
}; };
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
@ -118,9 +120,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) 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); return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
} }

View File

@ -122,9 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var attributes = ParseExtInf(extInf, out string remaining); var attributes = ParseExtInf(extInf, out string remaining);
extInf = 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)) 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; var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
string numberString = null; 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 (!IsValidChannelNumber(numberString))
{ {
if (attributes.TryGetValue("tvg-id", out attributeValue)) 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; 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[] { ' ', '.' }); 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(); numberString = numberPart.ToString();
} }
@ -255,19 +254,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private static bool IsValidChannelNumber(string numberString) private static bool IsValidChannelNumber(string numberString)
{ {
if (string.IsNullOrWhiteSpace(numberString) || if (string.IsNullOrWhiteSpace(numberString)
string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) || || string.Equals(numberString, "-1", StringComparison.Ordinal)
string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase)) || string.Equals(numberString, "0", StringComparison.Ordinal))
{ {
return false; return false;
} }
if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) return double.TryParse(numberString, CultureInfo.InvariantCulture, out _);
{
return false;
}
return true;
} }
private static string GetChannelName(string extInf, Dictionary<string, string> attributes) 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[] { ' ', '.' }); 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(); // channel.Number = number.ToString();
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); 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 dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase); var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
var matches = reg.Matches(line);
remaining = line; remaining = line;

View File

@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_appHost = appHost; _appHost = appHost;
OriginalStreamId = originalStreamId; OriginalStreamId = originalStreamId;
EnableStreamSharing = true;
} }
public override async Task Open(CancellationToken openCancellationToken) public override async Task Open(CancellationToken openCancellationToken)
@ -59,39 +58,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false); .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); var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token); _ = 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.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http; 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); var res = await taskCompletionSource.Task.ConfigureAwait(false);
if (!res) if (!res)
{ {
@ -108,15 +81,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try try
{ {
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath); Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
using var message = response; 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 using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await StreamHelper.CopyToAsync( await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
stream, await StreamHelper.CopyToAsync(
fileStream, stream,
IODefaults.CopyToBufferSize, fileStream,
() => Resolve(openTaskCompletionSource), IODefaults.CopyToBufferSize,
cancellationToken).ConfigureAwait(false); () => Resolve(openTaskCompletionSource),
cancellationToken).ConfigureAwait(false);
}
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
{ {

View File

@ -1,4 +1,127 @@
{ {
"Sync": "Сінхранізацыя", "Sync": "Сінхранізаваць",
"Playlists": "Плэйліст" "Playlists": "Плэйлісты",
"Latest": "Апошні",
"LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне прыкладання ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны",
"UserCreatedWithName": "Карыстальнік {0} быў створаны",
"Albums": "Альбомы",
"Application": "Прыкладанне",
"AuthenticationSucceededWithUserName": "{0} паспяхова аўтэнтыфікаваны",
"Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі",
"Default": "Па змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі",
"Favorites": "Абранае",
"External": "Знешні",
"Genres": "Жанры",
"HeaderContinueWatching": "Працягнуць прагляд",
"HeaderFavoriteAlbums": "Абраныя альбомы",
"HeaderFavoriteEpisodes": "Абраныя серыі",
"HeaderFavoriteShows": "Абраныя шоу",
"HeaderFavoriteSongs": "Абраныя песні",
"HeaderLiveTV": "Прамы эфір",
"HeaderAlbumArtists": "Выканаўцы альбома",
"LabelRunningTimeValue": "Працягласць: {0}",
"HomeVideos": "Хатнія відэа",
"ItemRemovedWithName": "{0} быў выдалены з бібліятэкі",
"MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да {0}",
"Movies": "Фільмы",
"Music": "Музыка",
"MusicVideos": "Музычныя кліпы",
"NameInstallFailed": "Устаноўка {0} не атрымалася",
"NameSeasonNumber": "Сезон {0}",
"NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне прыкладання",
"NotificationOptionPluginInstalled": "Плагін усталяваны",
"NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна усталявана",
"NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера",
"Photos": "Фатаграфіі",
"Plugin": "Плагін",
"PluginUninstalledWithName": "{0} быў выдалены",
"PluginUpdatedWithName": "{0} быў абноўлены",
"ProviderValue": "Пастаўшчык: {0}",
"Songs": "Песні",
"System": "Сістэма",
"User": "Карыстальнік",
"UserDeletedWithName": "Карыстальнік {0} быў выдалены",
"UserDownloadingItemWithValues": "{0} спампоўваецца {1}",
"TaskOptimizeDatabase": "Аптымізаваць базу дадзеных",
"Artists": "Выканаўцы",
"UserOfflineFromDevice": "{0} адключыўся ад {1}",
"UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
"TaskCleanActivityLogDescription": "Выдаляе старэйшыя за зададзены ўзрост запісы ў журнале актыўнасці.",
"TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.",
"TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.",
"TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія настроены на аўтаматычнае абнаўленне.",
"TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.",
"TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субтытры на аснове канфігурацыі метададзеных.",
"TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых змяненняў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць прадукцыйнасць.",
"TaskKeyframeExtractor": "Экстрактар ключавых кадраў",
"TasksApplicationCategory": "Прыкладанне",
"AppDeviceValues": "Прыкладанне: {0}, Прылада: {1}",
"Books": "Кнігі",
"CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}",
"DeviceOfflineWithName": "{0} адключыўся",
"DeviceOnlineWithName": "{0} падлучаны",
"Forced": "Прымусова",
"HeaderRecordingGroups": "Групы запісаў",
"HeaderNextUp": "Наступнае",
"HeaderFavoriteArtists": "Абраныя выканаўцы",
"HearingImpaired": "Са слабым слыхам",
"Inherit": "Атрымаць у спадчыну",
"MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера {0} абноўлена",
"MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена",
"MixedContent": "Змешаны змест",
"NameSeasonUnknown": "Невядомы сезон",
"NotificationOptionInstallationFailed": "Збой усталёўкі",
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена",
"NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося",
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
"NotificationOptionPluginError": "Збой плагіна",
"NotificationOptionPluginUninstalled": "Плагін выдалены",
"NotificationOptionTaskFailed": "Збой запланаванага задання",
"NotificationOptionUserLockedOut": "Карыстальнік заблакіраваны",
"NotificationOptionVideoPlayback": "Пачалося прайграванне відэа",
"NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена",
"ScheduledTaskFailedWithName": "{0} не атрымалася",
"ScheduledTaskStartedWithName": "{0} пачалося",
"ServerNameNeedsToBeRestarted": "{0} трэба перазапусціць",
"Shows": "Шоу",
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
"SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}",
"TvShows": "ТБ-шоу",
"Undefined": "Нявызначана",
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
"UserOnlineFromDevice": "{0} падключаны з {1}",
"UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}",
"UserStartedPlayingItemWithValues": "{0} грае {1} на {2}",
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
"ValueSpecialEpisodeName": "Спецэпізод - {0}",
"VersionNumber": "Версія {0}",
"TasksMaintenanceCategory": "Абслугоўванне",
"TasksLibraryCategory": "Медыятэка",
"TasksChannelsCategory": "Інтэрнэт-каналы",
"TaskCleanActivityLog": "Ачысціць журнал актыўнасці",
"TaskCleanCache": "Ачысціць кэш",
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
"TaskRefreshChapterImages": "Выняць выявы раздзелаў",
"TaskRefreshLibrary": "Сканіраваць медыятэку",
"TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
"TaskCleanLogs": "Ачысціць часопіс",
"TaskRefreshPeople": "Абнавіць людзей",
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
"TaskUpdatePlugins": "Абнавіць плагіны",
"TaskCleanTranscode": "Ачысціць каталог перакадзіравання",
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
"TaskRefreshChannels": "Абнавіць каналы",
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу."
} }

View File

@ -1,27 +1,27 @@
{ {
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে", "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে", "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
"Collections": "সংগ্রহ", "Collections": "সংগ্রহশালা",
"ChapterNameValue": "অধ্যায় {0}", "ChapterNameValue": "অধ্যায় {0}",
"Channels": "চ্যানেল", "Channels": "চ্যানেলসমূহ",
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে", "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "বই", "Books": "পুস্তকসমূহ",
"AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল", "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
"Artists": "শিল্পীরা", "Artists": "শিল্পীগণ",
"Application": "অ্যাপ্লিকেশন", "Application": "অ্যাপ্লিকেশন",
"Albums": "অ্যালবামগুলো", "Albums": "অ্যালবামসমূহ",
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো", "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন", "HeaderContinueWatching": "দেখতে থাকুন",
"HeaderAlbumArtists": "লবাম শিল্পীবৃন্দ", "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
"Genres": "শৈলী", "Genres": "শৈলীধারাসমূহ",
"Folders": "ফোল্ডারগুলো", "Folders": "ফোল্ডারসমূহ",
"Favorites": "পছন্দসমূহ", "Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে", "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}", "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
"VersionNumber": "সংস্করণ {0}", "VersionNumber": "সংস্করণ {0}",
"ValueSpecialEpisodeName": "বিশেষ - {0}", "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে", "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
"UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}", "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
"UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}", "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
@ -36,10 +36,10 @@
"User": "ব্যবহারকারী", "User": "ব্যবহারকারী",
"TvShows": "টিভি শোগুলো", "TvShows": "টিভি শোগুলো",
"System": "সিস্টেম", "System": "সিস্টেম",
"Sync": "সিংক", "Sync": "সমলয় স্থাপন",
"SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ", "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।", "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
"Songs": "গানগুলো", "Songs": "সঙ্গীতসমূহ",
"Shows": "টিভি পর্ব", "Shows": "টিভি পর্ব",
"ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন", "ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন",
"ScheduledTaskStartedWithName": "{0} শুরু হয়েছে", "ScheduledTaskStartedWithName": "{0} শুরু হয়েছে",
@ -49,8 +49,8 @@
"PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে", "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
"PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে", "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
"Plugin": "প্লাগিন", "Plugin": "প্লাগিন",
"Playlists": "প্লেলিস্ট", "Playlists": "প্লে লিস্ট সমূহ",
"Photos": "ছবিগুলো", "Photos": "চিত্রসমূহ",
"NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ", "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
"NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে", "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
"NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না", "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
@ -71,9 +71,9 @@
"NameSeasonUnknown": "সিজন অজানা", "NameSeasonUnknown": "সিজন অজানা",
"NameSeasonNumber": "সিজন {0}", "NameSeasonNumber": "সিজন {0}",
"NameInstallFailed": "{0} ইন্সটল ব্যর্থ", "NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
"MusicVideos": "গানের ভিডিও", "MusicVideos": "সঙ্গীত ভিডিয়ো সমূহ",
"Music": "গান", "Music": "গান",
"Movies": "চলচ্চিত্র", "Movies": "চলচ্চিত্রসমূহ",
"MixedContent": "মিশ্র কন্টেন্ট", "MixedContent": "মিশ্র কন্টেন্ট",
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে", "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
"HeaderRecordingGroups": "রেকর্ডিং দল", "HeaderRecordingGroups": "রেকর্ডিং দল",
@ -117,5 +117,11 @@
"Forced": "জোরকরে", "Forced": "জোরকরে",
"TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.", "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
"TaskCleanActivityLog": "কাজের ফাইল খালি করুন", "TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
"Default": "প্রাথমিক" "Default": "ডিফল্ট",
"HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
"TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
"External": "বাহ্যিক",
"TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।"
} }

View File

@ -5,7 +5,7 @@
"Artists": "Artistes", "Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament", "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
"Books": "Llibres", "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", "Channels": "Canals",
"ChapterNameValue": "Capítol {0}", "ChapterNameValue": "Capítol {0}",
"Collections": "Col·leccions", "Collections": "Col·leccions",
@ -16,65 +16,65 @@
"Folders": "Carpetes", "Folders": "Carpetes",
"Genres": "Gèneres", "Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes de l'àlbum", "HeaderAlbumArtists": "Artistes de l'àlbum",
"HeaderContinueWatching": "Continua Veient", "HeaderContinueWatching": "Continuar veient",
"HeaderFavoriteAlbums": "Àlbums Preferits", "HeaderFavoriteAlbums": "Àlbums preferits",
"HeaderFavoriteArtists": "Artistes Predilectes", "HeaderFavoriteArtists": "Artistes preferits",
"HeaderFavoriteEpisodes": "Episodis Predilectes", "HeaderFavoriteEpisodes": "Episodis preferits",
"HeaderFavoriteShows": "Sèries Predilectes", "HeaderFavoriteShows": "Sèries preferides",
"HeaderFavoriteSongs": "Cançons Predilectes", "HeaderFavoriteSongs": "Cançons preferides",
"HeaderLiveTV": "TV en Directe", "HeaderLiveTV": "TV en directe",
"HeaderNextUp": "A continuació", "HeaderNextUp": "A continuació",
"HeaderRecordingGroups": "Grups d'Enregistrament", "HeaderRecordingGroups": "Grups d'enregistrament",
"HomeVideos": "Vídeos Domèstics", "HomeVideos": "Vídeos domèstics",
"Inherit": "Hereta", "Inherit": "Hereta",
"ItemAddedWithName": "{0} ha estat afegit a la biblioteca", "ItemAddedWithName": "{0} ha sigut afegit a la biblioteca",
"ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca", "ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca",
"LabelIpAddressValue": "Adreça IP: {0}", "LabelIpAddressValue": "Adreça IP: {0}",
"LabelRunningTimeValue": "Temps en funcionament: {0}", "LabelRunningTimeValue": "Temps en funcionament: {0}",
"Latest": "Darreres", "Latest": "Darrers",
"MessageApplicationUpdated": "El Servidor de Jellyfin ha estat actualitzat", "MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
"MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}", "MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada", "MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
"MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor", "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
"MixedContent": "Contingut barrejat", "MixedContent": "Contingut barrejat",
"Movies": "Pel·lícules", "Movies": "Pel·lícules",
"Music": "Música", "Music": "Música",
"MusicVideos": "Vídeos Musicals", "MusicVideos": "Videoclips",
"NameInstallFailed": "{0} instal·lació fallida", "NameInstallFailed": "{0} instal·lació fallida",
"NameSeasonNumber": "Temporada {0}", "NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconeguda", "NameSeasonUnknown": "Temporada desconeguda",
"NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.", "NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
"NotificationOptionApplicationUpdateAvailable": "Actualització d'aplicació disponible", "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualització d'aplicació instal·lada", "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada",
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada", "NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada", "NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
"NotificationOptionCameraImageUploaded": "Imatge de càmera pujada", "NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
"NotificationOptionInstallationFailed": "Instal·lació fallida", "NotificationOptionInstallationFailed": "Instal·lació fallida",
"NotificationOptionNewLibraryContent": "Nou contingut afegit", "NotificationOptionNewLibraryContent": "Nou contingut afegit",
"NotificationOptionPluginError": "Un connector ha fallat", "NotificationOptionPluginError": "Un complement ha fallat",
"NotificationOptionPluginInstalled": "Connector instal·lat", "NotificationOptionPluginInstalled": "Complement instal·lat",
"NotificationOptionPluginUninstalled": "Connector desinstal·lat", "NotificationOptionPluginUninstalled": "Complement desinstal·lat",
"NotificationOptionPluginUpdateInstalled": "Actualització de connector instal·lada", "NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada",
"NotificationOptionServerRestartRequired": "Reinici del servidor requerit", "NotificationOptionServerRestartRequired": "Reinici del servidor requerit",
"NotificationOptionTaskFailed": "Tasca programada fallida", "NotificationOptionTaskFailed": "Tasca programada fallida",
"NotificationOptionUserLockedOut": "Usuari tancat", "NotificationOptionUserLockedOut": "Usuari expulsat",
"NotificationOptionVideoPlayback": "Reproducció de video iniciada", "NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reproducció de video aturada", "NotificationOptionVideoPlaybackStopped": "Reproducció de vídeo aturada",
"Photos": "Fotos", "Photos": "Fotos",
"Playlists": "Llistes de reproducció", "Playlists": "Llistes de reproducció",
"Plugin": "Connector", "Plugin": "Complement",
"PluginInstalledWithName": "{0} ha estat instal·lat", "PluginInstalledWithName": "{0} ha estat instal·lat",
"PluginUninstalledWithName": "{0} ha estat desinstal·lat", "PluginUninstalledWithName": "{0} ha estat desinstal·lat",
"PluginUpdatedWithName": "{0} ha estat actualitzat", "PluginUpdatedWithName": "{0} ha estat actualitzat",
"ProviderValue": "Proveïdor: {0}", "ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat", "ScheduledTaskFailedWithName": "{0} ha fallat",
"ScheduledTaskStartedWithName": "{0} iniciat", "ScheduledTaskStartedWithName": "{0} s'ha iniciat",
"ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat", "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
"Shows": "Sèries", "Shows": "Sèries",
"Songs": "Cançons", "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}", "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", "Sync": "Sincronitzar",
"System": "Sistema", "System": "Sistema",
"TvShows": "Sèries de TV", "TvShows": "Sèries de TV",
@ -82,11 +82,11 @@
"UserCreatedWithName": "S'ha creat l'usuari {0}", "UserCreatedWithName": "S'ha creat l'usuari {0}",
"UserDeletedWithName": "L'usuari {0} ha estat eliminat", "UserDeletedWithName": "L'usuari {0} ha estat eliminat",
"UserDownloadingItemWithValues": "{0} està descarregant {1}", "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}", "UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
"UserOnlineFromDevice": "{0} està connectat des de {1}", "UserOnlineFromDevice": "{0} està connectat des de {1}",
"UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}", "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}", "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}",
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}", "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
"ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca", "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
@ -94,14 +94,14 @@
"VersionNumber": "Versió {0}", "VersionNumber": "Versió {0}",
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.", "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
"TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin", "TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'Internet.", "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
"TaskRefreshChannels": "Actualitza Canals", "TaskRefreshChannels": "Actualitza els canals",
"TaskCleanTranscodeDescription": "Elimina els arxius temporals de transcodificacions que tinguin més d'un dia.", "TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
"TaskCleanTranscode": "Neteja les transcodificacions", "TaskCleanTranscode": "Neteja les transcodificacions",
"TaskUpdatePluginsDescription": "Actualitza les extensions que estan configurades per actualitzar-se automàticament.", "TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.",
"TaskUpdatePlugins": "Actualitza les extensions", "TaskUpdatePlugins": "Actualitza els connectors",
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.", "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.", "TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja els registres", "TaskCleanLogs": "Neteja els registres",
"TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.", "TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
@ -110,12 +110,12 @@
"TaskRefreshChapterImages": "Extreure les imatges dels capítols", "TaskRefreshChapterImages": "Extreure les imatges dels capítols",
"TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.", "TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
"TaskCleanCache": "Elimina arxius temporals", "TaskCleanCache": "Elimina arxius temporals",
"TasksChannelsCategory": "Canals d'Internet", "TasksChannelsCategory": "Canals d'internet",
"TasksApplicationCategory": "Aplicació", "TasksApplicationCategory": "Aplicació",
"TasksLibraryCategory": "Biblioteca", "TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manteniment", "TasksMaintenanceCategory": "Manteniment",
"TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.", "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", "Undefined": "Indefinit",
"Forced": "Forçat", "Forced": "Forçat",
"Default": "Per defecte", "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.", "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", "TaskKeyframeExtractor": "Extractor de fotogrames clau",
"External": "Extern", "External": "Extern",
"HearingImpaired": "Discapacitat Auditiva" "HearingImpaired": "Discapacitat auditiva"
} }

View File

@ -28,7 +28,7 @@
"NameSeasonNumber": "Tymor {0}", "NameSeasonNumber": "Tymor {0}",
"MusicVideos": "Fideos Cerddoriaeth", "MusicVideos": "Fideos Cerddoriaeth",
"MixedContent": "Cynnwys amrywiol", "MixedContent": "Cynnwys amrywiol",
"HomeVideos": "Fideos Cartref", "HomeVideos": "Genres",
"HeaderNextUp": "Nesaf i Fyny", "HeaderNextUp": "Nesaf i Fyny",
"HeaderFavoriteArtists": "Ffefryn Artistiaid", "HeaderFavoriteArtists": "Ffefryn Artistiaid",
"HeaderFavoriteAlbums": "Ffefryn Albwmau", "HeaderFavoriteAlbums": "Ffefryn Albwmau",
@ -122,5 +122,6 @@
"TaskRefreshChapterImagesDescription": "Creu mân-luniau ar gyfer fideos sydd â phenodau.", "TaskRefreshChapterImagesDescription": "Creu mân-luniau ar gyfer fideos sydd â phenodau.",
"TaskRefreshChapterImages": "Echdynnu Lluniau Pennod", "TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
"TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.", "TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
"TaskCleanCache": "Gwaghau Ffolder Cache" "TaskCleanCache": "Gwaghau Ffolder Cache",
"HearingImpaired": "Nam ar y clyw"
} }

View File

@ -1,9 +1,9 @@
{ {
"Albums": "Albummer", "Albums": "Album",
"AppDeviceValues": "App: {0}, Enhed: {1}", "AppDeviceValues": "App: {0}, Enhed: {1}",
"Application": "Applikation", "Application": "Applikation",
"Artists": "Kunstnere", "Artists": "Kunstnere",
"AuthenticationSucceededWithUserName": "{0} succesfuldt autentificeret", "AuthenticationSucceededWithUserName": "{0} er logget ind",
"Books": "Bøger", "Books": "Bøger",
"CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}", "CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}",
"Channels": "Kanaler", "Channels": "Kanaler",
@ -11,17 +11,17 @@
"Collections": "Samlinger", "Collections": "Samlinger",
"DeviceOfflineWithName": "{0} har afbrudt forbindelsen", "DeviceOfflineWithName": "{0} har afbrudt forbindelsen",
"DeviceOnlineWithName": "{0} er forbundet", "DeviceOnlineWithName": "{0} er forbundet",
"FailedLoginAttemptWithUserName": "Fejlet loginforsøg fra {0}", "FailedLoginAttemptWithUserName": "Mislykket loginforsøg fra {0}",
"Favorites": "Favoritter", "Favorites": "Favoritter",
"Folders": "Mapper", "Folders": "Mapper",
"Genres": "Genrer", "Genres": "Genrer",
"HeaderAlbumArtists": "Albumkunstner", "HeaderAlbumArtists": "Albums kunstnere",
"HeaderContinueWatching": "Fortsæt afspilning", "HeaderContinueWatching": "Fortsæt afspilning",
"HeaderFavoriteAlbums": "Favoritalbummer", "HeaderFavoriteAlbums": "Favorit albummer",
"HeaderFavoriteArtists": "Favoritkunstnere", "HeaderFavoriteArtists": "Favorit kunstnere",
"HeaderFavoriteEpisodes": "Favoritepisoder", "HeaderFavoriteEpisodes": "Favorit afsnit",
"HeaderFavoriteShows": "Favoritserier", "HeaderFavoriteShows": "Favorit serier",
"HeaderFavoriteSongs": "Favoritsange", "HeaderFavoriteSongs": "Favorit sange",
"HeaderLiveTV": "Live-TV", "HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste", "HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper", "HeaderRecordingGroups": "Optagelsesgrupper",
@ -39,90 +39,90 @@
"MixedContent": "Blandet indhold", "MixedContent": "Blandet indhold",
"Movies": "Film", "Movies": "Film",
"Music": "Musik", "Music": "Musik",
"MusicVideos": "Musik videoer", "MusicVideos": "Musikvideoer",
"NameInstallFailed": "{0} installationen mislykkedes", "NameInstallFailed": "{0} installationen mislykkedes",
"NameSeasonNumber": "Sæson {0}", "NameSeasonNumber": "Sæson {0}",
"NameSeasonUnknown": "Ukendt sæson", "NameSeasonUnknown": "Ukendt sæson",
"NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig til download.", "NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig.",
"NotificationOptionApplicationUpdateAvailable": "Opdatering til applikation tilgængelig", "NotificationOptionApplicationUpdateAvailable": "Opdatering til applikationen er tilgængelig",
"NotificationOptionApplicationUpdateInstalled": "Opdatering til applikation installeret", "NotificationOptionApplicationUpdateInstalled": "Opdatering til applikationen blev installeret",
"NotificationOptionAudioPlayback": "Lydafspilning påbegyndt", "NotificationOptionAudioPlayback": "Lydafspilning påbegyndt",
"NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet", "NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet",
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet", "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
"NotificationOptionInstallationFailed": "Installationen fejlede", "NotificationOptionInstallationFailed": "Installationen mislykkedes",
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet", "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
"NotificationOptionPluginError": "Pluginfejl", "NotificationOptionPluginError": "Plugin fejl",
"NotificationOptionPluginInstalled": "Plugin installeret", "NotificationOptionPluginInstalled": "Plugin blev installeret",
"NotificationOptionPluginUninstalled": "Plugin afinstalleret", "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin installeret", "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
"NotificationOptionServerRestartRequired": "Genstart af server påkrævet", "NotificationOptionServerRestartRequired": "Genstart af serveren er påkrævet",
"NotificationOptionTaskFailed": "Planlagt opgave fejlet", "NotificationOptionTaskFailed": "Planlagt opgave er fejlet",
"NotificationOptionUserLockedOut": "Bruger låst ude", "NotificationOptionUserLockedOut": "Bruger er låst ude",
"NotificationOptionVideoPlayback": "Videoafspilning påbegyndt", "NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
"NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet", "NotificationOptionVideoPlaybackStopped": "Videoafspilning blev stoppet",
"Photos": "Fotoer", "Photos": "Fotos",
"Playlists": "Afspilningslister", "Playlists": "Afspilningslister",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} blev installeret", "PluginInstalledWithName": "{0} blev installeret",
"PluginUninstalledWithName": "{0} blev afinstalleret", "PluginUninstalledWithName": "{0} blev afinstalleret",
"PluginUpdatedWithName": "{0} blev opdateret", "PluginUpdatedWithName": "{0} blev opdateret",
"ProviderValue": "Udbyder: {0}", "ProviderValue": "Udbyder: {0}",
"ScheduledTaskFailedWithName": "{0} fejlet", "ScheduledTaskFailedWithName": "{0} mislykkedes",
"ScheduledTaskStartedWithName": "{0} påbegyndt", "ScheduledTaskStartedWithName": "{0} påbegyndte",
"ServerNameNeedsToBeRestarted": "{0} skal genstartes", "ServerNameNeedsToBeRestarted": "{0} skal genstartes",
"Shows": "Serier", "Shows": "Serier",
"Songs": "Sange", "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}", "SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}",
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke downloades fra {0} til {1}", "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
"Sync": "Synk", "Sync": "Synkroniser",
"System": "System", "System": "System",
"TvShows": "Tv-serier", "TvShows": "TV-serier",
"User": "Bruger", "User": "Bruger",
"UserCreatedWithName": "Bruger {0} er blevet oprettet", "UserCreatedWithName": "Bruger {0} er blevet oprettet",
"UserDeletedWithName": "Brugeren {0} er blevet slettet", "UserDeletedWithName": "Brugeren {0} er nu slettet",
"UserDownloadingItemWithValues": "{0} downloader {1}", "UserDownloadingItemWithValues": "{0} henter {1}",
"UserLockedOutWithName": "Brugeren {0} er blevet låst ude", "UserLockedOutWithName": "Brugeren {0} er blevet låst ude",
"UserOfflineFromDevice": "{0} har afbrudt fra {1}", "UserOfflineFromDevice": "{0} har afbrudt fra {1}",
"UserOnlineFromDevice": "{0} er online fra {1}", "UserOnlineFromDevice": "{0} er online fra {1}",
"UserPasswordChangedWithName": "Adgangskode er ændret for bruger {0}", "UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
"UserPolicyUpdatedWithName": "Brugerpolitik er blevet opdateret for {0}", "UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}", "UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}", "UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek", "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}", "ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}", "VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfiguration.", "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
"TaskDownloadMissingSubtitles": "Download manglende undertekster", "TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Downloader og installere opdateringer for plugins som er konfigureret til at opdatere automatisk.", "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins", "TaskUpdatePlugins": "Opdater Plugins",
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gammle.", "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
"TaskCleanLogs": "Ryd Log Mappe", "TaskCleanLogs": "Ryd Log mappe",
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdaterer metadata.", "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
"TaskRefreshLibrary": "Scan Medie Bibliotek", "TaskRefreshLibrary": "Scan Medie Bibliotek",
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke har brug for længere.", "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
"TaskCleanCache": "Ryd Cache Mappe", "TaskCleanCache": "Ryd Cache mappe",
"TasksChannelsCategory": "Internet Kanaler", "TasksChannelsCategory": "Internet Kanaler",
"TasksApplicationCategory": "Applikation", "TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek", "TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedligeholdelse", "TasksMaintenanceCategory": "Vedligeholdelse",
"TaskRefreshChapterImages": "Udtræk Kapitel billeder", "TaskRefreshChapterImages": "Udtræk kapitel billeder",
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.", "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
"TaskRefreshChannelsDescription": "Genopfrisker internet kanal information.", "TaskRefreshChannelsDescription": "Opdater internet kanal information.",
"TaskRefreshChannels": "Genopfrisk Kanaler", "TaskRefreshChannels": "Opdater Kanaler",
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.", "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
"TaskCleanTranscode": "Rengør Transcode Mappen", "TaskCleanTranscode": "Tøm Transcode mappen",
"TaskRefreshPeople": "Genopfrisk Personer", "TaskRefreshPeople": "Opdater Personer",
"TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek.", "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigureret alder.", "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
"TaskCleanActivityLog": "Ryd Aktivitetslog", "TaskCleanActivityLog": "Ryd Aktivitetslog",
"Undefined": "Udefineret", "Undefined": "Udefineret",
"Forced": "Tvunget", "Forced": "Tvunget",
"Default": "Standard", "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", "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.", "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
"TaskKeyframeExtractor": "Billedramme udtrækker", "TaskKeyframeExtractor": "Nøglebillede udtræk",
"External": "Ekstern", "External": "Ekstern",
"HearingImpaired": "Hørehæmmet" "HearingImpaired": "Hørehæmmet"
} }

View File

@ -118,11 +118,11 @@
"TaskCleanActivityLog": "Borrar log de actividades", "TaskCleanActivityLog": "Borrar log de actividades",
"Undefined": "Indefinido", "Undefined": "Indefinido",
"Forced": "Forzado", "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.", "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", "TaskOptimizeDatabase": "Optimización de base de datos",
"External": "Externo", "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.", "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", "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"HearingImpaired": "Personas con discapacidad auditiva" "HearingImpaired": "Discapacidad Auditiva"
} }

View File

@ -31,7 +31,7 @@
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca", "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}", "LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo de funcionamiento: {0}", "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
"Latest": "Últimos", "Latest": "Últimas",
"MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin", "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", "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", "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada",

View File

@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "Optimizar base de datos", "TaskOptimizeDatabase": "Optimizar base de datos",
"External": "Externo", "External": "Externo",
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.", "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave" "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"HearingImpaired": "Discapacidad auditiva"
} }

View File

@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.", "TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.",
"TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.", "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
"TaskKeyframeExtractor": "استخراج کننده فریم کلیدی", "TaskKeyframeExtractor": "استخراج کننده فریم کلیدی",
"External": "خارجی" "External": "خارجی",
"HearingImpaired": "مشکل شنوایی"
} }

View File

@ -118,7 +118,7 @@
"TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.", "TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.",
"TaskCleanActivityLog": "Tyhjennä toimintahistoria", "TaskCleanActivityLog": "Tyhjennä toimintahistoria",
"Undefined": "Määrittelemätön", "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", "TaskOptimizeDatabase": "Optimoi tietokanta",
"TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.", "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
"TaskKeyframeExtractor": "Avainkuvien purkain", "TaskKeyframeExtractor": "Avainkuvien purkain",

View File

@ -119,5 +119,9 @@
"Undefined": "Hindi tiyak", "Undefined": "Hindi tiyak",
"Forced": "Sapilitan", "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.", "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"
} }

View File

@ -1,7 +1,7 @@
{ {
"Albums": "Alben", "Albums": "Alben",
"AppDeviceValues": "App: {0}, Gerät: {1}", "AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung", "Application": "Applikation",
"Artists": "Künstler", "Artists": "Künstler",
"AuthenticationSucceededWithUserName": "{0} hat sich angemeldet", "AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
"Books": "Bücher", "Books": "Bücher",
@ -14,7 +14,7 @@
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}", "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
"Favorites": "Favoriten", "Favorites": "Favoriten",
"Folders": "Ordner", "Folders": "Ordner",
"Genres": "Genres", "Genres": "Genre",
"HeaderAlbumArtists": "Album-Künstler", "HeaderAlbumArtists": "Album-Künstler",
"HeaderContinueWatching": "weiter schauen", "HeaderContinueWatching": "weiter schauen",
"HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteAlbums": "Lieblingsalben",
@ -49,7 +49,7 @@
"NotificationOptionAudioPlayback": "Audiowedergab gstartet", "NotificationOptionAudioPlayback": "Audiowedergab gstartet",
"NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt", "NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
"NotificationOptionCameraImageUploaded": "Foti ueglade", "NotificationOptionCameraImageUploaded": "Foti ueglade",
"NotificationOptionInstallationFailed": "Installationsfehler", "NotificationOptionInstallationFailed": "Installationsfähler",
"NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt", "NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
"NotificationOptionPluginError": "Plugin-Fäuer", "NotificationOptionPluginError": "Plugin-Fäuer",
"NotificationOptionPluginInstalled": "Plugin installiert", "NotificationOptionPluginInstalled": "Plugin installiert",
@ -120,5 +120,9 @@
"Forced": "Erzwungen", "Forced": "Erzwungen",
"Default": "Standard", "Default": "Standard",
"TaskOptimizeDatabase": "Datenbank optimieren", "TaskOptimizeDatabase": "Datenbank optimieren",
"External": "Extern" "External": "Extern",
"TaskOptimizeDatabaseDescription": "Kompromiert d Datenbank und trennt freie Speicherplatz. Durch die Ufagb cha d Leistig nach em ne Scan vor Bibliothek oder andere Ufgabe verbesseret werde.",
"HearingImpaired": "Hörgschädigti",
"TaskKeyframeExtractor": "Keyframe-Extraktor",
"TaskKeyframeExtractorDescription": "Extrahiert Keyframes us Videodateien zum erstelle vo genauere HLS Playliste. Die Ufgab cha für e langi Zyt laufe."
} }

View File

@ -67,5 +67,61 @@
"Plugin": "प्लग-इन", "Plugin": "प्लग-इन",
"Playlists": "प्लेलिस्ट", "Playlists": "प्लेलिस्ट",
"Photos": "तस्वीरें", "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": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।"
} }

View File

@ -82,7 +82,7 @@
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui", "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui", "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}", "FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
"CameraImageUploadedFrom": "Gambar kamera baru telah diunggah dari {0}", "CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus", "DeviceOfflineWithName": "{0} telah terputus",
"DeviceOnlineWithName": "{0} telah terhubung", "DeviceOnlineWithName": "{0} telah terhubung",
"NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti", "NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti",

View File

@ -107,5 +107,14 @@
"TasksApplicationCategory": "Forrit", "TasksApplicationCategory": "Forrit",
"TasksLibraryCategory": "Miðlasafn", "TasksLibraryCategory": "Miðlasafn",
"TasksMaintenanceCategory": "Viðhald", "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"
} }

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