Merge branch 'master' into sort-nfo-data

This commit is contained in:
Marc Brooks 2023-11-16 11:19:40 -06:00 committed by GitHub
commit 5b98a5cfbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
582 changed files with 10999 additions and 8205 deletions

View File

@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
default: 7.0.x
default: 8.0.x
jobs:
- job: CompatibilityCheck

View File

@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
DotNetSdkVersion: 7.0.x
DotNetSdkVersion: 8.0.x
jobs:
- job: Build

View File

@ -168,6 +168,7 @@ jobs:
- job: CollectArtifacts
timeoutInMinutes: 20
displayName: 'Collect Artifacts'
condition: succeededOrFailed()
continueOnError: true
dependsOn:
- BuildPackage
@ -207,10 +208,10 @@ jobs:
steps:
- task: UseDotNet@2
displayName: 'Use .NET 7.0 sdk'
displayName: 'Use .NET 8.0 sdk'
inputs:
packageType: 'sdk'
version: '7.0.x'
version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'

View File

@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
default: 7.0.x
default: 8.0.x
jobs:
- job: Test
@ -94,5 +94,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json"
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json"
artifactName: 'OpenAPI Spec'

12
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "7.0.13",
"commands": [
"dotnet-ef"
]
}
}
}

View File

@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
dotnet-version: '8.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
uses: github/codeql-action/init@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
uses: github/codeql-action/autobuild@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
uses: github/codeql-action/analyze@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6

View File

@ -14,23 +14,23 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
dotnet-version: '8.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: openapi-head
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
openapi-base:
name: OpenAPI - BASE
@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -55,16 +55,16 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
dotnet-version: '8.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
openapi-diff:
permissions:
@ -112,7 +112,7 @@ jobs:
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@ -127,7 +127,7 @@ jobs:
</details>
- name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}

50
.github/workflows/ci-tests.yml vendored Normal file
View File

@ -0,0 +1,50 @@
name: Tests
on:
push:
branches:
- master
# Run tests against the forked branch, but
# do not allow access to secrets
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories
pull_request:
env:
SDK_VERSION: "8.0.x"
jobs:
run-tests:
strategy:
matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3
with:
dotnet-version: ${{ env.SDK_VERSION }}
- name: Run DotNet CLI Tests
run: >
dotnet test Jellyfin.sln
--configuration Release
--collect:"XPlat Code Coverage"
--settings tests/coverletArgs.runsettings
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
reporttypes: "Cobertura"
# TODO - which action / tool to use to publish code coverage results?
# - name: Publish code coverage results
- name: Publish OpenAPI Artifact
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
with:
name: "OpenAPI Spec"
path: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net*/openapi.json"

View File

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

35
.github/workflows/issue-stale.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Stale Issue Labeler
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
actions: write
jobs:
issues:
name: Check for stale issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
operations-per-run: 500
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
close-issue-message: |-
This issue was closed due to inactivity.

View File

@ -1,4 +1,4 @@
name: Automation
name: Project Automation
on:
push:
@ -9,19 +9,6 @@ on:
permissions: {}
jobs:
label:
name: Labeling
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
repoToken: ${{ secrets.JF_BOT_TOKEN }}
project:
name: Project board
runs-on: ubuntu-latest

View File

@ -0,0 +1,23 @@
name: Merge Conflict Labeler
on:
push:
branches:
- master
pull_request_target:
issue_comment:
permissions: {}
jobs:
label:
name: Labeling
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
repoToken: ${{ secrets.JF_BOT_TOKEN }}

View File

@ -0,0 +1,30 @@
name: Stale PR Check
on:
schedule:
- cron: '30 */12 * * *'
workflow_dispatch:
permissions:
pull-requests: write
actions: write
jobs:
prs-stale-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 }}
ascending: true
operations-per-run: 150
# 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.

View File

@ -0,0 +1,82 @@
name: '🆙 Auto bump_version'
on:
release:
types:
- published
workflow_dispatch:
inputs:
TAG_BRANCH:
required: true
description: release-x.y.z
NEXT_VERSION:
required: true
description: x.y.z
jobs:
auto_bump_version:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' && !contains(github.event.release.tag_name, 'rc') }}
env:
TAG_BRANCH: ${{ github.event.release.target_commitish }}
steps:
- name: Wait for deploy checks to finish
uses: jitterbit/await-check-suites@292a541bb7618078395b2ce711a0d89cfb8a568a # v1
with:
ref: ${{ env.TAG_BRANCH }}
intervalSeconds: 60
timeoutSeconds: 3600
- name: Setup YQ
uses: chrisdickinson/setup-yq@latest
with:
yq-version: v4.9.8
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ env.TAG_BRANCH }}
- name: Setup EnvVars
run: |-
CURRENT_VERSION=$(yq e '.version' build.yaml)
CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*}
CURRENT_PATCH=${CURRENT_VERSION##*.}
echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV
echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV
echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV
echo "NEXT_VERSION=${CURRENT_MAJOR_MINOR}.$(($CURRENT_PATCH + 1))" >> $GITHUB_ENV
- name: Run bump_version
run: ./bump_version ${{ env.NEXT_VERSION }}
- name: Commit Changes
run: |-
git config user.name "jellyfin-bot"
git config user.email "team@jellyfin.org"
git checkout ${{ env.TAG_BRANCH }}
git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
git push origin ${{ env.TAG_BRANCH }}
manual_bump_version:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }}
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ env.TAG_BRANCH }}
- name: Run bump_version
run: ./bump_version ${{ env.NEXT_VERSION }}
- name: Commit Changes
run: |-
git config user.name "jellyfin-bot"
git config user.email "team@jellyfin.org"
git checkout ${{ env.TAG_BRANCH }}
git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
git push origin ${{ env.TAG_BRANCH }}

View File

@ -1,51 +0,0 @@
name: Stale Check
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
issues:
name: Check issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
operations-per-run: 75
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
prs-conflicts:
name: Check PRs with merge conflicts
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
# The merge conflict action will remove the label when updated
remove-stale-when-updated: false
days-before-stale: -1
days-before-close: 90
days-before-issue-close: -1
stale-pr-label: merge conflict
close-pr-message: |-
This PR has been closed due to having unresolved merge conflicts.

4
.vscode/launch.json vendored
View File

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

View File

@ -57,6 +57,7 @@
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [iwalton3](https://github.com/iwalton3)
- [jftuga](https://github.com/jftuga)
- [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h)
@ -88,6 +89,7 @@
- [neilsb](https://github.com/neilsb)
- [nevado](https://github.com/nevado)
- [Nickbert7](https://github.com/Nickbert7)
- [nicknsy](https://github.com/nicknsy)
- [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka)
- [OancaAndrei](https://github.com/OancaAndrei)
@ -167,6 +169,9 @@
- [ipitio](https://github.com/ipitio)
- [TheTyrius](https://github.com/TheTyrius)
- [tallbl0nde](https://github.com/tallbl0nde)
- [sleepycatcoding](https://github.com/sleepycatcoding)
- [scampower3](https://github.com/scampower3)
- [Chris-Codes-It] (https://github.com/Chris-Codes-It)
# Emby Contributors
@ -237,3 +242,4 @@
- [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir)
- [JPUC1143](https://github.com/Jpuc1143/)
- [0x25CBFC4F](https://github.com/0x25CBFC4F)

View File

@ -2,9 +2,7 @@
<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" />
@ -17,36 +15,39 @@
<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="EFCoreSecondLevelCacheInterceptor" Version="4.0.0" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" />
<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.9" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.9" />
<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.9" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.9" />
<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.Data.Sqlite" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
@ -54,40 +55,37 @@
<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.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.1.0" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="8.0.1" />
<PackageVersion Include="prometheus-net" Version="8.1.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.Settings.Configuration" Version="7.0.1" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" />
<PackageVersion Include="SkiaSharp" Version="2.88.3" />
<PackageVersion Include="SkiaSharp" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
<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="Svg.Skia" Version="1.0.0.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageVersion Include="System.Text.Json" Version="7.0.3" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.0" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.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.5.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.4.2" />
<PackageVersion Include="xunit" Version="2.6.1" />
</ItemGroup>
</Project>

View File

@ -2,9 +2,9 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=7.0
ARG DOTNET_VERSION=8.0
FROM node:lts-alpine as web-builder
FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

View File

@ -2,10 +2,10 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=7.0
ARG DOTNET_VERSION=8.0
FROM node:lts-alpine as web-builder
FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

View File

@ -2,10 +2,10 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=7.0
ARG DOTNET_VERSION=8.0
FROM node:lts-alpine as web-builder
FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

View File

@ -494,7 +494,7 @@ namespace Emby.Dlna.ContentDirectory
{
var folder = (Folder)item;
string[] mediaTypes = Array.Empty<string>();
MediaType[] mediaTypes = Array.Empty<MediaType>();
bool? isFolder = null;
switch (search.SearchType)
@ -565,30 +565,18 @@ namespace Emby.Dlna.ContentDirectory
if (stubType != StubType.Folder && item is IHasCollectionType collectionFolder)
{
var collectionType = collectionFolder.CollectionType;
if (string.Equals(CollectionType.Music, collectionType, StringComparison.OrdinalIgnoreCase))
switch (collectionFolder.CollectionType)
{
return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
}
if (string.Equals(CollectionType.Movies, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
}
if (string.Equals(CollectionType.TvShows, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetTvFolders(item, user, stubType, sort, startIndex, limit);
}
if (string.Equals(CollectionType.Folders, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetFolders(user, startIndex, limit);
}
if (string.Equals(CollectionType.LiveTv, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetLiveTvChannels(user, sort, startIndex, limit);
case CollectionType.Music:
return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
case CollectionType.Movies:
return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
case CollectionType.TvShows:
return GetTvFolders(item, user, stubType, sort, startIndex, limit);
case CollectionType.Folders:
return GetFolders(user, startIndex, limit);
case CollectionType.LiveTv:
return GetLiveTvChannels(user, sort, startIndex, limit);
}
}
@ -917,7 +905,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetGenres(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
query.OrderBy = Array.Empty<(string, SortOrder)>();
query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var genresResult = _libraryManager.GetGenres(query);
@ -933,7 +921,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetMusicGenres(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
query.OrderBy = Array.Empty<(string, SortOrder)>();
query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var genresResult = _libraryManager.GetMusicGenres(query);
@ -949,7 +937,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
query.OrderBy = Array.Empty<(string, SortOrder)>();
query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var artists = _libraryManager.GetAlbumArtists(query);
@ -965,7 +953,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetMusicArtists(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
query.OrderBy = Array.Empty<(string, SortOrder)>();
query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var artists = _libraryManager.GetArtists(query);
return ToResult(query.StartIndex, artists);
@ -980,7 +968,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetFavoriteArtists(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
query.OrderBy = Array.Empty<(string, SortOrder)>();
query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
query.IsFavorite = true;
var artists = _libraryManager.GetArtists(query);
@ -1011,7 +999,7 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetNextUp(BaseItem parent, InternalItemsQuery query)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
@ -1036,7 +1024,7 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
@ -1203,9 +1191,9 @@ namespace Emby.Dlna.ContentDirectory
/// </summary>
/// <param name="sort">The <see cref="SortCriteria"/>.</param>
/// <param name="isPreSorted">True if pre-sorted.</param>
private static (string SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
private static (ItemSortBy SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
{
return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
return isPreSorted ? Array.Empty<(ItemSortBy, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
}
/// <summary>

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -45,8 +43,8 @@ namespace Emby.Dlna.Didl
private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor;
private readonly string _serverAddress;
private readonly string _accessToken;
private readonly User _user;
private readonly string? _accessToken;
private readonly User? _user;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager;
@ -56,10 +54,10 @@ namespace Emby.Dlna.Didl
public DidlBuilder(
DeviceProfile profile,
User user,
User? user,
IImageProcessor imageProcessor,
string serverAddress,
string accessToken,
string? accessToken,
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
@ -85,7 +83,7 @@ namespace Emby.Dlna.Didl
return url + "&dlnaheaders=true";
}
public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo)
public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo)
{
var settings = new XmlWriterSettings
{
@ -140,12 +138,12 @@ namespace Emby.Dlna.Didl
public void WriteItemElement(
XmlWriter writer,
BaseItem item,
User user,
BaseItem context,
User? user,
BaseItem? context,
StubType? contextStubType,
string deviceId,
Filter filter,
StreamInfo streamInfo = null)
StreamInfo? streamInfo = null)
{
var clientId = GetClientId(item, null);
@ -176,13 +174,14 @@ namespace Emby.Dlna.Didl
if (item is IHasMediaSources)
{
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
switch (item.MediaType)
{
AddAudioResource(writer, item, deviceId, filter, streamInfo);
}
else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
AddVideoResource(writer, item, deviceId, filter, streamInfo);
case MediaType.Audio:
AddAudioResource(writer, item, deviceId, filter, streamInfo);
break;
case MediaType.Video:
AddVideoResource(writer, item, deviceId, filter, streamInfo);
break;
}
}
@ -190,7 +189,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null)
{
if (streamInfo is null)
{
@ -203,7 +202,7 @@ namespace Emby.Dlna.Didl
Profile = _profile,
DeviceId = deviceId,
MaxBitrate = _profile.MaxStreamingBitrate
});
}) ?? throw new InvalidOperationException("No optimal video stream found");
}
var targetWidth = streamInfo.TargetWidth;
@ -315,7 +314,7 @@ namespace Emby.Dlna.Didl
var mediaSource = streamInfo.MediaSource;
if (mediaSource.RunTimeTicks.HasValue)
if (mediaSource?.RunTimeTicks.HasValue == true)
{
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
@ -410,7 +409,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context)
private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context)
{
if (itemStubType.HasValue)
{
@ -452,7 +451,7 @@ namespace Emby.Dlna.Didl
/// <param name="episode">The episode.</param>
/// <param name="context">Current context.</param>
/// <returns>Formatted name of the episode.</returns>
private string GetEpisodeDisplayName(Episode episode, BaseItem context)
private string GetEpisodeDisplayName(Episode episode, BaseItem? context)
{
string[] components;
@ -530,7 +529,7 @@ namespace Emby.Dlna.Didl
private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null)
{
writer.WriteStartElement(string.Empty, "res", NsDidl);
@ -544,14 +543,14 @@ namespace Emby.Dlna.Didl
MediaSources = sources.ToArray(),
Profile = _profile,
DeviceId = deviceId
});
}) ?? throw new InvalidOperationException("No optimal audio stream found");
}
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
var mediaSource = streamInfo.MediaSource;
if (mediaSource.RunTimeTicks.HasValue)
if (mediaSource?.RunTimeTicks is not null)
{
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
@ -634,7 +633,7 @@ namespace Emby.Dlna.Didl
// Samsung sometimes uses 1 as root
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string? requestedId = null)
{
writer.WriteStartElement(string.Empty, "container", NsDidl);
@ -678,14 +677,14 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo)
private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo)
{
if (!item.SupportsPositionTicksResume || item is Folder)
{
return;
}
XmlAttribute secAttribute = null;
XmlAttribute? secAttribute = null;
foreach (var attribute in _profile.XmlRootAttributes)
{
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@ -695,8 +694,8 @@ namespace Emby.Dlna.Didl
}
}
// Not a samsung device
if (secAttribute is null)
// Not a samsung device or no user data
if (secAttribute is null || user is null)
{
return;
}
@ -717,7 +716,7 @@ namespace Emby.Dlna.Didl
/// <summary>
/// Adds fields used by both items and folders.
/// </summary>
private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
{
// Don't filter on dc:title because not all devices will include it in the filter
// MediaMonkey for example won't display content without a title
@ -795,7 +794,7 @@ namespace Emby.Dlna.Didl
if (item.IsDisplayedAsFolder || stubType.HasValue)
{
string classType = null;
string? classType = null;
if (!_profile.RequiresPlainFolders)
{
@ -823,15 +822,15 @@ namespace Emby.Dlna.Didl
writer.WriteString(classType ?? "object.container.storageFolder");
}
else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
else if (item.MediaType == MediaType.Audio)
{
writer.WriteString("object.item.audioItem.musicTrack");
}
else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
else if (item.MediaType == MediaType.Photo)
{
writer.WriteString("object.item.imageItem.photo");
}
else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
else if (item.MediaType == MediaType.Video)
{
if (!_profile.RequiresPlainVideoItems && item is Movie)
{
@ -899,7 +898,7 @@ namespace Emby.Dlna.Didl
}
}
private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
{
AddCommonFields(item, itemStubType, context, writer, filter);
@ -975,7 +974,7 @@ namespace Emby.Dlna.Didl
private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer)
{
ImageDownloadInfo imageInfo = GetImageInfo(item);
ImageDownloadInfo? imageInfo = GetImageInfo(item);
if (imageInfo is null)
{
@ -1008,8 +1007,7 @@ namespace Emby.Dlna.Didl
if (!_profile.EnableAlbumArtInDidl)
{
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
|| string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video)
{
if (!stubType.HasValue)
{
@ -1018,7 +1016,7 @@ namespace Emby.Dlna.Didl
}
}
if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
if (!_profile.EnableSingleAlbumArtLimit || item.MediaType == MediaType.Photo)
{
AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG");
AddImageResElement(item, writer, 1024, 768, "jpg", "JPEG_MED");
@ -1073,7 +1071,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
private ImageDownloadInfo GetImageInfo(BaseItem item)
private ImageDownloadInfo? GetImageInfo(BaseItem item)
{
if (item.HasImage(ImageType.Primary))
{
@ -1118,7 +1116,7 @@ namespace Emby.Dlna.Didl
return null;
}
private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item)
{
if (item is null)
{
@ -1148,7 +1146,7 @@ namespace Emby.Dlna.Didl
private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
{
var imageInfo = item.GetImageInfo(type, 0);
string tag = null;
string? tag = null;
try
{
@ -1250,7 +1248,7 @@ namespace Emby.Dlna.Didl
{
internal Guid ItemId { get; set; }
internal string ImageTag { get; set; }
internal string? ImageTag { get; set; }
internal ImageType Type { get; set; }
@ -1260,9 +1258,9 @@ namespace Emby.Dlna.Didl
internal bool IsDirectStream { get; set; }
internal string Format { get; set; }
internal required string Format { get; set; }
internal ItemImageInfo ItemImageInfo { get; set; }
internal required ItemImageInfo ItemImageInfo { get; set; }
}
}
}

View File

@ -228,7 +228,7 @@ namespace Emby.Dlna
try
{
return _fileSystem.GetFilePaths(path)
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
.Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
.Select(i => ParseProfileFile(i, type))
.Where(i => i is not null)
.ToList()!; // We just filtered out all the nulls

View File

@ -17,7 +17,7 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@ -26,8 +26,12 @@
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="IDisposableAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

View File

@ -0,0 +1,72 @@
using System;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Text;
using Emby.Dlna.ConnectionManager;
using Emby.Dlna.ContentDirectory;
using Emby.Dlna.Main;
using Emby.Dlna.MediaReceiverRegistrar;
using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Rssdp.Infrastructure;
namespace Emby.Dlna.Extensions;
/// <summary>
/// Extension methods for adding DLNA services.
/// </summary>
public static class DlnaServiceCollectionExtensions
{
/// <summary>
/// Adds DLNA services to the provided <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="applicationHost">The <see cref="IServerApplicationHost"/>.</param>
public static void AddDlnaServices(
this IServiceCollection services,
IServerApplicationHost applicationHost)
{
services.AddHttpClient(NamedClient.Dlna, c =>
{
c.DefaultRequestHeaders.UserAgent.ParseAdd(
string.Format(
CultureInfo.InvariantCulture,
"{0}/{1} UPnP/1.0 {2}/{3}",
Environment.OSVersion.Platform,
Environment.OSVersion,
applicationHost.Name,
applicationHost.ApplicationVersionString));
c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from?
})
.ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.All,
RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
});
services.AddSingleton<IDlnaManager, DlnaManager>();
services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
services.AddSingleton<IContentDirectory, ContentDirectoryService>();
services.AddSingleton<IConnectionManager, ConnectionManagerService>();
services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>();
services.AddSingleton<ISsdpCommunicationsServer>(provider => new SsdpCommunicationsServer(
provider.GetRequiredService<ISocketFactory>(),
provider.GetRequiredService<INetworkManager>(),
provider.GetRequiredService<ILogger<SsdpCommunicationsServer>>())
{
IsShared = true
});
services.AddHostedService<DlnaHost>();
}
}

View File

@ -1,463 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
using Rssdp;
using Rssdp.Infrastructure;
namespace Emby.Dlna.Main
{
public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
{
private readonly IServerConfigurationManager _config;
private readonly ILogger<DlnaEntryPoint> _logger;
private readonly IServerApplicationHost _appHost;
private readonly ISessionManager _sessionManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager;
private readonly IImageProcessor _imageProcessor;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery;
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
private readonly bool _disabled;
private PlayToManager _manager;
private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer;
private bool _disposed;
public DlnaEntryPoint(
IServerConfigurationManager config,
ILoggerFactory loggerFactory,
IServerApplicationHost appHost,
ISessionManager sessionManager,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IUserManager userManager,
IDlnaManager dlnaManager,
IImageProcessor imageProcessor,
IUserDataManager userDataManager,
ILocalizationManager localizationManager,
IMediaSourceManager mediaSourceManager,
IDeviceDiscovery deviceDiscovery,
IMediaEncoder mediaEncoder,
ISocketFactory socketFactory,
INetworkManager networkManager,
IUserViewManager userViewManager,
ITVSeriesManager tvSeriesManager)
{
_config = config;
_appHost = appHost;
_sessionManager = sessionManager;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
_userManager = userManager;
_dlnaManager = dlnaManager;
_imageProcessor = imageProcessor;
_userDataManager = userDataManager;
_localization = localizationManager;
_mediaSourceManager = mediaSourceManager;
_deviceDiscovery = deviceDiscovery;
_mediaEncoder = mediaEncoder;
_socketFactory = socketFactory;
_networkManager = networkManager;
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
ContentDirectory = new ContentDirectory.ContentDirectoryService(
dlnaManager,
userDataManager,
imageProcessor,
libraryManager,
config,
userManager,
loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
httpClientFactory,
localizationManager,
mediaSourceManager,
userViewManager,
mediaEncoder,
tvSeriesManager);
ConnectionManager = new ConnectionManager.ConnectionManagerService(
dlnaManager,
config,
loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
httpClientFactory);
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
httpClientFactory,
config);
Current = this;
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
}
public static DlnaEntryPoint Current { get; private set; }
/// <summary>
/// Gets a value indicating whether the dlna server is enabled.
/// </summary>
public static bool Enabled { get; private set; }
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public async Task RunAsync()
{
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
if (_disabled)
{
// No use starting as dlna won't work, as we're running purely on HTTPS.
return;
}
ReloadComponents();
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
}
private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
{
ReloadComponents();
}
}
private void ReloadComponents()
{
var options = _config.GetDlnaConfiguration();
Enabled = options.EnableServer;
StartSsdpHandler();
if (options.EnableServer)
{
StartDevicePublisher(options);
}
else
{
DisposeDevicePublisher();
}
if (options.EnablePlayTo)
{
StartPlayToManager();
}
else
{
DisposePlayToManager();
}
}
private void StartSsdpHandler()
{
try
{
if (_communicationsServer is null)
{
var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{
IsShared = true
};
StartDeviceDiscovery(_communicationsServer);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ssdp handlers");
}
}
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
{
try
{
if (communicationsServer is not null)
{
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting device discovery");
}
}
private void DisposeDeviceDiscovery()
{
try
{
_logger.LogInformation("Disposing DeviceDiscovery");
((DeviceDiscovery)_deviceDiscovery).Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error stopping device discovery");
}
}
public void StartDevicePublisher(Configuration.DlnaOptions options)
{
if (_publisher is not null)
{
return;
}
try
{
_publisher = new SsdpDevicePublisher(
_communicationsServer,
Environment.OSVersion.Platform.ToString(),
// Can not use VersionString here since that includes OS and version
Environment.OSVersion.Version.ToString(),
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
SupportPnpRootDevice = false
};
RegisterServerEndpoints();
if (options.BlastAliveMessages)
{
_publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error registering endpoint");
}
}
private void RegisterServerEndpoints()
{
var udn = CreateUuid(_appHost.SystemId);
var descriptorUri = "/dlna/" + udn + "/description.xml";
// Only get bind addresses in LAN
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses()
.Where(x => x.Address is not null)
.Where(x => x.AddressFamily != AddressFamily.InterNetworkV6)
.ToList();
if (validInterfaces.Count == 0)
{
// No interfaces returned, fall back to loopback
validInterfaces = _networkManager.GetLoopbacks().ToList();
}
foreach (var intf in validInterfaces)
{
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri);
var device = new SsdpRootDevice
{
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
Address = intf.Address,
PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix),
FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server",
Uuid = udn
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
};
SetProperies(device, fullService);
_publisher.AddDevice(device);
var embeddedDevices = new[]
{
"urn:schemas-upnp-org:service:ContentDirectory:1",
"urn:schemas-upnp-org:service:ConnectionManager:1",
// "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
};
foreach (var subDevice in embeddedDevices)
{
var embeddedDevice = new SsdpEmbeddedDevice
{
FriendlyName = device.FriendlyName,
Manufacturer = device.Manufacturer,
ModelName = device.ModelName,
Uuid = udn
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
};
SetProperies(embeddedDevice, subDevice);
device.AddDevice(embeddedDevice);
}
}
}
private string CreateUuid(string text)
{
if (!Guid.TryParse(text, out var guid))
{
guid = text.GetMD5();
}
return guid.ToString("D", CultureInfo.InvariantCulture);
}
private void SetProperies(SsdpDevice device, string fullDeviceType)
{
var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
var serviceParts = service.Split(':');
var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
device.DeviceTypeNamespace = deviceTypeNamespace;
device.DeviceClass = serviceParts[1];
device.DeviceType = serviceParts[2];
}
private void StartPlayToManager()
{
lock (_syncLock)
{
if (_manager is not null)
{
return;
}
try
{
_manager = new PlayToManager(
_logger,
_sessionManager,
_libraryManager,
_userManager,
_dlnaManager,
_appHost,
_imageProcessor,
_deviceDiscovery,
_httpClientFactory,
_userDataManager,
_localization,
_mediaSourceManager,
_mediaEncoder);
_manager.Start();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting PlayTo manager");
}
}
}
private void DisposePlayToManager()
{
lock (_syncLock)
{
if (_manager is not null)
{
try
{
_logger.LogInformation("Disposing PlayToManager");
_manager.Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error disposing PlayTo manager");
}
_manager = null;
}
}
}
public void DisposeDevicePublisher()
{
if (_publisher is not null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_publisher.Dispose();
_publisher = null;
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
DisposeDevicePublisher();
DisposePlayToManager();
DisposeDeviceDiscovery();
if (_communicationsServer is not null)
{
_logger.LogInformation("Disposing SsdpCommunicationsServer");
_communicationsServer.Dispose();
_communicationsServer = null;
}
ContentDirectory = null;
ConnectionManager = null;
MediaReceiverRegistrar = null;
Current = null;
_disposed = true;
}
}
}

387
Emby.Dlna/Main/DlnaHost.cs Normal file
View File

@ -0,0 +1,387 @@
#pragma warning disable CA1031 // Do not catch general exception types.
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Rssdp;
using Rssdp.Infrastructure;
namespace Emby.Dlna.Main;
/// <summary>
/// A <see cref="IHostedService"/> that manages a DLNA server.
/// </summary>
public sealed class DlnaHost : IHostedService, IDisposable
{
private readonly ILogger<DlnaHost> _logger;
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost;
private readonly ISessionManager _sessionManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager;
private readonly IImageProcessor _imageProcessor;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery;
private readonly ISsdpCommunicationsServer _communicationsServer;
private readonly INetworkManager _networkManager;
private readonly object _syncLock = new();
private SsdpDevicePublisher? _publisher;
private PlayToManager? _manager;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="DlnaHost"/> class.
/// </summary>
/// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
/// <param name="userManager">The <see cref="IUserManager"/>.</param>
/// <param name="dlnaManager">The <see cref="IDlnaManager"/>.</param>
/// <param name="imageProcessor">The <see cref="IImageProcessor"/>.</param>
/// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
/// <param name="localizationManager">The <see cref="ILocalizationManager"/>.</param>
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
/// <param name="deviceDiscovery">The <see cref="IDeviceDiscovery"/>.</param>
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
/// <param name="communicationsServer">The <see cref="ISsdpCommunicationsServer"/>.</param>
/// <param name="networkManager">The <see cref="INetworkManager"/>.</param>
public DlnaHost(
IServerConfigurationManager config,
ILoggerFactory loggerFactory,
IServerApplicationHost appHost,
ISessionManager sessionManager,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IUserManager userManager,
IDlnaManager dlnaManager,
IImageProcessor imageProcessor,
IUserDataManager userDataManager,
ILocalizationManager localizationManager,
IMediaSourceManager mediaSourceManager,
IDeviceDiscovery deviceDiscovery,
IMediaEncoder mediaEncoder,
ISsdpCommunicationsServer communicationsServer,
INetworkManager networkManager)
{
_config = config;
_appHost = appHost;
_sessionManager = sessionManager;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
_userManager = userManager;
_dlnaManager = dlnaManager;
_imageProcessor = imageProcessor;
_userDataManager = userDataManager;
_localization = localizationManager;
_mediaSourceManager = mediaSourceManager;
_deviceDiscovery = deviceDiscovery;
_mediaEncoder = mediaEncoder;
_communicationsServer = communicationsServer;
_networkManager = networkManager;
_logger = loggerFactory.CreateLogger<DlnaHost>();
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
var netConfig = _config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
if (_appHost.ListenWithHttps && netConfig.RequireHttps)
{
if (_config.GetDlnaConfiguration().EnableServer)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
// No use starting as dlna won't work, as we're running purely on HTTPS.
return;
}
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
ReloadComponents();
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken)
{
Stop();
return Task.CompletedTask;
}
/// <inheritdoc />
public void Dispose()
{
if (!_disposed)
{
Stop();
_disposed = true;
}
}
private void OnNamedConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs e)
{
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
{
ReloadComponents();
}
}
private void ReloadComponents()
{
var options = _config.GetDlnaConfiguration();
StartDeviceDiscovery();
if (options.EnableServer)
{
StartDevicePublisher(options);
}
else
{
DisposeDevicePublisher();
}
if (options.EnablePlayTo)
{
StartPlayToManager();
}
else
{
DisposePlayToManager();
}
}
private static string CreateUuid(string text)
{
if (!Guid.TryParse(text, out var guid))
{
guid = text.GetMD5();
}
return guid.ToString("D", CultureInfo.InvariantCulture);
}
private static void SetProperties(SsdpDevice device, string fullDeviceType)
{
var serviceParts = fullDeviceType
.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
.Split(':');
device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
device.DeviceClass = serviceParts[1];
device.DeviceType = serviceParts[2];
}
private void StartDeviceDiscovery()
{
try
{
((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting device discovery");
}
}
private void StartDevicePublisher(Configuration.DlnaOptions options)
{
if (_publisher is not null)
{
return;
}
try
{
_publisher = new SsdpDevicePublisher(
_communicationsServer,
Environment.OSVersion.Platform.ToString(),
// Can not use VersionString here since that includes OS and version
Environment.OSVersion.Version.ToString(),
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
LogFunction = msg => _logger.LogDebug("{Msg}", msg),
SupportPnpRootDevice = false
};
RegisterServerEndpoints();
if (options.BlastAliveMessages)
{
_publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error registering endpoint");
}
}
private void RegisterServerEndpoints()
{
var udn = CreateUuid(_appHost.SystemId);
var descriptorUri = "/dlna/" + udn + "/description.xml";
// Only get bind addresses in LAN
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses()
.Where(x => x.AddressFamily != AddressFamily.InterNetworkV6)
.ToList();
if (validInterfaces.Count == 0)
{
// No interfaces returned, fall back to loopback
validInterfaces = _networkManager.GetLoopbacks().ToList();
}
foreach (var intf in validInterfaces)
{
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri);
var device = new SsdpRootDevice
{
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
Address = intf.Address,
PrefixLength = NetworkUtils.MaskToCidr(intf.Subnet.Prefix),
FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server",
Uuid = udn
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
};
SetProperties(device, fullService);
_publisher!.AddDevice(device);
var embeddedDevices = new[]
{
"urn:schemas-upnp-org:service:ContentDirectory:1",
"urn:schemas-upnp-org:service:ConnectionManager:1",
// "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
};
foreach (var subDevice in embeddedDevices)
{
var embeddedDevice = new SsdpEmbeddedDevice
{
FriendlyName = device.FriendlyName,
Manufacturer = device.Manufacturer,
ModelName = device.ModelName,
Uuid = udn
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
};
SetProperties(embeddedDevice, subDevice);
device.AddDevice(embeddedDevice);
}
}
}
private void StartPlayToManager()
{
lock (_syncLock)
{
if (_manager is not null)
{
return;
}
try
{
_manager = new PlayToManager(
_logger,
_sessionManager,
_libraryManager,
_userManager,
_dlnaManager,
_appHost,
_imageProcessor,
_deviceDiscovery,
_httpClientFactory,
_userDataManager,
_localization,
_mediaSourceManager,
_mediaEncoder);
_manager.Start();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting PlayTo manager");
}
}
}
private void DisposePlayToManager()
{
lock (_syncLock)
{
if (_manager is not null)
{
try
{
_logger.LogInformation("Disposing PlayToManager");
_manager.Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error disposing PlayTo manager");
}
_manager = null;
}
}
}
private void DisposeDevicePublisher()
{
if (_publisher is not null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_publisher.Dispose();
_publisher = null;
}
}
private void Stop()
{
DisposeDevicePublisher();
DisposePlayToManager();
}
}

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -25,7 +23,7 @@ namespace Emby.Dlna.PlayTo
private readonly ILogger _logger;
private readonly object _timerLock = new object();
private Timer _timer;
private Timer? _timer;
private int _muteVol;
private int _volume;
private DateTime _lastVolumeRefresh;
@ -40,13 +38,13 @@ namespace Emby.Dlna.PlayTo
_logger = logger;
}
public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
public event EventHandler<PlaybackStartEventArgs>? PlaybackStart;
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
public event EventHandler<PlaybackProgressEventArgs>? PlaybackProgress;
public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped;
public event EventHandler<MediaChangedEventArgs> MediaChanged;
public event EventHandler<MediaChangedEventArgs>? MediaChanged;
public DeviceInfo Properties { get; set; }
@ -75,13 +73,13 @@ namespace Emby.Dlna.PlayTo
public bool IsStopped => TransportState == TransportState.STOPPED;
public Action OnDeviceUnavailable { get; set; }
public Action? OnDeviceUnavailable { get; set; }
private TransportCommands AvCommands { get; set; }
private TransportCommands? AvCommands { get; set; }
private TransportCommands RendererCommands { get; set; }
private TransportCommands? RendererCommands { get; set; }
public UBaseObject CurrentMediaInfo { get; private set; }
public UBaseObject? CurrentMediaInfo { get; private set; }
public void Start()
{
@ -131,7 +129,7 @@ namespace Emby.Dlna.PlayTo
_volumeRefreshActive = true;
var time = immediate ? 100 : 10000;
_timer.Change(time, Timeout.Infinite);
_timer?.Change(time, Timeout.Infinite);
}
}
@ -149,7 +147,7 @@ namespace Emby.Dlna.PlayTo
_volumeRefreshActive = false;
_timer.Change(Timeout.Infinite, Timeout.Infinite);
_timer?.Change(Timeout.Infinite, Timeout.Infinite);
}
}
@ -199,7 +197,7 @@ namespace Emby.Dlna.PlayTo
}
}
private DeviceService GetServiceRenderingControl()
private DeviceService? GetServiceRenderingControl()
{
var services = Properties.Services;
@ -207,7 +205,7 @@ namespace Emby.Dlna.PlayTo
services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase));
}
private DeviceService GetAvTransportService()
private DeviceService? GetAvTransportService()
{
var services = Properties.Services;
@ -240,7 +238,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
rendererCommands.BuildPost(command, service.ServiceType, value),
rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@ -265,12 +263,7 @@ namespace Emby.Dlna.PlayTo
return;
}
var service = GetServiceRenderingControl();
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service");
// Set it early and assume it will succeed
// Remote control will perform better
@ -281,7 +274,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
rendererCommands.BuildPost(command, service.ServiceType, value),
rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
@ -296,26 +289,20 @@ namespace Emby.Dlna.PlayTo
return;
}
var service = GetAvTransportService();
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
RestartTimer(true);
}
public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken)
public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
@ -335,14 +322,8 @@ namespace Emby.Dlna.PlayTo
{ "CurrentURIMetaData", CreateDidlMeta(metaData) }
};
var service = GetAvTransportService();
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
@ -372,7 +353,7 @@ namespace Emby.Dlna.PlayTo
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
* Without that information, the next track command on the device does not work.
*/
public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
@ -380,7 +361,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
if (command is null)
{
return;
@ -392,14 +373,8 @@ namespace Emby.Dlna.PlayTo
{ "NextURIMetaData", CreateDidlMeta(metaData) }
};
var service = GetAvTransportService();
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
.ConfigureAwait(false);
@ -423,12 +398,7 @@ namespace Emby.Dlna.PlayTo
return Task.CompletedTask;
}
var service = GetAvTransportService();
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
@ -460,14 +430,13 @@ namespace Emby.Dlna.PlayTo
return;
}
var service = GetAvTransportService();
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
avCommands.BuildPost(command, service.ServiceType, 1),
avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@ -484,14 +453,13 @@ namespace Emby.Dlna.PlayTo
return;
}
var service = GetAvTransportService();
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
avCommands.BuildPost(command, service.ServiceType, 1),
avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@ -500,7 +468,7 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true);
}
private async void TimerCallback(object sender)
private async void TimerCallback(object? sender)
{
if (_disposed)
{
@ -623,7 +591,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
rendererCommands.BuildPost(command, service.ServiceType),
rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result is null || result.Document is null)
@ -673,7 +641,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
rendererCommands.BuildPost(command, service.ServiceType),
rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result is null || result.Document is null)
@ -728,7 +696,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
if (command is null)
@ -798,7 +766,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
if (command is null)
@ -871,7 +839,7 @@ namespace Emby.Dlna.PlayTo
return (true, null);
}
XElement uPnpResponse = null;
XElement? uPnpResponse = null;
try
{
@ -895,7 +863,7 @@ namespace Emby.Dlna.PlayTo
return (true, uTrack);
}
private XElement ParseResponse(string xml)
private XElement? ParseResponse(string xml)
{
// Handle different variations sent back by devices.
try
@ -929,7 +897,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri)
{
ArgumentNullException.ThrowIfNull(container);
@ -959,20 +927,17 @@ namespace Emby.Dlna.PlayTo
var resElement = container.Element(UPnpNamespaces.Res);
if (resElement is not null)
{
var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo);
if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
{
return info.Value.Split(':');
}
if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
{
return info.Value.Split(':');
}
return new string[4];
}
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken)
{
if (AvCommands is not null)
{
@ -1004,7 +969,7 @@ namespace Emby.Dlna.PlayTo
return AvCommands;
}
private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken)
private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken)
{
if (RendererCommands is not null)
{
@ -1054,7 +1019,7 @@ namespace Emby.Dlna.PlayTo
return baseUrl + url;
}
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
public static async Task<Device?> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
{
var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
@ -1171,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
return new Device(deviceProperties, httpClientFactory, logger);
}
#nullable enable
private static DeviceIcon CreateIcon(XElement element)
{
ArgumentNullException.ThrowIfNull(element);
@ -1284,11 +1248,10 @@ namespace Emby.Dlna.PlayTo
if (disposing)
{
_timer?.Dispose();
_timer = null;
Properties = null!;
}
_timer = null;
Properties = null;
_disposed = true;
}

View File

@ -55,41 +55,42 @@ namespace Emby.Dlna.PlayTo
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using MemoryStream ms = new MemoryStream();
await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
ms.Position = 0;
try
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
return await XDocument.LoadAsync(
ms,
LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch (XmlException)
{
// try correcting the Xml response with common errors
ms.Position = 0;
using StreamReader sr = new StreamReader(ms);
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
// 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,
stream,
LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch (XmlException ex)
catch (XmlException)
{
_logger.LogError(ex, "Failed to parse response");
_logger.LogDebug("Malformed response: {Content}\n", xmlString);
// try correcting the Xml response with common errors
stream.Position = 0;
using StreamReader sr = new StreamReader(stream);
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;
}
}
}
}

View File

@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.Didl;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@ -42,7 +43,7 @@ namespace Emby.Dlna.PlayTo
private readonly IDeviceDiscovery _deviceDiscovery;
private readonly string _serverAddress;
private readonly string _accessToken;
private readonly string? _accessToken;
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
private Device _device;
@ -59,7 +60,7 @@ namespace Emby.Dlna.PlayTo
IUserManager userManager,
IImageProcessor imageProcessor,
string serverAddress,
string accessToken,
string? accessToken,
IDeviceDiscovery deviceDiscovery,
IUserDataManager userDataManager,
ILocalizationManager localization,
@ -577,7 +578,7 @@ namespace Emby.Dlna.PlayTo
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 (item.MediaType == MediaType.Video)
{
return new PlaylistItem
{
@ -597,7 +598,7 @@ namespace Emby.Dlna.PlayTo
};
}
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
if (item.MediaType == MediaType.Audio)
{
return new PlaylistItem
{
@ -615,7 +616,7 @@ namespace Emby.Dlna.PlayTo
};
}
if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
if (item.MediaType == MediaType.Photo)
{
return PlaylistItemFactory.Create((Photo)item, profile);
}
@ -683,16 +684,15 @@ namespace Emby.Dlna.PlayTo
if (disposing)
{
_device.PlaybackStart -= OnDevicePlaybackStart;
_device.PlaybackProgress -= OnDevicePlaybackProgress;
_device.PlaybackStopped -= OnDevicePlaybackStopped;
_device.MediaChanged -= OnDeviceMediaChanged;
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
_device.OnDeviceUnavailable = null;
_device.Dispose();
}
_device.PlaybackStart -= OnDevicePlaybackStart;
_device.PlaybackProgress -= OnDevicePlaybackProgress;
_device.PlaybackStopped -= OnDevicePlaybackStopped;
_device.MediaChanged -= OnDeviceMediaChanged;
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
_device.OnDeviceUnavailable = null;
_disposed = true;
}

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -41,9 +39,9 @@ namespace Emby.Dlna.PlayTo
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
private bool _disposed;
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{
@ -67,7 +65,7 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
}
private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
{
if (_disposed)
{
@ -76,12 +74,12 @@ namespace Emby.Dlna.PlayTo
var info = e.Argument;
if (!info.Headers.TryGetValue("USN", out string usn))
if (!info.Headers.TryGetValue("USN", out string? usn))
{
usn = string.Empty;
}
if (!info.Headers.TryGetValue("NT", out string nt))
if (!info.Headers.TryGetValue("NT", out string? nt))
{
nt = string.Empty;
}
@ -161,7 +159,7 @@ namespace Emby.Dlna.PlayTo
var uri = info.Location;
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
if (info.Headers.TryGetValue("USN", out string uuid))
if (info.Headers.TryGetValue("USN", out string? uuid))
{
uuid = GetUuid(uuid);
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using Jellyfin.Data.Enums;
namespace Emby.Dlna.PlayTo
{
@ -33,19 +34,19 @@ namespace Emby.Dlna.PlayTo
{
var classType = UpnpClass ?? string.Empty;
if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Audio, StringComparison.Ordinal) != -1)
if (classType.Contains("Audio", StringComparison.Ordinal))
{
return MediaBrowser.Model.Entities.MediaType.Audio;
return "Audio";
}
if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
if (classType.Contains("Video", StringComparison.Ordinal))
{
return MediaBrowser.Model.Entities.MediaType.Video;
return "Video";
}
if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
if (classType.Contains("image", StringComparison.Ordinal))
{
return MediaBrowser.Model.Entities.MediaType.Photo;
return "Photo";
}
return null;

View File

@ -318,22 +318,24 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
// <!-- foo.E01., foo.e01. -->
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
{
DateTimeFormats = new[]
{
"yyyy.MM.dd",
"yyyy-MM-dd",
"yyyy_MM_dd"
"yyyy_MM_dd",
"yyyy MM dd"
}
},
new EpisodeExpression(@"(?<day>[0-9]{2})[.-](?<month>[0-9]{2})[.-](?<year>[0-9]{4})", true)
new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
{
DateTimeFormats = new[]
{
"dd.MM.yyyy",
"dd-MM-yyyy",
"dd_MM_yyyy"
"dd_MM_yyyy",
"dd MM yyyy"
}
},
@ -374,7 +376,7 @@ namespace Emby.Naming.Common
IsNamed = true,
SupportsAbsoluteEpisodeNumbers = false
},
new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$")
new EpisodeExpression(@"[\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\/]*)$")
{
SupportsAbsoluteEpisodeNumbers = true
},
@ -415,7 +417,7 @@ namespace Emby.Naming.Common
},
// "1-12 episode title"
new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
new EpisodeExpression("([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
@ -710,7 +712,7 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename
"^(?<chapter>[0-9]+)",
// Part if often ending of filename
@"(?<!ch(?:apter) )(?<part>[0-9]+)$",
"(?<!ch(?:apter) )(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part)
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -45,8 +45,12 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
<!-- Code Analyzers-->
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="IDisposableAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

View File

@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
return null;
}
var extension = Path.GetExtension(path);
var extension = Path.GetExtension(path.AsSpan());
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
{

View File

@ -26,19 +26,18 @@ namespace Emby.Naming.Video
return false;
}
var extension = Path.GetExtension(path);
var extension = Path.GetExtension(path.AsSpan());
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
path = Path.GetFileNameWithoutExtension(path);
var token = Path.GetExtension(path).TrimStart('.');
var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
foreach (var rule in options.StubTypes)
{
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
stubType = rule.StubType;
return true;

View File

@ -19,19 +19,23 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<!-- Code Analyzers-->
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="IDisposableAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>

View File

@ -61,7 +61,7 @@ namespace Emby.Photos
item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
{
try
{

View File

@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase
/// </summary>
public abstract class BaseApplicationPaths : IApplicationPaths
{
private string _dataPath;
/// <summary>
/// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
/// </summary>
@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
/// <summary>
@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
public string DataPath => _dataPath;
public string DataPath { get; }
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";

View File

@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
/// </summary>
public abstract class BaseConfigurationManager : IConfigurationManager
{
private readonly IFileSystem _fileSystem;
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
/// <summary>
/// The _configuration sync lock.
/// </summary>
private readonly object _configurationSyncLock = new object();
private readonly ConcurrentDictionary<string, object> _configurations = new();
private readonly object _configurationSyncLock = new();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
/// <param name="applicationPaths">The application paths.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
/// <param name="fileSystem">The file system.</param>
protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
protected BaseConfigurationManager(
IApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IXmlSerializer xmlSerializer)
{
CommonApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
UpdateCachePath();
@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
{
var file = Path.Combine(path, Guid.NewGuid().ToString());
File.WriteAllText(file, string.Empty);
_fileSystem.DeleteFile(file);
File.Delete(file);
}
private string GetConfigurationFile(string key)

View File

@ -12,11 +12,8 @@ using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main;
using Emby.Dlna.Ssdp;
using Emby.Naming.Common;
using Emby.Photos;
using Emby.Server.Implementations.Channels;
@ -44,7 +41,6 @@ using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common;
@ -59,7 +55,6 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@ -83,7 +78,6 @@ using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
@ -105,6 +99,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
@ -112,7 +107,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Class CompositionRoot.
/// </summary>
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
public abstract class ApplicationHost : IServerApplicationHost, IDisposable
{
/// <summary>
/// The disposable parts.
@ -120,14 +115,12 @@ namespace Emby.Server.Implementations
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
private readonly DeviceId _deviceId;
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
private ISessionManager _sessionManager;
/// <summary>
/// Gets or sets all concrete types.
@ -135,7 +128,7 @@ namespace Emby.Server.Implementations
/// <value>All concrete types.</value>
private Type[] _allConcreteTypes;
private bool _disposed = false;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@ -154,10 +147,8 @@ namespace Emby.Server.Implementations
LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
_fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
@ -165,13 +156,15 @@ namespace Emby.Server.Implementations
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer();
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
_disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
}
/// <summary>
@ -186,23 +179,16 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
&& !_startupOptions.IsService
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary>
public INetworkManager NetManager { get; private set; }
/// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary>
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
/// <inheritdoc />
public bool HasPendingRestart { get; private set; }
/// <inheritdoc />
public bool IsShuttingDown { get; private set; }
public bool ShouldRestart { get; set; }
/// <summary>
/// Gets the logger.
@ -324,7 +310,9 @@ namespace Emby.Server.Implementations
{
_creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type);
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
return ServiceProvider is null
? Activator.CreateInstance(type)
: ActivatorUtilities.CreateInstance(ServiceProvider, type);
}
catch (Exception ex)
{
@ -406,11 +394,9 @@ namespace Emby.Server.Implementations
/// <summary>
/// Runs the startup tasks.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
public async Task RunStartupTasksAsync()
{
cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks");
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
@ -424,8 +410,6 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports<IServerEntryPoint>();
cancellationToken.ThrowIfCancellationRequested();
var stopWatch = new Stopwatch();
stopWatch.Start();
@ -435,8 +419,6 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true;
cancellationToken.ThrowIfCancellationRequested();
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@ -466,7 +448,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
@ -509,7 +491,11 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton(_fileSystemManager);
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
serviceCollection.AddScoped<ISystemManager, SystemManager>();
serviceCollection.AddSingleton<TmdbClientManager>();
serviceCollection.AddSingleton(NetManager);
@ -575,8 +561,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
@ -588,8 +572,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
@ -633,8 +615,6 @@ namespace Emby.Server.Implementations
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);
_sessionManager = Resolve<ISessionManager>();
SetStaticProperties();
FindParts();
@ -685,7 +665,7 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.FileSystem = _fileSystemManager;
BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
Video.LiveTvManager = Resolve<ILiveTvManager>();
@ -855,38 +835,6 @@ namespace Emby.Server.Implementations
}
}
/// <summary>
/// Restarts this instance.
/// </summary>
public void Restart()
{
if (IsShuttingDown)
{
return;
}
IsShuttingDown = true;
_pluginManager.UnloadAssemblies();
Task.Run(async () =>
{
try
{
await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error sending server restart notification");
}
Logger.LogInformation("Calling RestartInternal");
RestartInternal();
});
}
protected abstract void RestartInternal();
/// <summary>
/// Gets the composable part assemblies.
/// </summary>
@ -920,7 +868,7 @@ namespace Emby.Server.Implementations
yield return typeof(MediaBrowser.MediaEncoding.Encoder.MediaEncoder).Assembly;
// Dlna
yield return typeof(DlnaEntryPoint).Assembly;
yield return typeof(DlnaHost).Assembly;
// Local metadata
yield return typeof(BoxSetXmlSaver).Assembly;
@ -942,49 +890,6 @@ namespace Emby.Server.Implementations
protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
/// <summary>
/// Gets the system status.
/// </summary>
/// <param name="request">Where this request originated.</param>
/// <returns>SystemInfo.</returns>
public SystemInfo GetSystemInfo(HttpRequest request)
{
return new SystemInfo
{
HasPendingRestart = HasPendingRestart,
IsShuttingDown = IsShuttingDown,
Version = ApplicationVersionString,
WebSocketPortNumber = HttpPort,
CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
Id = SystemId,
ProgramDataPath = ApplicationPaths.ProgramDataPath,
WebPath = ApplicationPaths.WebPath,
LogPath = ApplicationPaths.LogDirectoryPath,
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
PackageName = _startupOptions.PackageName
};
}
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{
return new PublicSystemInfo
{
Version = ApplicationVersionString,
ProductName = ApplicationProductName,
Id = SystemId,
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
/// <inheritdoc/>
public string GetSmartApiUrl(IPAddress remoteAddr)
{
@ -1002,11 +907,11 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
public string GetSmartApiUrl(HttpRequest request)
{
// Return the host in the HTTP request as the API url
// Return the host in the HTTP request as the API URL if not configured otherwise
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
{
int? requestPort = request.Host.Port;
if (requestPort == null
if (requestPort is null
|| (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
|| (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
{
@ -1037,7 +942,7 @@ namespace Emby.Server.Implementations
public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
{
// With an empty source, the port will be null
var smart = NetManager.GetBindAddress(ipAddress, out _, true);
var smart = NetManager.GetBindAddress(ipAddress, out _, false);
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart, scheme, port);
@ -1065,30 +970,6 @@ namespace Emby.Server.Implementations
}.ToString().TrimEnd('/');
}
/// <inheritdoc />
public async Task Shutdown()
{
if (IsShuttingDown)
{
return;
}
IsShuttingDown = true;
try
{
await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error sending server shutdown notification");
}
ShutdownInternal();
}
protected abstract void ShutdownInternal();
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes
@ -1152,52 +1033,5 @@ namespace Emby.Server.Implementations
_disposed = true;
}
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(false);
GC.SuppressFinalize(this);
}
/// <summary>
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
/// </summary>
/// <returns>A ValueTask.</returns>
protected virtual async ValueTask DisposeAsyncCore()
{
var type = GetType();
Logger.LogInformation("Disposing {Type}", type.Name);
foreach (var (part, _) in _disposableParts)
{
var partType = part.GetType();
if (partType == type)
{
continue;
}
Logger.LogInformation("Disposing {Type}", partType.Name);
try
{
part.Dispose();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error disposing {Type}", partType.Name);
}
}
if (_sessionManager != null)
{
// used for closing websockets
foreach (var session in _sessionManager.Sessions)
{
await session.DisposeAsync().ConfigureAwait(false);
}
}
}
}
}

View File

@ -371,8 +371,11 @@ namespace Emby.Server.Implementations.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path));
await using FileStream createStream = File.Create(path);
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
FileStream createStream = File.Create(path);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
}
}
/// <inheritdoc />
@ -1156,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels
if (info.People is not null && info.People.Count > 0)
{
_libraryManager.UpdatePeople(item, info.People);
await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
}
}
else if (forceUpdate)

View File

@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
/// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
/// <param name="loggerFactory">The paramref name="loggerFactory" factory.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
/// <param name="fileSystem">The file system.</param>
public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
: base(applicationPaths, loggerFactory, xmlSerializer, fileSystem)
public ServerConfigurationManager(
IApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IXmlSerializer xmlSerializer)
: base(applicationPaths, loggerFactory, xmlSerializer)
{
UpdateMetadataPath();
}

View File

@ -5,8 +5,8 @@
using System;
using System.Collections.Generic;
using Jellyfin.Extensions;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
@ -45,24 +45,6 @@ namespace Emby.Server.Implementations.Data
/// <value>The logger.</value>
protected ILogger<BaseSqliteRepository> Logger { get; }
/// <summary>
/// Gets the default connection flags.
/// </summary>
/// <value>The default connection flags.</value>
protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex;
/// <summary>
/// Gets the transaction mode.
/// </summary>
/// <value>The transaction mode.</value>>
protected TransactionMode TransactionMode => TransactionMode.Deferred;
/// <summary>
/// Gets the transaction mode for read-only operations.
/// </summary>
/// <value>The transaction mode.</value>
protected TransactionMode ReadTransactionMode => TransactionMode.Deferred;
/// <summary>
/// Gets the cache size.
/// </summary>
@ -107,23 +89,8 @@ namespace Emby.Server.Implementations.Data
/// <see cref="SynchronousMode"/>
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
/// <summary>
/// Gets or sets the write lock.
/// </summary>
/// <value>The write lock.</value>
protected ConnectionPool WriteConnections { get; set; }
/// <summary>
/// Gets or sets the write connection.
/// </summary>
/// <value>The write connection.</value>
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())
{
@ -131,57 +98,10 @@ namespace Emby.Server.Implementations.Data
}
}
protected ManagedConnection GetConnection(bool readOnly = false)
=> readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
protected SQLiteDatabaseConnection CreateWriteConnection()
protected SqliteConnection GetConnection()
{
var writeConnection = SQLite3.Open(
DbFilePath,
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
null);
if (CacheSize.HasValue)
{
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(LockingMode))
{
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
}
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
return writeConnection;
}
protected SQLiteDatabaseConnection CreateReadConnection()
{
var connection = SQLite3.Open(
DbFilePath,
DefaultConnectionFlags | ConnectionFlags.ReadOnly,
null);
var connection = new SqliteConnection($"Filename={DbFilePath}");
connection.Open();
if (CacheSize.HasValue)
{
@ -208,39 +128,38 @@ namespace Emby.Server.Implementations.Data
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
connection.Execute("PRAGMA page_size=" + PageSize.Value);
}
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return connection;
}
public IStatement PrepareStatement(ManagedConnection connection, string sql)
=> connection.PrepareStatement(sql);
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
=> connection.PrepareStatement(sql);
protected bool TableExists(ManagedConnection connection, string name)
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
{
return connection.RunInTransaction(
db =>
{
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
{
foreach (var row in statement.ExecuteQuery())
{
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
},
ReadTransactionMode);
var command = connection.CreateCommand();
command.CommandText = sql;
return command;
}
protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
protected bool TableExists(SqliteConnection connection, string name)
{
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
foreach (var row in statement.ExecuteQuery())
{
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
protected List<string> GetColumnNames(SqliteConnection connection, string table)
{
var columnNames = new List<string>();
@ -255,7 +174,7 @@ namespace Emby.Server.Implementations.Data
return columnNames;
}
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{
@ -291,12 +210,6 @@ namespace Emby.Server.Implementations.Data
return;
}
if (dispose)
{
WriteConnections.Dispose();
ReadConnections.Dispose();
}
_disposed = true;
}
}

View File

@ -1,79 +0,0 @@
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

@ -1,81 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
public sealed class ManagedConnection : IDisposable
{
private readonly ConnectionPool _pool;
private SQLiteDatabaseConnection _db;
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
{
_db = db;
_pool = pool;
}
public IStatement PrepareStatement(string sql)
{
return _db.PrepareStatement(sql);
}
public IEnumerable<IStatement> PrepareAll(string sql)
{
return _db.PrepareAll(sql);
}
public void ExecuteAll(string sql)
{
_db.ExecuteAll(sql);
}
public void Execute(string sql, params object[] values)
{
_db.Execute(sql, values);
}
public void RunQueries(string[] sql)
{
_db.RunQueries(sql);
}
public void RunInTransaction(Action<IDatabaseConnection> action, TransactionMode mode)
{
_db.RunInTransaction(action, mode);
}
public T RunInTransaction<T>(Func<IDatabaseConnection, T> action, TransactionMode mode)
{
return _db.RunInTransaction(action, mode);
}
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
{
return _db.Query(sql);
}
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
{
return _db.Query(sql, values);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_pool.Return(_db);
_db = null!; // Don't dispose it
_disposed = true;
}
}
}

View File

@ -1,11 +1,10 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Data;
using System.Globalization;
using SQLitePCL.pretty;
using Microsoft.Data.Sqlite;
namespace Emby.Server.Implementations.Data
{
@ -52,19 +51,29 @@ namespace Emby.Server.Implementations.Data
"yy-MM-dd"
};
public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries)
public static IEnumerable<SqliteDataReader> Query(this SqliteConnection sqliteConnection, string commandText)
{
ArgumentNullException.ThrowIfNull(queries);
connection.RunInTransaction(conn =>
if (sqliteConnection.State != ConnectionState.Open)
{
conn.ExecuteAll(string.Join(';', queries));
});
sqliteConnection.Open();
}
using var command = sqliteConnection.CreateCommand();
command.CommandText = commandText;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
yield return reader;
}
}
}
public static Guid ReadGuidFromBlob(this ResultSetValue result)
public static void Execute(this SqliteConnection sqliteConnection, string commandText)
{
return new Guid(result.ToBlob());
using var command = sqliteConnection.CreateCommand();
command.CommandText = commandText;
command.ExecuteNonQuery();
}
public static string ToDateTimeParamValue(this DateTime dateValue)
@ -83,27 +92,15 @@ namespace Emby.Server.Implementations.Data
private static string GetDateTimeKindFormat(DateTimeKind kind)
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
public static DateTime ReadDateTime(this ResultSetValue result)
public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result)
{
var dateText = result.ToString();
return DateTime.ParseExact(
dateText,
_datetimeFormats,
DateTimeFormatInfo.InvariantInfo,
DateTimeStyles.AdjustToUniversal);
}
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
{
var item = reader[index];
if (item.IsDbNull())
if (reader.IsDBNull(index))
{
result = default;
return false;
}
var dateText = item.ToString();
var dateText = reader.GetString(index);
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{
@ -115,335 +112,145 @@ namespace Emby.Server.Implementations.Data
return false;
}
public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
public static bool TryGetGuid(this SqliteDataReader reader, int index, out Guid result)
{
var item = reader[index];
if (item.IsDbNull())
if (reader.IsDBNull(index))
{
result = default;
return false;
}
result = item.ReadGuidFromBlob();
result = reader.GetGuid(index);
return true;
}
public static bool IsDbNull(this ResultSetValue result)
public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
{
return result.SQLiteType == SQLiteType.Null;
}
result = string.Empty;
public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToString();
}
public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
{
result = null;
var item = reader[index];
if (item.IsDbNull())
if (reader.IsDBNull(index))
{
return false;
}
result = item.ToString();
result = reader.GetString(index);
return true;
}
public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
public static bool TryGetBoolean(this SqliteDataReader reader, int index, out bool result)
{
return result[index].ToBool();
}
public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
{
var item = reader[index];
if (item.IsDbNull())
if (reader.IsDBNull(index))
{
result = default;
return false;
}
result = item.ToBool();
result = reader.GetBoolean(index);
return true;
}
public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
public static bool TryGetInt32(this SqliteDataReader reader, int index, out int result)
{
var item = reader[index];
if (item.IsDbNull())
if (reader.IsDBNull(index))
{
result = default;
return false;
}
result = item.ToInt();
result = reader.GetInt32(index);
return true;
}
public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
public static bool TryGetInt64(this SqliteDataReader reader, int index, out long result)
{
return result[index].ToInt64();
}
public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
{
var item = reader[index];
if (item.IsDbNull())
if (reader.IsDBNull(index))
{
result = default;
return false;
}
result = item.ToInt64();
result = reader.GetInt64(index);
return true;
}
public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
public static bool TryGetSingle(this SqliteDataReader reader, int index, out float result)
{
var item = reader[index];
if (item.IsDbNull())
if (reader.IsDBNull(index))
{
result = default;
return false;
}
result = item.ToFloat();
result = reader.GetFloat(index);
return true;
}
public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
public static bool TryGetDouble(this SqliteDataReader reader, int index, out double result)
{
var item = reader[index];
if (item.IsDbNull())
if (reader.IsDBNull(index))
{
result = default;
return false;
}
result = item.ToDouble();
result = reader.GetDouble(index);
return true;
}
public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index)
public static void TryBind(this SqliteCommand statement, string name, Guid value)
{
return result[index].ReadGuidFromBlob();
statement.TryBind(name, value, true);
}
[Conditional("DEBUG")]
private static void CheckName(string name)
public static void TryBind(this SqliteCommand statement, string name, object? value, bool isBlob = false)
{
throw new ArgumentException("Invalid param name: " + name, nameof(name));
}
public static void TryBind(this IStatement statement, string name, double value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
var preparedValue = value ?? DBNull.Value;
if (statement.Parameters.Contains(name))
{
bindParam.Bind(value);
statement.Parameters[name].Value = preparedValue;
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, string value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
if (value is null)
// Blobs aren't always detected automatically
if (isBlob)
{
bindParam.BindNull();
statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value });
}
else
{
bindParam.Bind(value);
statement.Parameters.AddWithValue(name, preparedValue);
}
}
else
}
public static void TryBindNull(this SqliteCommand statement, string name)
{
statement.TryBind(name, DBNull.Value);
}
public static IEnumerable<SqliteDataReader> ExecuteQuery(this SqliteCommand command)
{
using (var reader = command.ExecuteReader())
{
CheckName(name);
while (reader.Read())
{
yield return reader;
}
}
}
public static void TryBind(this IStatement statement, string name, bool value)
public static int SelectScalarInt(this SqliteCommand command)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
var result = command.ExecuteScalar();
// Can't be null since the method is used to retrieve Count
return Convert.ToInt32(result!, CultureInfo.InvariantCulture);
}
public static void TryBind(this IStatement statement, string name, float value)
public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, int value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, Guid value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
Span<byte> byteValue = stackalloc byte[16];
value.TryWriteBytes(byteValue);
bindParam.Bind(byteValue);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, DateTime value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value.ToDateTimeParamValue());
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, long value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBindNull(this IStatement statement, string name)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.BindNull();
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, DateTime? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, Guid? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, double? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, int? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, float? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, bool? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
{
while (statement.MoveNext())
{
yield return statement.Current;
}
var command = sqliteConnection.CreateCommand();
command.CommandText = sql;
return command;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
@ -44,48 +44,48 @@ namespace Emby.Server.Implementations.Data
var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : _userManager.Users;
using var transaction = connection.BeginTransaction();
connection.Execute(string.Join(
';',
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
"drop index if exists idx_userdata",
"drop index if exists idx_userdata1",
"drop index if exists idx_userdata2",
"drop index if exists userdataindex1",
"drop index if exists userdataindex",
"drop index if exists userdataindex3",
"drop index if exists userdataindex4",
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"));
connection.RunInTransaction(
db =>
{
db.ExecuteAll(string.Join(';', new[]
{
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
if (!userDataTableExists)
{
transaction.Commit();
return;
}
"drop index if exists idx_userdata",
"drop index if exists idx_userdata1",
"drop index if exists idx_userdata2",
"drop index if exists userdataindex1",
"drop index if exists userdataindex",
"drop index if exists userdataindex3",
"drop index if exists userdataindex4",
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
}));
var existingColumnNames = GetColumnNames(connection, "userdata");
if (userDataTableExists)
{
var existingColumnNames = GetColumnNames(db, "userdata");
AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
if (userDatasTableExists)
{
return;
}
if (!userDatasTableExists)
{
ImportUserIds(db, users);
ImportUserIds(connection, users);
db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
}
}
},
TransactionMode);
connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
transaction.Commit();
}
}
private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users)
private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
{
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
@ -101,13 +101,12 @@ namespace Emby.Server.Implementations.Data
statement.TryBind("@UserId", user.Id);
statement.TryBind("@InternalUserId", user.InternalId);
statement.MoveNext();
statement.Reset();
statement.ExecuteNonQuery();
}
}
}
private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db)
private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
{
var list = new List<Guid>();
@ -117,7 +116,7 @@ namespace Emby.Server.Implementations.Data
{
try
{
list.Add(row[0].ReadGuidFromBlob());
list.Add(row.GetGuid(0));
}
catch (Exception ex)
{
@ -169,17 +168,14 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{
connection.RunInTransaction(
db =>
{
SaveUserData(db, internalUserId, key, userData);
},
TransactionMode);
SaveUserData(connection, internalUserId, key, userData);
transaction.Commit();
}
}
private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData)
private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
{
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
{
@ -227,7 +223,7 @@ namespace Emby.Server.Implementations.Data
statement.TryBindNull("@SubtitleStreamIndex");
}
statement.MoveNext();
statement.ExecuteNonQuery();
}
}
@ -239,16 +235,14 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{
connection.RunInTransaction(
db =>
{
foreach (var userItemData in userDataList)
{
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
}
},
TransactionMode);
foreach (var userItemData in userDataList)
{
SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
}
transaction.Commit();
}
}
@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.Data
ArgumentException.ThrowIfNullOrEmpty(key);
using (var connection = GetConnection(true))
using (var connection = GetConnection())
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
@ -336,7 +330,7 @@ namespace Emby.Server.Implementations.Data
/// </summary>
/// <param name="reader">The list of result set values.</param>
/// <returns>The user item data.</returns>
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
private UserItemData ReadRow(SqliteDataReader reader)
{
var userData = new UserItemData();
@ -348,10 +342,10 @@ namespace Emby.Server.Implementations.Data
userData.Rating = rating;
}
userData.Played = reader[3].ToBool();
userData.PlayCount = reader[4].ToInt();
userData.IsFavorite = reader[5].ToBool();
userData.PlaybackPositionTicks = reader[6].ToInt64();
userData.Played = reader.GetBoolean(3);
userData.PlayCount = reader.GetInt32(4);
userData.IsFavorite = reader.GetBoolean(5);
userData.PlaybackPositionTicks = reader.GetInt64(6);
if (reader.TryReadDateTime(7, out var lastPlayedDate))
{

View File

@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ILyricManager _lyricManager;
private readonly ITrickplayManager _trickplayManager;
public DtoService(
ILogger<DtoService> logger,
@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory,
ILyricManager lyricManager)
ILyricManager lyricManager,
ITrickplayManager trickplayManager)
{
_logger = logger;
_libraryManager = libraryManager;
@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_lyricManager = lyricManager;
_trickplayManager = trickplayManager;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@ -903,10 +907,11 @@ namespace Emby.Server.Implementations.Dto
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
}
dto.LUFS = item.LUFS;
// Add audio info
if (item is Audio audio)
{
dto.LUFS = audio.LUFS;
dto.Album = audio.Album;
if (audio.ExtraType.HasValue)
{
@ -1058,6 +1063,11 @@ namespace Emby.Server.Implementations.Dto
dto.Chapters = _itemRepo.GetChapters(item);
}
if (options.ContainsField(ItemFields.Trickplay))
{
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
}
if (video.ExtraType.HasValue)
{
dto.ExtraType = video.ExtraType.Value.ToString();

View File

@ -24,6 +24,7 @@
<ItemGroup>
<PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Jellyfin.XmlTv" />
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
@ -31,7 +32,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Mono.Nat" />
<PackageReference Include="prometheus-net.DotNetRuntime" />
<PackageReference Include="SQLitePCL.pretty.netstandard" />
<PackageReference Include="DotNet.Glob" />
</ItemGroup>
@ -40,19 +40,22 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<!-- TODO: Add IDisposableAnalyzers -->
<!-- <PackageReference Include="IDisposableAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> -->
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

View File

@ -9,7 +9,7 @@ using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Networking.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Plugins;

View File

@ -6,19 +6,18 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Udp;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
namespace Emby.Server.Implementations.EntryPoints
{
/// <summary>
/// Class UdpServerEntryPoint.
/// Class responsible for registering all UDP broadcast endpoints and their handlers.
/// </summary>
public sealed class UdpServerEntryPoint : IServerEntryPoint
{
@ -35,14 +34,13 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IConfiguration _config;
private readonly IConfigurationManager _configurationManager;
private readonly INetworkManager _networkManager;
private readonly bool _enableMultiSocketBinding;
/// <summary>
/// The UDP server.
/// </summary>
private List<UdpServer> _udpServers;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private bool _disposed = false;
private readonly List<UdpServer> _udpServers;
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
@ -65,7 +63,6 @@ namespace Emby.Server.Implementations.EntryPoints
_configurationManager = configurationManager;
_networkManager = networkManager;
_udpServers = new List<UdpServer>();
_enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
}
/// <inheritdoc />
@ -80,19 +77,21 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
if (_enableMultiSocketBinding)
// Linux needs to bind to the broadcast addresses to get broadcast traffic
// Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
if (OperatingSystem.IsLinux())
{
// Add global broadcast socket
// Add global broadcast listener
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server);
// Add bind address specific broadcast sockets
// Add bind address specific broadcast listeners
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
foreach (var intf in validInterfaces)
{
var broadcastAddress = NetworkExtensions.GetBroadcastAddress(intf.Subnet);
var broadcastAddress = NetworkUtils.GetBroadcastAddress(intf.Subnet);
_logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber);
server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber);
@ -102,9 +101,18 @@ namespace Emby.Server.Implementations.EntryPoints
}
else
{
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber);
server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server);
// Add bind address specific broadcast listeners
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
foreach (var intf in validInterfaces)
{
var intfAddress = intf.Address;
_logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server);
}
}
}
catch (SocketException ex)
@ -119,7 +127,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (_disposed)
{
throw new ObjectDisposedException(this.GetType().Name);
throw new ObjectDisposedException(GetType().Name);
}
}

View File

@ -12,7 +12,6 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer

View File

@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.IO
}
}
public void ResetPath(string path, string affectedFile)
public void ResetPath(string path, string? affectedFile)
{
lock (_timerLock)
{
@ -148,13 +148,6 @@ namespace Emby.Server.Implementations.IO
{
item.ChangedExternally();
}
catch (IOException ex)
{
// For now swallow and log.
// Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
// Should we remove it from it's parent?
_logger.LogError(ex, "Error refreshing {Name}", item.Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {Name}", item.Name);
@ -217,7 +210,6 @@ namespace Emby.Server.Implementations.IO
DisposeTimer();
_disposed = true;
GC.SuppressFinalize(this);
}
}
}

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -160,7 +158,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e)
private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
@ -173,7 +171,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
@ -189,19 +187,28 @@ namespace Emby.Server.Implementations.IO
/// <param name="path">The path.</param>
/// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
if (path.IsEmpty)
{
throw new ArgumentException("Path can't be empty", nameof(path));
}
path = path.TrimEnd(Path.DirectorySeparatorChar);
return lst.Any(str =>
foreach (var str in lst)
{
// this should be a little quicker than examining each actual parent folder...
var compare = str.TrimEnd(Path.DirectorySeparatorChar);
var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
});
if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
|| (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar))
{
return true;
}
}
return false;
}
/// <summary>
@ -349,21 +356,19 @@ namespace Emby.Server.Implementations.IO
{
ArgumentException.ThrowIfNullOrEmpty(path);
var monitorPath = !IgnorePatterns.ShouldIgnore(path);
if (IgnorePatterns.ShouldIgnore(path))
{
return;
}
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
if (_tempIgnoredPaths.Keys.Any(i =>
foreach (var i in _tempIgnoredPaths.Keys)
{
if (_fileSystem.AreEqual(i, path))
if (_fileSystem.AreEqual(i, path)
|| _fileSystem.ContainsSubPath(i, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
return true;
}
if (_fileSystem.ContainsSubPath(i, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
return true;
return;
}
// Go up a level
@ -371,20 +376,11 @@ namespace Emby.Server.Implementations.IO
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
return true;
return;
}
return false;
}))
{
monitorPath = false;
}
if (monitorPath)
{
// Avoid implicitly captured closure
CreateRefresher(path);
}
CreateRefresher(path);
}
private void CreateRefresher(string path)
@ -417,7 +413,8 @@ namespace Emby.Server.Implementations.IO
}
// They are siblings. Rebase the refresher to the parent folder.
if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal))
if (parentPath is not null
&& Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
{
refresher.ResetPath(parentPath, path);
return;
@ -430,8 +427,13 @@ namespace Emby.Server.Implementations.IO
}
}
private void OnNewRefresherCompleted(object sender, EventArgs e)
private void OnNewRefresherCompleted(object? sender, EventArgs e)
{
if (sender is null)
{
return;
}
var refresher = (FileRefresher)sender;
DisposeRefresher(refresher);
}

View File

@ -15,10 +15,6 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public class ManagedFileSystem : IFileSystem
{
private readonly ILogger<ManagedFileSystem> _logger;
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath;
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
private static readonly char[] _invalidPathCharacters =
{
@ -29,23 +25,24 @@ namespace Emby.Server.Implementations.IO
(char)31, ':', '*', '?', '\\', '/'
};
private readonly ILogger<ManagedFileSystem> _logger;
private readonly List<IShortcutHandler> _shortcutHandlers;
private readonly string _tempPath;
/// <summary>
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
/// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param>
public ManagedFileSystem(
ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths)
IApplicationPaths applicationPaths,
IEnumerable<IShortcutHandler> shortcutHandlers)
{
_logger = logger;
_tempPath = applicationPaths.TempDirectory;
}
/// <inheritdoc />
public virtual void AddShortcutHandler(IShortcutHandler handler)
{
_shortcutHandlers.Add(handler);
_shortcutHandlers = shortcutHandlers.ToList();
}
/// <summary>
@ -94,7 +91,7 @@ namespace Emby.Server.Implementations.IO
}
// unc path
if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
{
return filePath;
}
@ -106,15 +103,17 @@ namespace Emby.Server.Implementations.IO
return filePath;
}
var filePathSpan = filePath.AsSpan();
// relative path
if (firstChar == '\\')
{
filePath = filePath.Substring(1);
filePathSpan = filePathSpan.Slice(1);
}
try
{
return Path.GetFullPath(Path.Combine(folderPath, filePath));
return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
}
catch (ArgumentException)
{
@ -485,25 +484,11 @@ namespace Emby.Server.Implementations.IO
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
/// <inheritdoc />
public virtual string NormalizePath(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
{
return path;
}
return Path.TrimEndingDirectorySeparator(path);
}
/// <inheritdoc />
public virtual bool AreEqual(string path1, string path2)
{
return string.Equals(
NormalizePath(path1),
NormalizePath(path2),
return Path.TrimEndingDirectorySeparator(path1).Equals(
Path.TrimEndingDirectorySeparator(path2),
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}

View File

@ -8,24 +8,17 @@ namespace Emby.Server.Implementations.IO
{
public class MbLinkShortcutHandler : IShortcutHandler
{
private readonly IFileSystem _fileSystem;
public MbLinkShortcutHandler(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
public string Extension => ".mblink";
public string? Resolve(string shortcutPath)
{
ArgumentException.ThrowIfNullOrEmpty(shortcutPath);
if (string.Equals(Path.GetExtension(shortcutPath), ".mblink", StringComparison.OrdinalIgnoreCase))
if (Path.GetExtension(shortcutPath.AsSpan()).Equals(".mblink", StringComparison.OrdinalIgnoreCase))
{
var path = File.ReadAllText(shortcutPath);
return _fileSystem.NormalizePath(path);
return Path.TrimEndingDirectorySeparator(path);
}
return null;

View File

@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Images
Recursive = true,
DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary },
OrderBy = new (string, SortOrder)[]
OrderBy = new (ItemSortBy, SortOrder)[]
{
(ItemSortBy.IsFolder, SortOrder.Ascending),
(ItemSortBy.SortName, SortOrder.Ascending)

View File

@ -30,47 +30,43 @@ namespace Emby.Server.Implementations.Images
BaseItemKind[] includeItemTypes;
if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
switch (viewType)
{
includeItemTypes = new[] { BaseItemKind.Movie };
}
else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
{
includeItemTypes = new[] { BaseItemKind.Series };
}
else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
{
includeItemTypes = new[] { BaseItemKind.MusicAlbum };
}
else if (string.Equals(viewType, CollectionType.MusicVideos, StringComparison.Ordinal))
{
includeItemTypes = new[] { BaseItemKind.MusicVideo };
}
else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal))
{
includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
}
else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal))
{
includeItemTypes = new[] { BaseItemKind.BoxSet };
}
else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal))
{
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
}
else
{
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
case CollectionType.Movies:
includeItemTypes = new[] { BaseItemKind.Movie };
break;
case CollectionType.TvShows:
includeItemTypes = new[] { BaseItemKind.Series };
break;
case CollectionType.Music:
includeItemTypes = new[] { BaseItemKind.MusicAlbum };
break;
case CollectionType.MusicVideos:
includeItemTypes = new[] { BaseItemKind.MusicVideo };
break;
case CollectionType.Books:
includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
break;
case CollectionType.BoxSets:
includeItemTypes = new[] { BaseItemKind.BoxSet };
break;
case CollectionType.HomeVideos:
case CollectionType.Photos:
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
break;
default:
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
break;
}
var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase);
var recursive = viewType != CollectionType.Playlists;
return view.GetItemList(new InternalItemsQuery
{
CollapseBoxSetItems = false,
Recursive = recursive,
DtoOptions = new DtoOptions(false),
ImageTypes = new ImageType[] { ImageType.Primary },
ImageTypes = new[] { ImageType.Primary },
Limit = 8,
OrderBy = new[]
{

View File

@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Images
var view = (UserView)item;
var isUsingCollectionStrip = IsUsingCollectionStrip(view);
var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.BoxSets && view.ViewType != CollectionType.Playlists;
var result = view.GetItemList(new InternalItemsQuery
{
@ -112,14 +112,14 @@ namespace Emby.Server.Implementations.Images
private static bool IsUsingCollectionStrip(UserView view)
{
string[] collectionStripViewTypes =
CollectionType[] collectionStripViewTypes =
{
CollectionType.Movies,
CollectionType.TvShows,
CollectionType.Playlists
};
return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty);
return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value);
}
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)

View File

@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
// bts sync files
"**/*.bts",
"**/*.sync",
// zfs
"**/.zfs/**",
"**/.zfs"
};
private static readonly GlobOptions _globOptions = new GlobOptions

View File

@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -45,7 +46,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger;
private readonly IMemoryCache _memoryCache;
private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
@ -111,7 +111,6 @@ namespace Emby.Server.Implementations.Library
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="memoryCache">The memory cache.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
public LibraryManager(
@ -128,7 +127,6 @@ namespace Emby.Server.Implementations.Library
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IImageProcessor imageProcessor,
IMemoryCache memoryCache,
NamingOptions namingOptions,
IDirectoryService directoryService)
{
@ -145,7 +143,7 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
_memoryCache = memoryCache;
_cache = new ConcurrentDictionary<Guid, BaseItem>();
_namingOptions = namingOptions;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
@ -300,7 +298,7 @@ namespace Emby.Server.Implementations.Library
}
}
_memoryCache.Set(item.Id, item);
_cache[item.Id] = item;
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@ -359,7 +357,7 @@ namespace Emby.Server.Implementations.Library
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
: Enumerable.Empty<BaseItem>();
: Array.Empty<BaseItem>();
foreach (var metadataPath in GetMetadataPaths(item, children))
{
@ -441,7 +439,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.DeleteItem(child.Id);
}
_memoryCache.Remove(item.Id);
_cache.TryRemove(item.Id, out _);
ReportItemRemoved(item, parent);
}
@ -527,14 +525,14 @@ namespace Emby.Server.Implementations.Library
IDirectoryService directoryService,
IItemResolver[] resolvers,
Folder parent = null,
string collectionType = null,
CollectionType? collectionType = null,
LibraryOptions libraryOptions = null)
{
ArgumentNullException.ThrowIfNull(fileInfo);
var fullPath = fileInfo.FullName;
if (string.IsNullOrEmpty(collectionType) && parent is not null)
if (collectionType is null && parent is not null)
{
collectionType = GetContentTypeOverride(fullPath, true);
}
@ -609,7 +607,7 @@ namespace Emby.Server.Implementations.Library
var originalList = paths.ToList();
var list = originalList.Where(i => i.IsDirectory)
.Select(i => _fileSystem.NormalizePath(i.FullName))
.Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
@ -637,7 +635,7 @@ namespace Emby.Server.Implementations.Library
return !args.ContainsFileSystemEntryByName(".ignore");
}
public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null)
public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null)
{
return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
}
@ -647,7 +645,7 @@ namespace Emby.Server.Implementations.Library
IDirectoryService directoryService,
Folder parent,
LibraryOptions libraryOptions,
string collectionType,
CollectionType? collectionType,
IItemResolver[] resolvers)
{
var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
@ -677,7 +675,7 @@ namespace Emby.Server.Implementations.Library
IReadOnlyList<FileSystemMetadata> fileList,
IDirectoryService directoryService,
Folder parent,
string collectionType,
CollectionType? collectionType,
IItemResolver[] resolvers,
LibraryOptions libraryOptions)
{
@ -840,19 +838,12 @@ namespace Emby.Server.Implementations.Library
{
var path = Person.GetPath(name);
var id = GetItemByNameId<Person>(path);
if (GetItemById(id) is not Person item)
if (GetItemById(id) is Person item)
{
item = new Person
{
Name = name,
Id = id,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
Path = path
};
return item;
}
return item;
return null;
}
/// <summary>
@ -1163,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
Name = Path.GetFileName(dir),
Locations = _fileSystem.GetFilePaths(dir, false)
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
.Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.Select(i =>
{
try
@ -1233,7 +1224,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
if (_memoryCache.TryGetValue(id, out BaseItem item))
if (_cache.TryGetValue(id, out BaseItem item))
{
return item;
}
@ -1523,7 +1514,7 @@ namespace Emby.Server.Implementations.Library
{
if (item is UserView view)
{
if (string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.Ordinal))
if (view.ViewType == CollectionType.LiveTv)
{
return new[] { view.Id };
}
@ -1552,13 +1543,13 @@ namespace Emby.Server.Implementations.Library
}
// Handle grouping
if (user is not null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)
if (user is not null && view.ViewType != CollectionType.Unknown && UserView.IsEligibleForGrouping(view.ViewType)
&& user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
{
return GetUserRootFolder()
.GetChildren(user, true)
.OfType<CollectionFolder>()
.Where(i => string.IsNullOrEmpty(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase))
.Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
.Where(i => user.IsFolderGrouped(i.Id))
.SelectMany(i => GetTopParentIdsForQuery(i, user));
}
@ -1687,7 +1678,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="sortBy">The sort by.</param>
/// <param name="sortOrder">The sort order.</param>
/// <returns>IEnumerable{BaseItem}.</returns>
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder)
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
{
var isFirst = true;
@ -1710,7 +1701,7 @@ namespace Emby.Server.Implementations.Library
return orderedItems ?? items;
}
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy)
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
{
var isFirst = true;
@ -1745,9 +1736,9 @@ namespace Emby.Server.Implementations.Library
/// <param name="name">The name.</param>
/// <param name="user">The user.</param>
/// <returns>IBaseItemComparer.</returns>
private IBaseItemComparer GetComparer(string name, User user)
private IBaseItemComparer GetComparer(ItemSortBy name, User user)
{
var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase));
var comparer = Comparers.FirstOrDefault(c => name == c.Type);
// If it requires a user, create a new one, and assign the user
if (comparer is IUserBaseItemComparer)
@ -2074,16 +2065,16 @@ namespace Emby.Server.Implementations.Library
: collectionFolder.GetLibraryOptions();
}
public string GetContentType(BaseItem item)
public CollectionType? GetContentType(BaseItem item)
{
string configuredContentType = GetConfiguredContentType(item, false);
if (!string.IsNullOrEmpty(configuredContentType))
var configuredContentType = GetConfiguredContentType(item, false);
if (configuredContentType is not null)
{
return configuredContentType;
}
configuredContentType = GetConfiguredContentType(item, true);
if (!string.IsNullOrEmpty(configuredContentType))
if (configuredContentType is not null)
{
return configuredContentType;
}
@ -2091,31 +2082,31 @@ namespace Emby.Server.Implementations.Library
return GetInheritedContentType(item);
}
public string GetInheritedContentType(BaseItem item)
public CollectionType? GetInheritedContentType(BaseItem item)
{
var type = GetTopFolderContentType(item);
if (!string.IsNullOrEmpty(type))
if (type is not null)
{
return type;
}
return item.GetParents()
.Select(GetConfiguredContentType)
.LastOrDefault(i => !string.IsNullOrEmpty(i));
.LastOrDefault(i => i is not null);
}
public string GetConfiguredContentType(BaseItem item)
public CollectionType? GetConfiguredContentType(BaseItem item)
{
return GetConfiguredContentType(item, false);
}
public string GetConfiguredContentType(string path)
public CollectionType? GetConfiguredContentType(string path)
{
return GetContentTypeOverride(path, false);
}
public string GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
{
if (item is ICollectionFolder collectionFolder)
{
@ -2125,16 +2116,21 @@ namespace Emby.Server.Implementations.Library
return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
}
private string GetContentTypeOverride(string path, bool inherit)
private CollectionType? GetContentTypeOverride(string path, bool inherit)
{
var nameValuePair = _configurationManager.Configuration.ContentTypes
.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
|| (inherit && !string.IsNullOrEmpty(i.Name)
&& _fileSystem.ContainsSubPath(i.Name, path)));
return nameValuePair?.Value;
if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
{
return collectionType;
}
return null;
}
private string GetTopFolderContentType(BaseItem item)
private CollectionType? GetTopFolderContentType(BaseItem item)
{
if (item is null)
{
@ -2156,13 +2152,13 @@ namespace Emby.Server.Implementations.Library
.OfType<ICollectionFolder>()
.Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path))
.Select(i => i.CollectionType)
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
.FirstOrDefault(i => i is not null);
}
public UserView GetNamedView(
User user,
string name,
string viewType,
CollectionType? viewType,
string sortName)
{
return GetNamedView(user, name, Guid.Empty, viewType, sortName);
@ -2170,13 +2166,13 @@ namespace Emby.Server.Implementations.Library
public UserView GetNamedView(
string name,
string viewType,
CollectionType viewType,
string sortName)
{
var path = Path.Combine(
_configurationManager.ApplicationPaths.InternalMetadataPath,
"views",
_fileSystem.GetValidFilename(viewType));
_fileSystem.GetValidFilename(viewType.ToString()));
var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
@ -2216,13 +2212,13 @@ namespace Emby.Server.Implementations.Library
User user,
string name,
Guid parentId,
string viewType,
CollectionType? viewType,
string sortName)
{
var parentIdString = parentId.Equals(default)
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType ?? string.Empty);
var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
var id = GetNewItemId(idValues, typeof(UserView));
@ -2278,7 +2274,7 @@ namespace Emby.Server.Implementations.Library
public UserView GetShadowView(
BaseItem parent,
string viewType,
CollectionType? viewType,
string sortName)
{
ArgumentNullException.ThrowIfNull(parent);
@ -2286,7 +2282,7 @@ namespace Emby.Server.Implementations.Library
var name = parent.Name;
var parentId = parent.Id;
var idValues = "38_namedview_" + name + parentId + (viewType ?? string.Empty);
var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
var id = GetNewItemId(idValues, typeof(UserView));
@ -2343,7 +2339,7 @@ namespace Emby.Server.Implementations.Library
public UserView GetNamedView(
string name,
Guid parentId,
string viewType,
CollectionType? viewType,
string sortName,
string uniqueId)
{
@ -2352,7 +2348,7 @@ namespace Emby.Server.Implementations.Library
var parentIdString = parentId.Equals(default)
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty);
var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
if (!string.IsNullOrEmpty(uniqueId))
{
idValues += uniqueId;
@ -2387,7 +2383,7 @@ namespace Emby.Server.Implementations.Library
isNew = true;
}
if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase))
if (viewType != item.ViewType)
{
item.ViewType = viewType;
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
@ -2859,7 +2855,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
File.WriteAllBytes(path, Array.Empty<byte>());
await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@ -2901,9 +2897,18 @@ namespace Emby.Server.Implementations.Library
var saveEntity = false;
var personEntity = GetPerson(person.Name);
// if PresentationUniqueKey is empty it's likely a new item.
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
if (personEntity is null)
{
var path = Person.GetPath(person.Name);
personEntity = new Person()
{
Name = person.Name,
Id = GetItemByNameId<Person>(path),
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
Path = path
};
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
}
@ -3136,7 +3141,7 @@ namespace Emby.Server.Implementations.Library
}
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
.Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(shortcut))

View File

@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey))
{
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try
{
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
catch
catch (Exception ex)
{
_logger.LogError(ex, "Error deserializing mediainfo cache");
}
finally
{
await jsonStream.DisposeAsync().ConfigureAwait(false);
}
}
@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
_logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
}

View File

@ -11,6 +11,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using EasyCaching.Core.Configurations;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
@ -186,11 +187,11 @@ namespace Emby.Server.Implementations.Library
{
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
if (item.MediaType == MediaType.Audio)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
else if (item.MediaType == MediaType.Video)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
@ -334,11 +335,11 @@ namespace Emby.Server.Implementations.Library
{
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
if (item.MediaType == MediaType.Audio)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
else if (item.MediaType == MediaType.Video)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
@ -417,9 +418,9 @@ namespace Emby.Server.Implementations.Library
public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
{
// Item would only be null if the app didn't supply ItemId as part of the live stream open request
var mediaType = item is null ? MediaType.Video : item.MediaType;
var mediaType = item?.MediaType ?? MediaType.Video;
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
if (mediaType == MediaType.Video)
{
var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item);
@ -428,7 +429,7 @@ namespace Emby.Server.Implementations.Library
SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
}
else if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
else if (mediaType == MediaType.Audio)
{
var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
@ -625,17 +626,19 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey))
{
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try
{
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
catch (Exception ex)
{
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
}
finally
{
await jsonStream.DisposeAsync().ConfigureAwait(false);
}
}
if (mediaInfo is null)
@ -664,8 +667,11 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
await using FileStream createStream = File.Create(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
FileStream createStream = File.Create(cacheFilePath);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}

View File

@ -10,11 +10,11 @@ using Emby.Naming.Audio;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library.Resolvers.Audio
@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public MultiItemResolverResult ResolveMultiple(
Folder parent,
List<FileSystemMetadata> files,
string collectionType,
CollectionType? collectionType,
IDirectoryService directoryService)
{
var result = ResolveMultipleInternal(parent, files, collectionType);
@ -59,9 +59,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
string collectionType)
CollectionType? collectionType)
{
if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
if (collectionType == CollectionType.Books)
{
return ResolveMultipleAudio(parent, files, true);
}
@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var collectionType = args.GetCollectionType();
var isBooksCollectionType = string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase);
var isBooksCollectionType = collectionType == CollectionType.Books;
if (args.IsDirectory)
{
@ -94,15 +94,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
{
var extension = Path.GetExtension(args.Path);
var extension = Path.GetExtension(args.Path.AsSpan());
if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase))
if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
{
// if audio file exists of same name, return null
return null;
}
var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
var isMixedCollectionType = collectionType is null;
// For conflicting extensions, give priority to videos
if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
MediaBrowser.Controller.Entities.Audio.Audio item = null;
var isMusicCollectionType = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
var isMusicCollectionType = collectionType == CollectionType.Music;
// Use regular audio type for mixed libraries, owned items and music
if (isMixedCollectionType ||
@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (item is not null)
{
item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
item.IsInMixedFolder = true;
}

View File

@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Audio;
using Emby.Naming.Common;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@ -54,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
protected override MusicAlbum Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
var isMusicMediaFolder = collectionType == CollectionType.Music;
// If there's a collection type and it's not music, don't allow it.
if (!isMusicMediaFolder)

View File

@ -4,6 +4,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Emby.Naming.Common;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var collectionType = args.GetCollectionType();
var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
var isMusicMediaFolder = collectionType == CollectionType.Music;
// If there's a collection type and it's not music, it can't be a music artist
if (!isMusicMediaFolder)

View File

@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return false;
}
return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
}
/// <summary>

View File

@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var collectionType = args.GetCollectionType();
// Only process items that are in a collection folder containing books
if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
if (collectionType != CollectionType.Books)
{
return null;
}
@ -32,9 +33,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
return GetBook(args);
}
var extension = Path.GetExtension(args.Path);
var extension = Path.GetExtension(args.Path.AsSpan());
if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's a book
return new Book
@ -51,12 +52,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
var bookFiles = args.FileSystemChildren.Where(f =>
{
var fileExtension = Path.GetExtension(f.FullName)
?? string.Empty;
var fileExtension = Path.GetExtension(f.FullName.AsSpan());
return _validExtensions.Contains(
fileExtension,
StringComparer.OrdinalIgnoreCase);
StringComparison.OrdinalIgnoreCase);
}).ToList();
// Don't return a Book if there is more (or less) than one document in the directory

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@ -28,13 +29,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
private readonly IImageProcessor _imageProcessor;
private string[] _validCollectionTypes = new[]
private static readonly CollectionType[] _validCollectionTypes = new[]
{
CollectionType.Movies,
CollectionType.HomeVideos,
CollectionType.MusicVideos,
CollectionType.TvShows,
CollectionType.Photos
CollectionType.Movies,
CollectionType.HomeVideos,
CollectionType.MusicVideos,
CollectionType.TvShows,
CollectionType.Photos
};
/// <summary>
@ -63,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
public MultiItemResolverResult ResolveMultiple(
Folder parent,
List<FileSystemMetadata> files,
string collectionType,
CollectionType? collectionType,
IDirectoryService directoryService)
{
var result = ResolveMultipleInternal(parent, files, collectionType);
@ -99,17 +100,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Video movie = null;
var files = args.GetActualFileSystemChildren().ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
if (collectionType == CollectionType.MusicVideos)
{
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
if (collectionType == CollectionType.HomeVideos)
{
movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
if (collectionType is null)
{
// Owned items will be caught by the video extra resolver
if (args.Parent is null)
@ -125,7 +126,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
if (collectionType == CollectionType.Movies)
{
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
@ -146,22 +147,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Video item = null;
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
if (collectionType == CollectionType.MusicVideos)
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
else if (collectionType == CollectionType.Movies)
{
item = ResolveVideo<Movie>(args, true);
}
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
else if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos)
{
item = ResolveVideo<Video>(args, false);
}
else if (string.IsNullOrEmpty(collectionType))
else if (collectionType is null)
{
if (args.HasParent<Series>())
{
@ -188,25 +188,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
string collectionType)
CollectionType? collectionType)
{
if (IsInvalid(parent, collectionType))
{
return null;
}
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
if (collectionType is CollectionType.MusicVideos)
{
return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos)
{
return ResolveVideos<Video>(parent, files, false, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
if (collectionType is null)
{
// Owned items should just use the plain video type
if (parent is null)
@ -222,12 +221,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return ResolveVideos<Movie>(parent, files, false, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
if (collectionType == CollectionType.Movies)
{
return ResolveVideos<Movie>(parent, files, true, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
if (collectionType == CollectionType.TvShows)
{
return ResolveVideos<Episode>(parent, files, false, collectionType, true);
}
@ -239,13 +238,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Folder parent,
IEnumerable<FileSystemMetadata> fileSystemEntries,
bool supportMultiEditions,
string collectionType,
CollectionType? collectionType,
bool parseName)
where T : Video, new()
{
var files = new List<FileSystemMetadata>();
var leftOver = new List<FileSystemMetadata>();
var hasCollectionType = !string.IsNullOrEmpty(collectionType);
var hasCollectionType = collectionType is not null;
// Loop through each child file/folder and see if we find a video
foreach (var child in fileSystemEntries)
@ -398,13 +397,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// Finds a movie based on a child file system entries.
/// </summary>
/// <returns>Movie.</returns>
private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName)
private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, CollectionType? collectionType, bool parseName)
where T : Video, new()
{
var multiDiscFolders = new List<FileSystemMetadata>();
var libraryOptions = args.LibraryOptions;
var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos;
var supportPhotos = collectionType == CollectionType.HomeVideos && libraryOptions.EnablePhotos;
var photos = new List<FileSystemMetadata>();
// Search for a folder rip
@ -460,8 +459,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult();
var isPhotosCollection = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)
|| string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase);
var isPhotosCollection = collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos;
if (!isPhotosCollection && result.Items.Count == 1)
{
var videoPath = result.Items[0].Path;
@ -562,7 +560,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return returnVideo;
}
private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType)
private bool IsInvalid(Folder parent, CollectionType? collectionType)
{
if (parent is not null)
{
@ -572,12 +570,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
if (collectionType.IsEmpty)
if (collectionType is null)
{
return false;
}
return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase);
return !_validCollectionTypes.Contains(collectionType.Value);
}
}
}

View File

@ -2,6 +2,7 @@
using System;
using Emby.Naming.Common;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@ -45,8 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection
var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
|| (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
if (collectionType == CollectionType.Photos
|| (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos))
{
if (HasPhotos(args))
{

View File

@ -1,11 +1,9 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@ -25,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
private static readonly string[] _ignoreFiles = new[]
{
"folder",
"thumb",
@ -56,22 +54,23 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Trailer.</returns>
protected override Photo Resolve(ItemResolveArgs args)
protected override Photo? Resolve(ItemResolveArgs args)
{
if (!args.IsDirectory)
{
// Must be an image file within a photo collection
var collectionType = args.CollectionType;
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
|| (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
if (collectionType == CollectionType.Photos
|| (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos))
{
if (IsImageFile(args.Path, _imageProcessor))
{
var filename = Path.GetFileNameWithoutExtension(args.Path);
var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
// Make sure the image doesn't belong to a video file
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
?? throw new InvalidOperationException("Path can't be a root directory."));
foreach (var file in files)
{
@ -92,32 +91,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename)
{
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
}
internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename)
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{
ArgumentNullException.ThrowIfNull(path);
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var filename = Path.GetFileNameWithoutExtension(path);
if (_ignoreFiles.Contains(filename))
if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
{
return false;
}
string extension = Path.GetExtension(path).TrimStart('.');
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
return true;
}
}
}

View File

@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
@ -19,9 +20,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
public class PlaylistResolver : GenericFolderResolver<Playlist>
{
private string[] _musicPlaylistCollectionTypes =
private CollectionType?[] _musicPlaylistCollectionTypes =
{
string.Empty,
null,
CollectionType.Music
};
@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Check if this is a music playlist file
// It should have the correct collection type and a supported file extension
else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType))
{
var extension = Path.GetExtension(args.Path.AsSpan());
if (Playlist.SupportedExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))

View File

@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
private string GetCollectionType(ItemResolveArgs args)
private CollectionType? GetCollectionType(ItemResolveArgs args)
{
return args.FileSystemChildren
.Where(i =>
@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
})
.Select(i => _fileSystem.GetFileNameWithoutExtension(i))
.FirstOrDefault();
.Select(i => Enum.TryParse<CollectionType>(i, out var collectionType) ? collectionType : (CollectionType?)null)
.FirstOrDefault(i => i is not null);
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using Emby.Naming.Common;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@ -48,9 +49,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders
if (season is not null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
if (season is not null
|| args.GetCollectionType() == CollectionType.TvShows
|| args.HasParent<Series>())
{
var episode = ResolveVideo<Episode>(args, false);

View File

@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var resolver = new Naming.TV.EpisodeResolver(namingOptions);
var folderName = System.IO.Path.GetFileName(path);
var testPath = "\\\\test\\" + folderName;
var testPath = @"\\test\" + folderName;
var episodeInfo = resolver.Resolve(testPath, true);

View File

@ -8,6 +8,7 @@ using System.IO;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Naming.Video;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
@ -59,11 +60,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
if (collectionType == CollectionType.TvShows)
{
// TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
var configuredContentType = args.GetConfiguredContentType();
if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
if (configuredContentType != CollectionType.TvShows)
{
return new Series
{
@ -72,7 +73,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
};
}
}
else if (string.IsNullOrEmpty(collectionType))
else if (collectionType is null)
{
if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
{

View File

@ -8,7 +8,6 @@ using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@ -64,8 +63,8 @@ namespace Emby.Server.Implementations.Library
var collectionFolder = folder as ICollectionFolder;
var folderViewType = collectionFolder?.CollectionType;
// Playlist library requires special handling because the folder only refrences user playlists
if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
// Playlist library requires special handling because the folder only references user playlists
if (folderViewType == CollectionType.Playlists)
{
var items = folder.GetItemList(new InternalItemsQuery(user)
{
@ -90,7 +89,7 @@ namespace Emby.Server.Implementations.Library
continue;
}
if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
if (query.PresetViews.Contains(folderViewType))
{
list.Add(GetUserView(folder, folderViewType, string.Empty));
}
@ -102,14 +101,14 @@ namespace Emby.Server.Implementations.Library
foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows })
{
var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(i.CollectionType))
var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null)
.ToList();
if (parents.Count > 0)
{
var localizationKey = string.Equals(viewType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ?
"TvShows" :
"Movies";
var localizationKey = viewType == CollectionType.TvShows
? "TvShows"
: "Movies";
list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews));
}
@ -164,14 +163,14 @@ namespace Emby.Server.Implementations.Library
.ToArray();
}
public UserView GetUserSubViewWithName(string name, Guid parentId, string type, string sortName)
public UserView GetUserSubViewWithName(string name, Guid parentId, CollectionType? type, string sortName)
{
var uniqueId = parentId + "subview" + type;
return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId);
}
public UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName)
public UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName)
{
var name = _localizationManager.GetLocalizedString(localizationKey);
@ -180,15 +179,15 @@ namespace Emby.Server.Implementations.Library
private Folder GetUserView(
List<ICollectionFolder> parents,
string viewType,
CollectionType? viewType,
string localizationKey,
string sortName,
User user,
string[] presetViews)
CollectionType?[] presetViews)
{
if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
if (parents.Count == 1 && parents.All(i => i.CollectionType == viewType))
{
if (!presetViews.Contains(viewType, StringComparison.OrdinalIgnoreCase))
if (!presetViews.Contains(viewType))
{
return (Folder)parents[0];
}
@ -200,7 +199,7 @@ namespace Emby.Server.Implementations.Library
return _libraryManager.GetNamedView(user, name, viewType, sortName);
}
public UserView GetUserView(Folder parent, string viewType, string sortName)
public UserView GetUserView(Folder parent, CollectionType? viewType, string sortName)
{
return _libraryManager.GetShadowView(parent, viewType, sortName);
}
@ -280,7 +279,7 @@ namespace Emby.Server.Implementations.Library
var isPlayed = request.IsPlayed;
if (parents.OfType<ICollectionFolder>().Any(i => string.Equals(i.CollectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase)))
if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.Music))
{
isPlayed = null;
}
@ -306,18 +305,18 @@ namespace Emby.Server.Implementations.Library
var hasCollectionType = parents.OfType<UserView>().ToArray();
if (hasCollectionType.Length > 0)
{
if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)))
if (hasCollectionType.All(i => i.CollectionType == CollectionType.Movies))
{
includeItemTypes = new[] { BaseItemKind.Movie };
}
else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)))
else if (hasCollectionType.All(i => i.CollectionType == CollectionType.TvShows))
{
includeItemTypes = new[] { BaseItemKind.Episode };
}
}
}
var mediaTypes = new List<string>();
var mediaTypes = new List<MediaType>();
if (includeItemTypes.Length == 0)
{

View File

@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
MediaTypes = new string[] { MediaType.Video },
MediaTypes = new[] { MediaType.Video },
IncludeItemTypes = new[] { BaseItemKind.Movie },
IsVirtualItem = false,
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },

View File

@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
await using (stream.ConfigureAwait(false))
{
var settings = new XmlWriterSettings
{
@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Async = true
};
await using (var writer = XmlWriter.Create(stream, settings))
var writer = XmlWriter.Create(stream, settings);
await using (writer.ConfigureAwait(false))
{
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
await using (stream.ConfigureAwait(false))
{
var settings = new XmlWriterSettings
{
@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var isSeriesEpisode = timer.IsProgramSeries;
await using (var writer = XmlWriter.Create(stream, settings))
var writer = XmlWriter.Create(stream, settings);
await using (writer.ConfigureAwait(false))
{
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
else
{
await writer.WriteStartElementAsync(null, "movie", null);
await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(item.Name))
{

View File

@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = JsonContent.Create(requestList, options: _jsonOptions);
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null)
{
return Array.Empty<ProgramInfo>();
@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (programDetails is null)
{
return Array.Empty<ProgramInfo>();
@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is not null)
{
foreach (HeadendsDto headend in root)
@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
}
catch (HttpRequestException ex)
@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token);
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is null)
{
return new List<ChannelInfo>();

View File

@ -207,7 +207,7 @@ namespace Emby.Server.Implementations.LiveTv
orderBy.Insert(0, (ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
}
if (!internalQuery.OrderBy.Any(i => string.Equals(i.OrderBy, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)))
if (internalQuery.OrderBy.All(i => i.OrderBy != ItemSortBy.SortName))
{
orderBy.Add((ItemSortBy.SortName, SortOrder.Ascending));
}

View File

@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -16,21 +17,20 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public abstract class BaseTunerHost
{
private readonly IMemoryCache _memoryCache;
private readonly ConcurrentDictionary<string, List<ChannelInfo>> _cache;
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem)
{
Config = config;
Logger = logger;
_memoryCache = memoryCache;
FileSystem = fileSystem;
_cache = new ConcurrentDictionary<string, List<ChannelInfo>>();
}
protected IServerConfigurationManager Config { get; }
@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var key = tuner.Id;
if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache))
if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List<ChannelInfo> cache))
{
return cache;
}
@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!string.IsNullOrEmpty(key) && list.Count > 0)
{
_memoryCache.Set(key, list);
_cache[key] = list;
}
return list;

View File

@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@ -27,7 +28,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@ -50,9 +50,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
IStreamHelper streamHelper,
IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
IStreamHelper streamHelper)
: base(config, logger, fileSystem)
{
_httpClientFactory = httpClientFactory;
_appHost = appHost;
@ -77,13 +76,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>();
if (info.ImportFavoritesOnly)
{
lineup = lineup.Where(i => i.Favorite).ToList();
lineup = lineup.Where(i => i.Favorite);
}
return lineup.Where(i => !i.DRM).ToList();
@ -130,9 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
.GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false);
var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey))
{
@ -176,34 +170,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List<LiveTvTunerInfo>();
await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
string stripedLine = StripXML(line);
if (stripedLine.Contains("Channel", StringComparison.Ordinal))
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
{
LiveTvTunerStatus status;
var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
var name = stripedLine.Substring(0, index - 1);
var currentChannel = stripedLine.Substring(index + 7);
if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
string stripedLine = StripXML(line);
if (stripedLine.Contains("Channel", StringComparison.Ordinal))
{
status = LiveTvTunerStatus.LiveTv;
}
else
{
status = LiveTvTunerStatus.Available;
}
LiveTvTunerStatus status;
var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
var name = stripedLine.Substring(0, index - 1);
var currentChannel = stripedLine.Substring(index + 7);
if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
{
status = LiveTvTunerStatus.LiveTv;
}
else
{
status = LiveTvTunerStatus.Available;
}
tuners.Add(new LiveTvTunerInfo
{
Name = name,
SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
ProgramName = currentChannel,
Status = status
});
tuners.Add(new LiveTvTunerInfo
{
Name = name,
SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
ProgramName = currentChannel,
Status = status
});
}
}
}

View File

@ -44,8 +44,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult();
}
}
GC.SuppressFinalize(this);
}
public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)

View File

@ -84,15 +84,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.CompletedTask;
}
public Task Close()
public async Task Close()
{
EnableStreamSharing = false;
Logger.LogInformation("Closing {Type}", GetType().Name);
LiveStreamCancellationTokenSource.Cancel();
return Task.CompletedTask;
await LiveStreamCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
public Stream GetStream()

View File

@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@ -54,9 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
INetworkManager networkManager,
IStreamHelper streamHelper,
IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
IStreamHelper streamHelper)
: base(config, logger, fileSystem)
{
_httpClientFactory = httpClientFactory;
_appHost = appHost;

View File

@ -94,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
if (string.IsNullOrWhiteSpace(channel.Id))
{
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
else
{
channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
channel.Path = trimmedLine;
channels.Add(channel);

View File

@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Haal keyframes vanuit video lêers om meer presiese HLS afspeellyste te maak. Dit kan lank duur.",
"TaskKeyframeExtractor": "Keyframe Ekstraktor",
"External": "Ekstern",
"HearingImpaired": "gehoorgestremd"
"HearingImpaired": "gehoorgestremd",
"TaskRefreshTrickplayImages": "Genereer Fopspeel Beelde",
"TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling."
}

View File

@ -16,7 +16,7 @@
"Folders": "المجلدات",
"Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم",
"HeaderContinueWatching": "أستئناف المشاهدة",
"HeaderContinueWatching": "استئناف المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة",

View File

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

View File

@ -123,5 +123,7 @@
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
"TaskRefreshChannels": "Абнавіць каналы",
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу."
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.",
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках."
}

View File

@ -124,5 +124,6 @@
"TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.",
"TaskKeyframeExtractor": "Извличане на ключови кадри",
"External": "Външен",
"HearingImpaired": "Увреден слух"
"HearingImpaired": "Увреден слух",
"TaskRefreshTrickplayImages": "Генерирай изображение"
}

View File

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

View File

@ -22,7 +22,7 @@
"HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály",
"HeaderFavoriteSongs": "Oblíbená hudba",
"HeaderLiveTV": "Televize",
"HeaderLiveTV": "Živý přenos",
"HeaderNextUp": "Další díly",
"HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domácí videa",
@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.",
"TaskKeyframeExtractor": "Vytahovač klíčových snímků",
"External": "Externí",
"HearingImpaired": "Sluchově postižení"
"HearingImpaired": "Sluchově postižení",
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
"TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno."
}

View File

@ -15,13 +15,13 @@
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albums kunstnere",
"HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
"HeaderFavoriteAlbums": "Favorit albummer",
"HeaderFavoriteArtists": "Favorit kunstnere",
"HeaderFavoriteEpisodes": "Favorit afsnit",
"HeaderFavoriteShows": "Favorit serier",
"HeaderFavoriteSongs": "Favorit sange",
"HeaderFavoriteAlbums": "Favoritalbummer",
"HeaderFavoriteArtists": "Favoritkunstnere",
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
"HeaderFavoriteShows": "Yndlingsserier",
"HeaderFavoriteSongs": "Yndlingssange",
"HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper",
@ -34,8 +34,8 @@
"Latest": "Seneste",
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
"MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
"MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
"MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
"MixedContent": "Blandet indhold",
"Movies": "Film",
"Music": "Musik",
@ -51,7 +51,7 @@
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
"NotificationOptionInstallationFailed": "Installationen mislykkedes",
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
"NotificationOptionPluginError": "Plugin fejl",
"NotificationOptionPluginError": "Plugin-fejl",
"NotificationOptionPluginInstalled": "Plugin blev installeret",
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
@ -92,26 +92,26 @@
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins",
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
"TaskCleanLogs": "Ryd Log mappe",
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
"TaskRefreshLibrary": "Scan Medie Bibliotek",
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
"TaskCleanCache": "Ryd Cache mappe",
"TasksChannelsCategory": "Internet Kanaler",
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
"TaskCleanLogs": "Ryd Log-mappe",
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
"TaskRefreshLibrary": "Scan Mediebibliotek",
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
"TaskCleanCache": "Ryd Cache-mappe",
"TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedligeholdelse",
"TaskRefreshChapterImages": "Udtræk kapitel billeder",
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
"TaskRefreshChannelsDescription": "Opdater internet kanal information.",
"TaskRefreshChapterImages": "Udtræk kapitelbilleder",
"TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
"TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
"TaskRefreshChannels": "Opdater Kanaler",
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
"TaskCleanTranscode": "Tøm Transcode mappen",
"TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
"TaskCleanTranscode": "Tøm Transcode-mappen",
"TaskRefreshPeople": "Opdater Personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
@ -121,8 +121,8 @@
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
"TaskOptimizeDatabase": "Optimér database",
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
"TaskKeyframeExtractor": "Nøglebillede udtræk",
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
"TaskKeyframeExtractor": "Udtræk af nøglebillede",
"External": "Ekstern",
"HearingImpaired": "Hørehæmmet"
}

View File

@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
"TaskKeyframeExtractor": "Keyframe Extraktor",
"External": "Extern",
"HearingImpaired": "Hörgeschädigt"
"HearingImpaired": "Hörgeschädigt",
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
"TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken."
}

View File

@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.",
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
"External": "Εξωτερικό",
"HearingImpaired": "Με προβλήματα ακοής"
"HearingImpaired": "Με προβλήματα ακοής",
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες."
}

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