mirror of https://github.com/jellyfin/jellyfin.git
Merge branch 'master' into master
This commit is contained in:
commit
6f26d2cd34
|
@ -7,7 +7,7 @@ parameters:
|
|||
default: "ubuntu-latest"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 7.0.x
|
||||
default: 8.0.x
|
||||
|
||||
jobs:
|
||||
- job: CompatibilityCheck
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -47,7 +47,7 @@ jobs:
|
|||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
|
||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) --label "org.opencontainers.image.url=$(Build.Repository.Uri)" --label "org.opencontainers.image.revision=$(Build.SourceVersion)" deployment'
|
||||
displayName: 'Build Dockerfile'
|
||||
|
||||
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
|
||||
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.0",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,9 +30,9 @@ body:
|
|||
label: Jellyfin Version
|
||||
description: What version of Jellyfin are you running?
|
||||
options:
|
||||
- 10.8.0
|
||||
- 10.8.z
|
||||
- 10.8.9
|
||||
- 10.7.7
|
||||
- 10.7.z
|
||||
- 10.6.4
|
||||
- Other
|
||||
validations:
|
||||
|
@ -47,13 +47,15 @@ body:
|
|||
label: Environment
|
||||
description: |
|
||||
Examples:
|
||||
- **OS**: [e.g. Debian, Windows]
|
||||
- **OS**: [e.g. Debian 11, Windows 10]
|
||||
- **Linux Kernel**: [e.g. none, 5.15, 6.1, etc.]
|
||||
- **Virtualization**: [e.g. Docker, KVM, LXC]
|
||||
- **Clients**: [Browser, Android, Fire Stick, etc.]
|
||||
- **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
|
||||
- **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
|
||||
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
|
||||
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
|
||||
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
|
||||
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
|
||||
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
|
||||
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
||||
- **Base URL**: [e.g. none, yes: /example]
|
||||
|
@ -61,12 +63,14 @@ body:
|
|||
- **Storage**: [e.g. local, NFS, cloud]
|
||||
value: |
|
||||
- OS:
|
||||
- Linux Kernel:
|
||||
- Virtualization:
|
||||
- Clients:
|
||||
- Browser:
|
||||
- FFmpeg Version:
|
||||
- Playback Method:
|
||||
- Hardware Acceleration:
|
||||
- GPU Model:
|
||||
- Plugins:
|
||||
- Reverse Proxy:
|
||||
- Base URL:
|
||||
|
@ -84,8 +88,8 @@ body:
|
|||
id: ffmpeg-logs
|
||||
attributes:
|
||||
label: FFmpeg logs
|
||||
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||
placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
|
||||
description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log.
|
||||
placeholder: This field is mandatory for debugging hardware transcoding issues. It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: browserlogs
|
||||
|
|
|
@ -20,18 +20,18 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
|
||||
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@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
|
||||
uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
|
||||
uses: github/codeql-action/autobuild@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
|
||||
uses: github/codeql-action/analyze@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
|
|
@ -14,23 +14,23 @@ jobs:
|
|||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
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@607fce577a46308457984d59e4954e075820f10a # tag=v3
|
||||
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
|
||||
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,30 +39,32 @@ jobs:
|
|||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
- name: Checkout common ancestor
|
||||
env:
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
run: |
|
||||
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
|
||||
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
|
||||
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
|
||||
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
|
||||
git checkout --progress --force $ANCESTOR_REF
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
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
|
||||
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:
|
||||
|
@ -76,12 +78,12 @@ jobs:
|
|||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
|
@ -103,14 +105,14 @@ jobs:
|
|||
body="${body//$'\r'/'%0D'}"
|
||||
echo ::set-output name=body::$body
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@85a676a52594b4481e0532825a2d8906ef96dac2 # v2
|
||||
uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
direction: last
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
if: ${{ steps.read-diff.outputs.body != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
@ -125,7 +127,7 @@ jobs:
|
|||
|
||||
</details>
|
||||
- name: Edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
|
@ -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"
|
|
@ -17,14 +17,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@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@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
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@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
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@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
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@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
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@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
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@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
if: ${{ github.event.comment != null && failure() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
|
|
@ -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.
|
|
@ -1,4 +1,4 @@
|
|||
name: Automation
|
||||
name: Project Automation
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -9,18 +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'
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
project:
|
||||
name: Project board
|
||||
runs-on: ubuntu-latest
|
|
@ -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 }}
|
|
@ -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.
|
|
@ -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 }}
|
|
@ -1,30 +0,0 @@
|
|||
name: Issue Stale Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
|
||||
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).
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
@ -126,6 +128,7 @@
|
|||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||
- [tbraeutigam](https://github.com/tbraeutigam)
|
||||
- [teacupx](https://github.com/teacupx)
|
||||
- [TelepathicWalrus](https://github.com/TelepathicWalrus)
|
||||
- [Terror-Gene](https://github.com/Terror-Gene)
|
||||
- [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
|
||||
- [ThibaultNocchi](https://github.com/ThibaultNocchi)
|
||||
|
@ -163,6 +166,14 @@
|
|||
- [vgambier](https://github.com/vgambier)
|
||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||
- [ipitio](https://github.com/ipitio)
|
||||
- [TheTyrius](https://github.com/TheTyrius)
|
||||
- [tallbl0nde](https://github.com/tallbl0nde)
|
||||
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||
- [scampower3](https://github.com/scampower3)
|
||||
- [Chris-Codes-It] (https://github.com/Chris-Codes-It)
|
||||
- [Pithaya](https://github.com/Pithaya)
|
||||
- [Çağrı Sakaoğlu](https://github.com/ilovepilav)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
@ -233,3 +244,4 @@
|
|||
- [Jakob Kukla](https://github.com/jakobkukla)
|
||||
- [Utku Özdemir](https://github.com/utkuozdemir)
|
||||
- [JPUC1143](https://github.com/Jpuc1143/)
|
||||
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
||||
|
|
|
@ -2,89 +2,89 @@
|
|||
<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.17.0" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.17.0" />
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.0" />
|
||||
<PackageVersion Include="BDInfo" Version="0.7.6.2" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.2.0" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.3.0" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.14" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.18" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="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.10" />
|
||||
<PackageVersion Include="LrcParser" Version="2022.529.1" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
|
||||
<PackageVersion Include="libse" Version="3.6.13" />
|
||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||
<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="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
|
||||
<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="MimeTypes" Version="2.4.0" />
|
||||
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="NEbml" Version="0.11.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageVersion Include="PlaylistsNET" Version="1.3.1" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="PlaylistsNET" Version="1.4.0" />
|
||||
<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.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.1.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
||||
<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="2.3.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.0.1" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
|
||||
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
|
||||
<PackageVersion Include="SkiaSharp" Version="2.88.3" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||
<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.4" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
|
||||
<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.2" />
|
||||
<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.4.5" />
|
||||
<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>
|
||||
|
|
|
@ -2,14 +2,15 @@
|
|||
#####################################
|
||||
# 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 - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM debian:stable-slim as app
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
#####################################
|
||||
# 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 - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-arm as qemu
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
#####################################
|
||||
# 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 - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace Emby.Dlna.Configuration
|
|||
BlastAliveMessages = true;
|
||||
SendOnlyMatchedHost = true;
|
||||
ClientDiscoveryIntervalSeconds = 60;
|
||||
AliveMessageIntervalSeconds = 1800;
|
||||
AliveMessageIntervalSeconds = 180;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace Emby.Dlna.ConnectionManager
|
|||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
||||
private static IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
var list = new List<StateVariable>
|
||||
return new StateVariable[]
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
|
@ -114,8 +114,6 @@ namespace Emby.Dlna.ConnectionManager
|
|||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
||||
private static IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
var list = new List<StateVariable>
|
||||
return new StateVariable[]
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
|
@ -154,8 +154,6 @@ namespace Emby.Dlna.ContentDirectory
|
|||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -10,6 +8,7 @@ using System.Text;
|
|||
using System.Xml;
|
||||
using Emby.Dlna.ContentDirectory;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -44,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;
|
||||
|
@ -55,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,
|
||||
|
@ -84,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
|
||||
{
|
||||
|
@ -139,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);
|
||||
|
||||
|
@ -175,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,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)
|
||||
{
|
||||
|
@ -202,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;
|
||||
|
@ -314,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));
|
||||
}
|
||||
|
@ -409,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)
|
||||
{
|
||||
|
@ -451,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;
|
||||
|
||||
|
@ -529,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);
|
||||
|
||||
|
@ -543,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));
|
||||
}
|
||||
|
@ -633,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);
|
||||
|
||||
|
@ -677,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))
|
||||
|
@ -694,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;
|
||||
}
|
||||
|
@ -716,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
|
||||
|
@ -794,7 +794,7 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
if (item.IsDisplayedAsFolder || stubType.HasValue)
|
||||
{
|
||||
string classType = null;
|
||||
string? classType = null;
|
||||
|
||||
if (!_profile.RequiresPlainFolders)
|
||||
{
|
||||
|
@ -822,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)
|
||||
{
|
||||
|
@ -870,11 +870,11 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
var types = new[]
|
||||
{
|
||||
PersonType.Director,
|
||||
PersonType.Writer,
|
||||
PersonType.Producer,
|
||||
PersonType.Composer,
|
||||
"creator"
|
||||
PersonKind.Director,
|
||||
PersonKind.Writer,
|
||||
PersonKind.Producer,
|
||||
PersonKind.Composer,
|
||||
PersonKind.Creator
|
||||
};
|
||||
|
||||
// Seeing some LG models locking up due content with large lists of people
|
||||
|
@ -888,14 +888,17 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
foreach (var actor in people)
|
||||
{
|
||||
var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
|
||||
?? PersonType.Actor;
|
||||
var type = types.FirstOrDefault(i => i == actor.Type || string.Equals(actor.Role, i.ToString(), StringComparison.OrdinalIgnoreCase));
|
||||
if (type == PersonKind.Unknown)
|
||||
{
|
||||
type = PersonKind.Actor;
|
||||
}
|
||||
|
||||
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
|
||||
AddValue(writer, "upnp", type.ToString().ToLowerInvariant(), actor.Name, NsUpnp);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
@ -971,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)
|
||||
{
|
||||
|
@ -1004,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)
|
||||
{
|
||||
|
@ -1014,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");
|
||||
|
@ -1069,7 +1071,7 @@ namespace Emby.Dlna.Didl
|
|||
writer.WriteFullEndElement();
|
||||
}
|
||||
|
||||
private ImageDownloadInfo GetImageInfo(BaseItem item)
|
||||
private ImageDownloadInfo? GetImageInfo(BaseItem item)
|
||||
{
|
||||
if (item.HasImage(ImageType.Primary))
|
||||
{
|
||||
|
@ -1114,7 +1116,7 @@ namespace Emby.Dlna.Didl
|
|||
return null;
|
||||
}
|
||||
|
||||
private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
|
||||
private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item)
|
||||
{
|
||||
if (item is null)
|
||||
{
|
||||
|
@ -1144,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
|
||||
{
|
||||
|
@ -1246,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; }
|
||||
|
||||
|
@ -1256,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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -164,7 +164,7 @@ namespace Emby.Dlna.Eventing
|
|||
|
||||
try
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
|
||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
}
|
|
@ -1,475 +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.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.PlayTo;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using Jellyfin.Networking.Manager;
|
||||
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 (!options.BlastAliveMessages)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_publisher is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_publisher = new SsdpDevicePublisher(
|
||||
_communicationsServer,
|
||||
Environment.OSVersion.Platform.ToString(),
|
||||
Environment.OSVersion.VersionString,
|
||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
{
|
||||
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
|
||||
SupportPnpRootDevice = false
|
||||
};
|
||||
|
||||
RegisterServerEndpoints();
|
||||
|
||||
_publisher.StartBroadcastingAliveMessages(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";
|
||||
|
||||
var bindAddresses = NetworkManager.CreateCollection(
|
||||
_networkManager.GetInternalBindAddresses()
|
||||
.Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)));
|
||||
|
||||
if (bindAddresses.Count == 0)
|
||||
{
|
||||
// No interfaces returned, so use loopback.
|
||||
bindAddresses = _networkManager.GetLoopbacks();
|
||||
}
|
||||
|
||||
foreach (IPNetAddress address in bindAddresses)
|
||||
{
|
||||
if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
// Not supporting IPv6 right now
|
||||
continue;
|
||||
}
|
||||
|
||||
// Limit to LAN addresses only
|
||||
if (!_networkManager.IsInLocalNetwork(address))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
||||
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(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 = address.Address,
|
||||
PrefixLength = address.PrefixLength,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
|
@ -15,7 +17,10 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class DlnaHttpClient
|
||||
/// <summary>
|
||||
/// Http client for Dlna PlayTo function.
|
||||
/// </summary>
|
||||
public partial class DlnaHttpClient
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
@ -26,6 +31,9 @@ namespace Emby.Dlna.PlayTo
|
|||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[GeneratedRegex("(&(?![a-z]*;))")]
|
||||
private static partial Regex EscapeAmpersandRegex();
|
||||
|
||||
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||
{
|
||||
// If it's already a complete url, don't stick anything onto the front of it
|
||||
|
@ -44,25 +52,46 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
|
||||
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse response");
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException)
|
||||
{
|
||||
// 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, "&");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -10,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;
|
||||
|
@ -44,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;
|
||||
|
@ -61,12 +60,13 @@ namespace Emby.Dlna.PlayTo
|
|||
IUserManager userManager,
|
||||
IImageProcessor imageProcessor,
|
||||
string serverAddress,
|
||||
string accessToken,
|
||||
string? accessToken,
|
||||
IDeviceDiscovery deviceDiscovery,
|
||||
IUserDataManager userDataManager,
|
||||
ILocalizationManager localization,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IMediaEncoder mediaEncoder)
|
||||
IMediaEncoder mediaEncoder,
|
||||
Device device)
|
||||
{
|
||||
_session = session;
|
||||
_sessionManager = sessionManager;
|
||||
|
@ -82,14 +82,7 @@ namespace Emby.Dlna.PlayTo
|
|||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
public bool IsSessionActive => !_disposed && _device is not null;
|
||||
|
||||
public bool SupportsMediaControl => IsSessionActive;
|
||||
|
||||
public void Init(Device device)
|
||||
{
|
||||
_device = device;
|
||||
_device.OnDeviceUnavailable = OnDeviceUnavailable;
|
||||
_device.PlaybackStart += OnDevicePlaybackStart;
|
||||
|
@ -102,6 +95,10 @@ namespace Emby.Dlna.PlayTo
|
|||
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
||||
}
|
||||
|
||||
public bool IsSessionActive => !_disposed;
|
||||
|
||||
public bool SupportsMediaControl => IsSessionActive;
|
||||
|
||||
/*
|
||||
* Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||
*/
|
||||
|
@ -131,22 +128,22 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
var info = e.Argument;
|
||||
|
||||
if (!_disposed
|
||||
&& info.Headers.TryGetValue("USN", out string usn)
|
||||
&& info.Headers.TryGetValue("USN", out string? usn)
|
||||
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| (info.Headers.TryGetValue("NT", out string nt)
|
||||
|| (info.Headers.TryGetValue("NT", out string? nt)
|
||||
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
|
||||
{
|
||||
OnDeviceUnavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
||||
private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
|
||||
{
|
||||
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
|
||||
{
|
||||
|
@ -188,7 +185,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e)
|
||||
private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
|
@ -257,7 +254,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e)
|
||||
private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
|
@ -281,7 +278,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e)
|
||||
private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
|
@ -486,9 +483,9 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
private PlaylistItem CreatePlaylistItem(
|
||||
BaseItem item,
|
||||
User user,
|
||||
User? user,
|
||||
long startPostionTicks,
|
||||
string mediaSourceId,
|
||||
string? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex)
|
||||
{
|
||||
|
@ -525,7 +522,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return playlistItem;
|
||||
}
|
||||
|
||||
private string GetDlnaHeaders(PlaylistItem item)
|
||||
private string? GetDlnaHeaders(PlaylistItem item)
|
||||
{
|
||||
var profile = item.Profile;
|
||||
var streamInfo = item.StreamInfo;
|
||||
|
@ -579,9 +576,9 @@ namespace Emby.Dlna.PlayTo
|
|||
return null;
|
||||
}
|
||||
|
||||
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
||||
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
||||
{
|
||||
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
||||
if (item.MediaType == MediaType.Video)
|
||||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
|
@ -601,7 +598,7 @@ namespace Emby.Dlna.PlayTo
|
|||
};
|
||||
}
|
||||
|
||||
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
||||
if (item.MediaType == MediaType.Audio)
|
||||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
|
@ -619,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);
|
||||
}
|
||||
|
@ -687,17 +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;
|
||||
_device = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
|
@ -716,7 +711,7 @@ namespace Emby.Dlna.PlayTo
|
|||
case GeneralCommandType.ToggleMute:
|
||||
return _device.ToggleMute(cancellationToken);
|
||||
case GeneralCommandType.SetAudioStreamIndex:
|
||||
if (command.Arguments.TryGetValue("Index", out string index))
|
||||
if (command.Arguments.TryGetValue("Index", out string? index))
|
||||
{
|
||||
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
|
@ -740,7 +735,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
|
||||
case GeneralCommandType.SetVolume:
|
||||
if (command.Arguments.TryGetValue("Volume", out string vol))
|
||||
if (command.Arguments.TryGetValue("Volume", out string? vol))
|
||||
{
|
||||
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
|
||||
{
|
||||
|
@ -865,34 +860,19 @@ namespace Emby.Dlna.PlayTo
|
|||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
|
||||
if (_device is null)
|
||||
return name switch
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (name == SessionMessageType.Play)
|
||||
{
|
||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (name == SessionMessageType.Playstate)
|
||||
{
|
||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (name == SessionMessageType.GeneralCommand)
|
||||
{
|
||||
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
|
||||
}
|
||||
|
||||
// Not supported or needed right now
|
||||
return Task.CompletedTask;
|
||||
SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
|
||||
SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
|
||||
SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
|
||||
_ => Task.CompletedTask // Not supported or needed right now
|
||||
};
|
||||
}
|
||||
|
||||
private class StreamParams
|
||||
{
|
||||
private MediaSourceInfo _mediaSource;
|
||||
private IMediaSourceManager _mediaSourceManager;
|
||||
private MediaSourceInfo? _mediaSource;
|
||||
private IMediaSourceManager? _mediaSourceManager;
|
||||
|
||||
public Guid ItemId { get; set; }
|
||||
|
||||
|
@ -904,17 +884,17 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
public string DeviceProfileId { get; set; }
|
||||
public string? DeviceProfileId { get; set; }
|
||||
|
||||
public string DeviceId { get; set; }
|
||||
public string? DeviceId { get; set; }
|
||||
|
||||
public string MediaSourceId { get; set; }
|
||||
public string? MediaSourceId { get; set; }
|
||||
|
||||
public string LiveStreamId { get; set; }
|
||||
public string? LiveStreamId { get; set; }
|
||||
|
||||
public BaseItem Item { get; set; }
|
||||
public BaseItem? Item { get; set; }
|
||||
|
||||
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
|
||||
public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_mediaSource is not null)
|
||||
{
|
||||
|
@ -944,8 +924,8 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (Guid.TryParse(parts[i + 1], out var result))
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -189,7 +187,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress);
|
||||
|
||||
controller = new PlayToController(
|
||||
sessionInfo,
|
||||
|
@ -205,12 +203,11 @@ namespace Emby.Dlna.PlayTo
|
|||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_mediaEncoder);
|
||||
_mediaEncoder,
|
||||
device);
|
||||
|
||||
sessionInfo.AddController(controller);
|
||||
|
||||
controller.Init(device);
|
||||
|
||||
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
|
||||
_dlnaManager.GetDefaultProfile();
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "")
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
|
@ -137,10 +137,10 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary)
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
|
@ -150,9 +150,9 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else if (dictionary.ContainsKey(arg.Name))
|
||||
else if (dictionary.TryGetValue(arg.Name, out var argValue))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
|
||||
stateString += BuildArgumentXml(arg, argValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -160,7 +160,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -73,7 +73,11 @@ namespace Emby.Dlna.Ssdp
|
|||
{
|
||||
if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null)
|
||||
{
|
||||
_deviceLocator = new SsdpDeviceLocator(_commsServer);
|
||||
_deviceLocator = new SsdpDeviceLocator(
|
||||
_commsServer,
|
||||
Environment.OSVersion.Platform.ToString(),
|
||||
// Can not use VersionString here since that includes OS and version
|
||||
Environment.OSVersion.Version.ToString());
|
||||
|
||||
// (Optional) Set the filter so we only see notifications for devices we care about
|
||||
// (can be any search target value i.e device type, uuid value etc - any value that appears in the
|
||||
|
@ -106,7 +110,7 @@ namespace Emby.Dlna.Ssdp
|
|||
{
|
||||
Location = e.DiscoveredDevice.DescriptionLocation,
|
||||
Headers = headers,
|
||||
RemoteIpAddress = e.RemoteIpAddress
|
||||
RemoteIPAddress = e.RemoteIPAddress
|
||||
});
|
||||
|
||||
DeviceDiscoveredInternal?.Invoke(this, args);
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace Emby.Naming.Audio
|
|||
/// <summary>
|
||||
/// Helper class to determine if Album is multipart.
|
||||
/// </summary>
|
||||
public class AlbumParser
|
||||
public partial class AlbumParser
|
||||
{
|
||||
private readonly NamingOptions _options;
|
||||
|
||||
|
@ -23,6 +23,9 @@ namespace Emby.Naming.Audio
|
|||
_options = options;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"[-\.\(\)\s]+")]
|
||||
private static partial Regex CleanRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Function that determines if album is multipart.
|
||||
/// </summary>
|
||||
|
@ -42,13 +45,9 @@ namespace Emby.Naming.Audio
|
|||
|
||||
// Normalize
|
||||
// Remove whitespace
|
||||
filename = filename.Replace('-', ' ');
|
||||
filename = filename.Replace('.', ' ');
|
||||
filename = filename.Replace('(', ' ');
|
||||
filename = filename.Replace(')', ' ');
|
||||
filename = Regex.Replace(filename, @"\s+", " ");
|
||||
filename = CleanRegex().Replace(filename, " ");
|
||||
|
||||
ReadOnlySpan<char> trimmedFilename = filename.TrimStart();
|
||||
ReadOnlySpan<char> trimmedFilename = filename.AsSpan().TrimStart();
|
||||
|
||||
foreach (var prefix in _options.AlbumStackingPrefixes)
|
||||
{
|
||||
|
|
|
@ -79,25 +79,25 @@ namespace Emby.Naming.AudioBook
|
|||
{
|
||||
if (group.Count() > 1 || haveChaptersOrPages)
|
||||
{
|
||||
var ex = new List<AudioBookFileInfo>();
|
||||
var alt = new List<AudioBookFileInfo>();
|
||||
List<AudioBookFileInfo>? ex = null;
|
||||
List<AudioBookFileInfo>? alt = null;
|
||||
|
||||
foreach (var audioFile in group)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
|
||||
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
|
||||
var name = Path.GetFileNameWithoutExtension(audioFile.Path.AsSpan());
|
||||
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
alt.Add(audioFile);
|
||||
(alt ??= new()).Add(audioFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
ex.Add(audioFile);
|
||||
(ex ??= new()).Add(audioFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (ex.Count > 0)
|
||||
if (ex is not null)
|
||||
{
|
||||
var extra = ex
|
||||
.OrderBy(x => x.Container)
|
||||
|
@ -108,7 +108,7 @@ namespace Emby.Naming.AudioBook
|
|||
extras.AddRange(extra);
|
||||
}
|
||||
|
||||
if (alt.Count > 0)
|
||||
if (alt is not null)
|
||||
{
|
||||
var alternatives = alt
|
||||
.OrderBy(x => x.Container)
|
||||
|
|
|
@ -141,8 +141,7 @@ namespace Emby.Naming.Common
|
|||
VideoFileStackingRules = new[]
|
||||
{
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
|
||||
};
|
||||
|
||||
CleanDateTimes = new[]
|
||||
|
@ -157,7 +156,8 @@ namespace Emby.Naming.Common
|
|||
@"^(?<cleaned>.+?)(\[.*\])",
|
||||
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
||||
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
||||
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
|
||||
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
|
||||
@"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
|
||||
};
|
||||
|
||||
SubtitleFileExtensions = new[]
|
||||
|
@ -270,7 +270,6 @@ namespace Emby.Naming.Common
|
|||
".sfx",
|
||||
".shn",
|
||||
".sid",
|
||||
".spc",
|
||||
".stm",
|
||||
".strm",
|
||||
".ult",
|
||||
|
@ -319,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"
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -375,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
|
||||
},
|
||||
|
@ -416,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?[^\\\/]*$")
|
||||
|
@ -711,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.
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
|
@ -41,12 +41,12 @@
|
|||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
|
|
|
@ -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)))
|
||||
{
|
||||
|
|
|
@ -7,14 +7,15 @@ namespace Emby.Naming.TV
|
|||
/// <summary>
|
||||
/// Used to resolve information about series from path.
|
||||
/// </summary>
|
||||
public static class SeriesResolver
|
||||
public static partial class SeriesResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Regex that matches strings of at least 2 characters separated by a dot or underscore.
|
||||
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
|
||||
/// preserving namings like "S.H.O.W".
|
||||
/// </summary>
|
||||
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled);
|
||||
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
|
||||
private static partial Regex SeriesNameRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Resolve information about series from path.
|
||||
|
@ -37,7 +38,7 @@ namespace Emby.Naming.TV
|
|||
|
||||
if (!string.IsNullOrEmpty(seriesName))
|
||||
{
|
||||
seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
|
||||
seriesName = SeriesNameRegex().Replace(seriesName, "${a} ${b}").Trim();
|
||||
}
|
||||
|
||||
return new SeriesInfo(path)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -12,9 +12,13 @@ namespace Emby.Naming.Video
|
|||
/// <summary>
|
||||
/// Resolves alternative versions and extras from list of video files.
|
||||
/// </summary>
|
||||
public static class VideoListResolver
|
||||
public static partial class VideoListResolver
|
||||
{
|
||||
private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
[GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex ResolutionRegex();
|
||||
|
||||
[GeneratedRegex(@"^\[([^]]*)\]")]
|
||||
private static partial Regex CheckMultiVersionRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Resolves alternative versions and extras from list of video files.
|
||||
|
@ -131,7 +135,7 @@ namespace Emby.Naming.Video
|
|||
|
||||
if (videos.Count > 1)
|
||||
{
|
||||
var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
|
||||
var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
|
||||
videos.Clear();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
|
@ -201,7 +205,7 @@ namespace Emby.Naming.Video
|
|||
// The CleanStringParser should have removed common keywords etc.
|
||||
return testFilename.IsEmpty
|
||||
|| testFilename[0] == '-'
|
||||
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
||||
|| CheckMultiVersionRegex().IsMatch(testFilename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,8 +87,7 @@ namespace Emby.Naming.Video
|
|||
name = cleanDateTimeResult.Name;
|
||||
year = cleanDateTimeResult.Year;
|
||||
|
||||
if (extraResult.ExtraType is null
|
||||
&& TryCleanString(name, namingOptions, out var newName))
|
||||
if (TryCleanString(name, namingOptions, out var newName))
|
||||
{
|
||||
name = newName;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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%";
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
@ -45,7 +42,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;
|
||||
|
@ -60,7 +56,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;
|
||||
|
@ -82,11 +77,12 @@ using MediaBrowser.Controller.Subtitles;
|
|||
using MediaBrowser.Controller.SyncPlay;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.LocalMetadata.Savers;
|
||||
using MediaBrowser.MediaEncoding.BdInfo;
|
||||
using MediaBrowser.MediaEncoding.Subtitles;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.System;
|
||||
|
@ -105,6 +101,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 +109,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 +117,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 +130,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 +149,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 +158,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 +181,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 +312,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 +396,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 +412,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
var entryPoints = GetExports<IServerEntryPoint>();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
|
||||
|
@ -435,8 +421,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,11 +450,11 @@ namespace Emby.Server.Implementations
|
|||
|
||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||
|
||||
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
||||
NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
|
||||
|
||||
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||
HttpPort = networkConfiguration.HttpServerPortNumber;
|
||||
HttpsPort = networkConfiguration.HttpsPortNumber;
|
||||
HttpPort = networkConfiguration.InternalHttpPort;
|
||||
HttpsPort = networkConfiguration.InternalHttpsPort;
|
||||
|
||||
// Safeguard against invalid configuration
|
||||
if (HttpPort == HttpsPort)
|
||||
|
@ -503,7 +487,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);
|
||||
|
@ -525,6 +513,8 @@ namespace Emby.Server.Implementations
|
|||
|
||||
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||
|
||||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||
|
||||
|
@ -567,8 +557,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||
|
@ -580,8 +568,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
||||
|
||||
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||
|
@ -620,13 +606,15 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
}
|
||||
|
||||
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
|
||||
((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
|
||||
|
||||
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
|
||||
await localizationManager.LoadAll().ConfigureAwait(false);
|
||||
|
||||
_sessionManager = Resolve<ISessionManager>();
|
||||
|
||||
SetStaticProperties();
|
||||
|
||||
|
||||
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
|
||||
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
|
||||
|
||||
|
@ -684,7 +672,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>();
|
||||
|
@ -784,8 +772,8 @@ namespace Emby.Server.Implementations
|
|||
if (HttpPort != 0 && HttpsPort != 0)
|
||||
{
|
||||
// Need to restart if ports have changed
|
||||
if (networkConfiguration.HttpServerPortNumber != HttpPort
|
||||
|| networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||
if (networkConfiguration.InternalHttpPort != HttpPort
|
||||
|| networkConfiguration.InternalHttpsPort != HttpsPort)
|
||||
{
|
||||
if (ConfigurationManager.Configuration.IsPortAuthorized)
|
||||
{
|
||||
|
@ -854,38 +842,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>
|
||||
|
@ -919,7 +875,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;
|
||||
|
@ -941,49 +897,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)
|
||||
{
|
||||
|
@ -994,18 +907,20 @@ namespace Emby.Server.Implementations
|
|||
return PublishedServerUrl.Trim('/');
|
||||
}
|
||||
|
||||
string smart = NetManager.GetBindInterface(remoteAddr, out var port);
|
||||
string smart = NetManager.GetBindAddress(remoteAddr, out var port);
|
||||
return GetLocalApiUrl(smart.Trim('/'), null, port);
|
||||
}
|
||||
|
||||
/// <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 == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
||||
if (requestPort is null
|
||||
|| (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
|
||||
|| (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
requestPort = -1;
|
||||
}
|
||||
|
@ -1026,15 +941,15 @@ namespace Emby.Server.Implementations
|
|||
return PublishedServerUrl.Trim('/');
|
||||
}
|
||||
|
||||
string smart = NetManager.GetBindInterface(hostname, out var port);
|
||||
string smart = NetManager.GetBindAddress(hostname, out var port);
|
||||
return GetLocalApiUrl(smart.Trim('/'), null, port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true)
|
||||
public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
|
||||
{
|
||||
// With an empty source, the port will be null
|
||||
var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _);
|
||||
var smart = NetManager.GetBindAddress(ipAddress, out _, false);
|
||||
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||
int? port = !allowHttps ? HttpPort : null;
|
||||
return GetLocalApiUrl(smart, scheme, port);
|
||||
|
@ -1062,30 +977,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
|
||||
|
@ -1149,52 +1040,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,16 +157,16 @@ namespace Emby.Server.Implementations.Channels
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
|
||||
public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
|
||||
{
|
||||
var user = query.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(query.UserId);
|
||||
|
||||
var channels = GetAllChannels()
|
||||
.Select(GetChannelEntity)
|
||||
var channels = await GetAllChannelEntitiesAsync()
|
||||
.OrderBy(i => i.SortName)
|
||||
.ToList();
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (query.IsRecordingsFolder.HasValue)
|
||||
{
|
||||
|
@ -226,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
if (user is not null)
|
||||
{
|
||||
var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
channels = channels.Where(i =>
|
||||
{
|
||||
if (!i.IsVisible(user))
|
||||
|
@ -235,7 +236,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
try
|
||||
{
|
||||
return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture));
|
||||
return GetChannelProvider(i).IsEnabledFor(userId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -258,7 +259,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
{
|
||||
foreach (var item in all)
|
||||
{
|
||||
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
|
||||
await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,13 +270,13 @@ namespace Emby.Server.Implementations.Channels
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
|
||||
public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
|
||||
{
|
||||
var user = query.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(query.UserId);
|
||||
|
||||
var internalResult = GetChannelsInternal(query);
|
||||
var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
|
||||
|
@ -327,9 +328,12 @@ namespace Emby.Server.Implementations.Channels
|
|||
progress.Report(100);
|
||||
}
|
||||
|
||||
private Channel GetChannelEntity(IChannel channel)
|
||||
private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync()
|
||||
{
|
||||
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult();
|
||||
foreach (IChannel channel in GetAllChannels())
|
||||
{
|
||||
yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
|
||||
|
@ -367,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 />
|
||||
|
@ -401,7 +408,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
}
|
||||
else
|
||||
{
|
||||
results = new List<MediaSourceInfo>();
|
||||
results = Enumerable.Empty<MediaSourceInfo>();
|
||||
}
|
||||
|
||||
return results
|
||||
|
@ -1152,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)
|
||||
|
|
|
@ -112,7 +112,8 @@ namespace Emby.Server.Implementations.Collections
|
|||
return Path.Combine(_appPaths.DataPath, "collections");
|
||||
}
|
||||
|
||||
private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
|
||||
/// <inheritdoc />
|
||||
public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
|
||||
{
|
||||
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
|
||||
}
|
||||
|
@ -206,8 +207,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
throw new ArgumentException("No collection exists with the supplied Id");
|
||||
}
|
||||
|
||||
var list = new List<LinkedChild>();
|
||||
var itemList = new List<BaseItem>();
|
||||
List<BaseItem>? itemList = null;
|
||||
|
||||
var linkedChildrenList = collection.GetLinkedChildren();
|
||||
var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList();
|
||||
|
@ -223,18 +223,23 @@ namespace Emby.Server.Implementations.Collections
|
|||
|
||||
if (!currentLinkedChildrenIds.Contains(id))
|
||||
{
|
||||
itemList.Add(item);
|
||||
(itemList ??= new()).Add(item);
|
||||
|
||||
list.Add(LinkedChild.Create(item));
|
||||
linkedChildrenList.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (list.Count > 0)
|
||||
if (itemList is not null)
|
||||
{
|
||||
LinkedChild[] newChildren = new LinkedChild[collection.LinkedChildren.Length + list.Count];
|
||||
var originalLen = collection.LinkedChildren.Length;
|
||||
var newItemCount = itemList.Count;
|
||||
LinkedChild[] newChildren = new LinkedChild[originalLen + newItemCount];
|
||||
collection.LinkedChildren.CopyTo(newChildren, 0);
|
||||
list.CopyTo(newChildren, collection.LinkedChildren.Length);
|
||||
for (int i = 0; i < newItemCount; i++)
|
||||
{
|
||||
newChildren[originalLen + i] = LinkedChild.Create(itemList[i]);
|
||||
}
|
||||
|
||||
collection.LinkedChildren = newChildren;
|
||||
collection.UpdateRatingToItems(linkedChildrenList);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -11,14 +11,15 @@ namespace Emby.Server.Implementations
|
|||
/// <summary>
|
||||
/// Gets a new copy of the default configuration options.
|
||||
/// </summary>
|
||||
public static Dictionary<string, string?> DefaultConfiguration => new Dictionary<string, string?>
|
||||
public static Dictionary<string, string?> DefaultConfiguration => new()
|
||||
{
|
||||
{ HostWebClientKey, bool.TrueString },
|
||||
{ DefaultRedirectKey, "web/index.html" },
|
||||
{ DefaultRedirectKey, "web/" },
|
||||
{ FfmpegProbeSizeKey, "1G" },
|
||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
||||
{ BindToUnixSocketKey, bool.FalseString }
|
||||
{ BindToUnixSocketKey, bool.FalseString },
|
||||
{ SqliteCacheSizeKey, "20000" }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,9 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
|
@ -27,33 +26,25 @@ namespace Emby.Server.Implementations.Data
|
|||
/// <summary>
|
||||
/// Gets or sets the path to the DB file.
|
||||
/// </summary>
|
||||
/// <value>Path to the DB file.</value>
|
||||
protected string DbFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of write connections to create.
|
||||
/// </summary>
|
||||
/// <value>Path to the DB file.</value>
|
||||
protected int WriteConnectionsCount { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of read connections to create.
|
||||
/// </summary>
|
||||
protected int ReadConnectionsCount { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
|
@ -63,7 +54,7 @@ namespace Emby.Server.Implementations.Data
|
|||
/// <summary>
|
||||
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
|
||||
/// </summary>
|
||||
protected virtual string LockingMode => "EXCLUSIVE";
|
||||
protected virtual string LockingMode => "NORMAL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
|
||||
|
@ -73,9 +64,10 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
/// <summary>
|
||||
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
|
||||
/// The default (-1) is overriden to prevent unconstrained WAL size, as reported by users.
|
||||
/// </summary>
|
||||
/// <value>The journal size limit.</value>
|
||||
protected virtual int? JournalSizeLimit => 0;
|
||||
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
|
||||
|
||||
/// <summary>
|
||||
/// Gets the page size.
|
||||
|
@ -88,7 +80,7 @@ namespace Emby.Server.Implementations.Data
|
|||
/// </summary>
|
||||
/// <value>The temp store mode.</value>
|
||||
/// <see cref="TempStoreMode"/>
|
||||
protected virtual TempStoreMode TempStore => TempStoreMode.Default;
|
||||
protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the synchronous mode.
|
||||
|
@ -97,109 +89,77 @@ 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 SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the write connection.
|
||||
/// </summary>
|
||||
/// <value>The write connection.</value>
|
||||
protected SQLiteDatabaseConnection WriteConnection { get; set; }
|
||||
|
||||
protected ManagedConnection GetConnection(bool readOnly = false)
|
||||
public virtual void Initialize()
|
||||
{
|
||||
WriteLock.Wait();
|
||||
if (WriteConnection is not null)
|
||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
return new ManagedConnection(WriteConnection, WriteLock);
|
||||
connection.Execute("VACUUM");
|
||||
}
|
||||
}
|
||||
|
||||
WriteConnection = SQLite3.Open(
|
||||
DbFilePath,
|
||||
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
|
||||
null);
|
||||
protected SqliteConnection GetConnection()
|
||||
{
|
||||
var connection = new SqliteConnection($"Filename={DbFilePath}");
|
||||
connection.Open();
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||
connection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
connection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
}
|
||||
|
||||
if (JournalSizeLimit.HasValue)
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA journal_size_limit=" + (int)JournalSizeLimit.Value);
|
||||
connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
||||
}
|
||||
|
||||
if (Synchronous.HasValue)
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
}
|
||||
|
||||
if (PageSize.HasValue)
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
connection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
}
|
||||
|
||||
WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
||||
WriteConnection.Execute("VACUUM");
|
||||
|
||||
return new ManagedConnection(WriteConnection, WriteLock);
|
||||
return connection;
|
||||
}
|
||||
|
||||
public IStatement PrepareStatement(ManagedConnection connection, string sql)
|
||||
=> connection.PrepareStatement(sql);
|
||||
|
||||
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
|
||||
=> connection.PrepareStatement(sql);
|
||||
|
||||
public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql)
|
||||
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
|
||||
{
|
||||
int len = sql.Count;
|
||||
IStatement[] statements = new IStatement[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
return command;
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
statements[i] = connection.PrepareStatement(sql[i]);
|
||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return statements;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected bool TableExists(ManagedConnection connection, string name)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
|
||||
protected List<string> GetColumnNames(SqliteConnection connection, string table)
|
||||
{
|
||||
var columnNames = new List<string>();
|
||||
|
||||
|
@ -214,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))
|
||||
{
|
||||
|
@ -250,24 +210,6 @@ namespace Emby.Server.Implementations.Data
|
|||
return;
|
||||
}
|
||||
|
||||
if (dispose)
|
||||
{
|
||||
WriteLock.Wait();
|
||||
try
|
||||
{
|
||||
WriteConnection?.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
WriteLock.Release();
|
||||
}
|
||||
|
||||
WriteLock.Dispose();
|
||||
}
|
||||
|
||||
WriteConnection = null;
|
||||
WriteLock = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public sealed class ManagedConnection : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _writeLock;
|
||||
|
||||
private SQLiteDatabaseConnection? _db;
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
|
||||
{
|
||||
_db = db;
|
||||
_writeLock = writeLock;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
_writeLock.Release();
|
||||
|
||||
_db = null; // Don't dispose it
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -7,86 +7,85 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public SqliteUserDataRepository(
|
||||
ILogger<SqliteUserDataRepository> logger,
|
||||
IApplicationPaths appPaths)
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager)
|
||||
: base(logger)
|
||||
{
|
||||
DbFilePath = Path.Combine(appPaths.DataPath, "library.db");
|
||||
_userManager = userManager;
|
||||
|
||||
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database.
|
||||
/// </summary>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
/// <param name="dbLock">The lock to use for database IO.</param>
|
||||
/// <param name="dbConnection">The connection to use for database IO.</param>
|
||||
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
|
||||
public override void Initialize()
|
||||
{
|
||||
WriteLock.Dispose();
|
||||
WriteLock = dbLock;
|
||||
WriteConnection?.Dispose();
|
||||
WriteConnection = dbConnection;
|
||||
base.Initialize();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
var userDatasTableExists = TableExists(connection, "UserDatas");
|
||||
var userDataTableExists = TableExists(connection, "userdata");
|
||||
|
||||
var users = userDatasTableExists ? null : userManager.Users;
|
||||
var users = userDatasTableExists ? null : _userManager.Users;
|
||||
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);
|
||||
|
||||
|
@ -102,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>();
|
||||
|
||||
|
@ -118,7 +116,7 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
try
|
||||
{
|
||||
list.Add(row[0].ReadGuidFromBlob());
|
||||
list.Add(row.GetGuid(0));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -170,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)"))
|
||||
{
|
||||
|
@ -228,7 +223,7 @@ namespace Emby.Server.Implementations.Data
|
|||
statement.TryBindNull("@SubtitleStreamIndex");
|
||||
}
|
||||
|
||||
statement.MoveNext();
|
||||
statement.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,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"))
|
||||
{
|
||||
|
@ -337,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();
|
||||
|
||||
|
@ -349,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))
|
||||
{
|
||||
|
@ -371,20 +364,5 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
return userData;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2215
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and
|
||||
/// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>.
|
||||
/// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>.
|
||||
/// </remarks>
|
||||
protected override void Dispose(bool dispose)
|
||||
{
|
||||
// The write lock and connection for the item repository are shared with the user data repository
|
||||
// since they point to the same database. The item repo has responsibility for disposing these two objects,
|
||||
// so the user data repo should not attempt to dispose them as well
|
||||
}
|
||||
#pragma warning restore CA2215
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ using System.Collections.Generic;
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
|
@ -23,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;
|
||||
|
@ -53,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,
|
||||
|
@ -64,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;
|
||||
|
@ -76,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
|
|||
_mediaSourceManager = mediaSourceManager;
|
||||
_livetvManagerFactory = livetvManagerFactory;
|
||||
_lyricManager = lyricManager;
|
||||
_trickplayManager = trickplayManager;
|
||||
}
|
||||
|
||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||
|
@ -83,22 +86,23 @@ namespace Emby.Server.Implementations.Dto
|
|||
/// <inheritdoc />
|
||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
||||
{
|
||||
var returnItems = new BaseItemDto[items.Count];
|
||||
var programTuples = new List<(BaseItem, BaseItemDto)>();
|
||||
var channelTuples = new List<(BaseItemDto, LiveTvChannel)>();
|
||||
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
|
||||
var returnItems = new BaseItemDto[accessibleItems.Count];
|
||||
List<(BaseItem, BaseItemDto)> programTuples = null;
|
||||
List<(BaseItemDto, LiveTvChannel)> channelTuples = null;
|
||||
|
||||
for (int index = 0; index < items.Count; index++)
|
||||
for (int index = 0; index < accessibleItems.Count; index++)
|
||||
{
|
||||
var item = items[index];
|
||||
var item = accessibleItems[index];
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
channelTuples.Add((dto, tvChannel));
|
||||
(channelTuples ??= new()).Add((dto, tvChannel));
|
||||
}
|
||||
else if (item is LiveTvProgram)
|
||||
{
|
||||
programTuples.Add((item, dto));
|
||||
(programTuples ??= new()).Add((item, dto));
|
||||
}
|
||||
|
||||
if (item is IItemByName byName)
|
||||
|
@ -121,12 +125,12 @@ namespace Emby.Server.Implementations.Dto
|
|||
returnItems[index] = dto;
|
||||
}
|
||||
|
||||
if (programTuples.Count > 0)
|
||||
if (programTuples is not null)
|
||||
{
|
||||
LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (channelTuples.Count > 0)
|
||||
if (channelTuples is not null)
|
||||
{
|
||||
LivetvManager.AddChannelInfo(channelTuples, options, user);
|
||||
}
|
||||
|
@ -522,32 +526,32 @@ namespace Emby.Server.Implementations.Dto
|
|||
var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
|
||||
.ThenBy(i =>
|
||||
{
|
||||
if (i.IsType(PersonType.Actor))
|
||||
if (i.IsType(PersonKind.Actor))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (i.IsType(PersonType.GuestStar))
|
||||
if (i.IsType(PersonKind.GuestStar))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (i.IsType(PersonType.Director))
|
||||
if (i.IsType(PersonKind.Director))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (i.IsType(PersonType.Writer))
|
||||
if (i.IsType(PersonKind.Writer))
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (i.IsType(PersonType.Producer))
|
||||
if (i.IsType(PersonKind.Producer))
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (i.IsType(PersonType.Composer))
|
||||
if (i.IsType(PersonKind.Composer))
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
@ -571,9 +575,7 @@ namespace Emby.Server.Implementations.Dto
|
|||
return null;
|
||||
}
|
||||
}).Where(i => i is not null)
|
||||
.Where(i => user is null ?
|
||||
true :
|
||||
i.IsVisible(user))
|
||||
.Where(i => user is null || i.IsVisible(user))
|
||||
.DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
@ -905,6 +907,8 @@ namespace Emby.Server.Implementations.Dto
|
|||
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
|
||||
}
|
||||
|
||||
dto.LUFS = item.LUFS;
|
||||
|
||||
// Add audio info
|
||||
if (item is Audio audio)
|
||||
{
|
||||
|
@ -1059,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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
return new StringBuilder(32)
|
||||
.Append(config.EnableUPnP).Append(Separator)
|
||||
.Append(config.PublicPort).Append(Separator)
|
||||
.Append(config.PublicHttpPort).Append(Separator)
|
||||
.Append(config.PublicHttpsPort).Append(Separator)
|
||||
.Append(_appHost.HttpPort).Append(Separator)
|
||||
.Append(_appHost.HttpsPort).Append(Separator)
|
||||
|
@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
private IEnumerable<Task> CreatePortMaps(INatDevice device)
|
||||
{
|
||||
var config = _config.GetNetworkConfiguration();
|
||||
yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort);
|
||||
yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
|
||||
|
||||
if (_appHost.ListenWithHttps)
|
||||
{
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
@ -12,6 +8,7 @@ using System.Threading.Tasks;
|
|||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -22,470 +19,382 @@ using MediaBrowser.Model.Entities;
|
|||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.EntryPoints
|
||||
namespace Emby.Server.Implementations.EntryPoints;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IServerEntryPoint"/> that notifies users when libraries are updated.
|
||||
/// </summary>
|
||||
public sealed class LibraryChangedNotifier : IServerEntryPoint
|
||||
{
|
||||
public class LibraryChangedNotifier : IServerEntryPoint
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILogger<LibraryChangedNotifier> _logger;
|
||||
|
||||
private readonly object _libraryChangedSyncLock = new();
|
||||
private readonly List<Folder> _foldersAddedTo = new();
|
||||
private readonly List<Folder> _foldersRemovedFrom = new();
|
||||
private readonly List<BaseItem> _itemsAdded = new();
|
||||
private readonly List<BaseItem> _itemsRemoved = new();
|
||||
private readonly List<BaseItem> _itemsUpdated = new();
|
||||
private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new();
|
||||
|
||||
private Timer? _libraryUpdateTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryChangedNotifier"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
|
||||
/// <param name="configurationManager">The <see cref="IServerConfigurationManager"/>.</param>
|
||||
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
|
||||
/// <param name="userManager">The <see cref="IUserManager"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||
/// <param name="providerManager">The <see cref="IProviderManager"/>.</param>
|
||||
public LibraryChangedNotifier(
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager configurationManager,
|
||||
ISessionManager sessionManager,
|
||||
IUserManager userManager,
|
||||
ILogger<LibraryChangedNotifier> logger,
|
||||
IProviderManager providerManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// The library update duration.
|
||||
/// </summary>
|
||||
private const int LibraryUpdateDuration = 30000;
|
||||
_libraryManager = libraryManager;
|
||||
_configurationManager = configurationManager;
|
||||
_sessionManager = sessionManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILogger<LibraryChangedNotifier> _logger;
|
||||
/// <inheritdoc />
|
||||
public Task RunAsync()
|
||||
{
|
||||
_libraryManager.ItemAdded += OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated += OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved += OnLibraryItemRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// The library changed sync lock.
|
||||
/// </summary>
|
||||
private readonly object _libraryChangedSyncLock = new object();
|
||||
_providerManager.RefreshCompleted += OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted += OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress += OnProviderRefreshProgress;
|
||||
|
||||
private readonly List<Folder> _foldersAddedTo = new List<Folder>();
|
||||
private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
|
||||
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
|
||||
private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
|
||||
private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
|
||||
private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new ConcurrentDictionary<Guid, DateTime>();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public LibraryChangedNotifier(
|
||||
ILibraryManager libraryManager,
|
||||
ISessionManager sessionManager,
|
||||
IUserManager userManager,
|
||||
ILogger<LibraryChangedNotifier> logger,
|
||||
IProviderManager providerManager)
|
||||
private void OnProviderRefreshProgress(object? sender, GenericEventArgs<Tuple<BaseItem, double>> e)
|
||||
{
|
||||
var item = e.Argument.Item1;
|
||||
|
||||
if (!EnableRefreshMessage(item))
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_sessionManager = sessionManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_providerManager = providerManager;
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the library update timer.
|
||||
/// </summary>
|
||||
/// <value>The library update timer.</value>
|
||||
private Timer LibraryUpdateTimer { get; set; }
|
||||
var progress = e.Argument.Item2;
|
||||
|
||||
public Task RunAsync()
|
||||
if (_lastProgressMessageTimes.TryGetValue(item.Id, out var lastMessageSendTime))
|
||||
{
|
||||
_libraryManager.ItemAdded += OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated += OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved += OnLibraryItemRemoved;
|
||||
|
||||
_providerManager.RefreshCompleted += OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted += OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress += OnProviderRefreshProgress;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
|
||||
{
|
||||
var item = e.Argument.Item1;
|
||||
|
||||
if (!EnableRefreshMessage(item))
|
||||
if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var progress = e.Argument.Item2;
|
||||
_lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow);
|
||||
|
||||
if (_lastProgressMessageTimes.TryGetValue(item.Id, out var lastMessageSendTime))
|
||||
var dict = new Dictionary<string, string>();
|
||||
dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
try
|
||||
{
|
||||
_sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var collectionFolders = _libraryManager.GetCollectionFolders(item);
|
||||
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
var collectionFolderDict = new Dictionary<string, string>
|
||||
{
|
||||
if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow);
|
||||
|
||||
var dict = new Dictionary<string, string>();
|
||||
dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture);
|
||||
["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
|
||||
_sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var collectionFolders = _libraryManager.GetCollectionFolders(item);
|
||||
private void OnProviderRefreshStarted(object? sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
|
||||
}
|
||||
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
var collectionFolderDict = new Dictionary<string, string>
|
||||
{
|
||||
["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
private void OnProviderRefreshCompleted(object? sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
|
||||
|
||||
try
|
||||
{
|
||||
_sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
_lastProgressMessageTimes.TryRemove(e.Argument.Id, out _);
|
||||
}
|
||||
|
||||
private static bool EnableRefreshMessage(BaseItem item)
|
||||
=> item is Folder { IsRoot: false, IsTopParent: true }
|
||||
and not (AggregateFolder or UserRootFolder or UserView or Channel);
|
||||
|
||||
private void OnLibraryItemAdded(object? sender, ItemChangeEventArgs e)
|
||||
=> OnLibraryChange(e.Item, e.Parent, _itemsAdded, _foldersAddedTo);
|
||||
|
||||
private void OnLibraryItemUpdated(object? sender, ItemChangeEventArgs e)
|
||||
=> OnLibraryChange(e.Item, e.Parent, _itemsUpdated, null);
|
||||
|
||||
private void OnLibraryItemRemoved(object? sender, ItemChangeEventArgs e)
|
||||
=> OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom);
|
||||
|
||||
private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder>? foldersList)
|
||||
{
|
||||
if (!FilterItem(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e)
|
||||
lock (_libraryChangedSyncLock)
|
||||
{
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
|
||||
var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration);
|
||||
|
||||
if (_libraryUpdateTimer is null)
|
||||
{
|
||||
_libraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
_libraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
if (foldersList is not null && parent is Folder folder)
|
||||
{
|
||||
foldersList.Add(folder);
|
||||
}
|
||||
|
||||
itemsList.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private async void LibraryUpdateTimerCallback(object? state)
|
||||
{
|
||||
List<Folder> foldersAddedTo;
|
||||
List<Folder> foldersRemovedFrom;
|
||||
List<BaseItem> itemsUpdated;
|
||||
List<BaseItem> itemsAdded;
|
||||
List<BaseItem> itemsRemoved;
|
||||
lock (_libraryChangedSyncLock)
|
||||
{
|
||||
// Remove dupes in case some were saved multiple times
|
||||
foldersAddedTo = _foldersAddedTo
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
foldersRemovedFrom = _foldersRemovedFrom
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
itemsUpdated = _itemsUpdated
|
||||
.Where(i => !_itemsAdded.Contains(i))
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
itemsAdded = _itemsAdded.ToList();
|
||||
itemsRemoved = _itemsRemoved.ToList();
|
||||
|
||||
if (_libraryUpdateTimer is not null)
|
||||
{
|
||||
_libraryUpdateTimer.Dispose();
|
||||
_libraryUpdateTimer = null;
|
||||
}
|
||||
|
||||
_itemsAdded.Clear();
|
||||
_itemsRemoved.Clear();
|
||||
_itemsUpdated.Clear();
|
||||
_foldersAddedTo.Clear();
|
||||
_foldersRemovedFrom.Clear();
|
||||
}
|
||||
|
||||
private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
|
||||
await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendChangeNotifications(
|
||||
List<BaseItem> itemsAdded,
|
||||
List<BaseItem> itemsUpdated,
|
||||
List<BaseItem> itemsRemoved,
|
||||
List<Folder> foldersAddedTo,
|
||||
List<Folder> foldersRemovedFrom,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userIds = _sessionManager.Sessions
|
||||
.Select(i => i.UserId)
|
||||
.Where(i => !i.Equals(default))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
foreach (var userId in userIds)
|
||||
{
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
|
||||
LibraryUpdateInfo info;
|
||||
|
||||
_lastProgressMessageTimes.TryRemove(e.Argument.Id, out _);
|
||||
}
|
||||
|
||||
private static bool EnableRefreshMessage(BaseItem item)
|
||||
{
|
||||
if (item is not Folder folder)
|
||||
try
|
||||
{
|
||||
return false;
|
||||
info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, userId);
|
||||
}
|
||||
|
||||
if (folder.IsRoot)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (folder is AggregateFolder || folder is UserRootFolder)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (folder is UserView || folder is Channel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!folder.IsTopParent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the ItemAdded event of the libraryManager control.
|
||||
/// </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 OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in GetLibraryUpdateInfo");
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_libraryChangedSyncLock)
|
||||
if (info.IsEmpty)
|
||||
{
|
||||
if (LibraryUpdateTimer is null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(
|
||||
LibraryUpdateTimerCallback,
|
||||
null,
|
||||
LibraryUpdateDuration,
|
||||
Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.Item.GetParent() is Folder parent)
|
||||
{
|
||||
_foldersAddedTo.Add(parent);
|
||||
}
|
||||
|
||||
_itemsAdded.Add(e.Item);
|
||||
try
|
||||
{
|
||||
await _sessionManager.SendMessageToUserSessions(
|
||||
new List<Guid> { userId },
|
||||
SessionMessageType.LibraryChanged,
|
||||
info,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending LibraryChanged message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the ItemUpdated event of the libraryManager control.
|
||||
/// </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 OnLibraryItemUpdated(object sender, ItemChangeEventArgs e)
|
||||
private LibraryUpdateInfo GetLibraryUpdateInfo(
|
||||
List<BaseItem> itemsAdded,
|
||||
List<BaseItem> itemsUpdated,
|
||||
List<BaseItem> itemsRemoved,
|
||||
List<Folder> foldersAddedTo,
|
||||
List<Folder> foldersRemovedFrom,
|
||||
Guid userId)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
var newAndRemoved = new List<BaseItem>();
|
||||
newAndRemoved.AddRange(foldersAddedTo);
|
||||
newAndRemoved.AddRange(foldersRemovedFrom);
|
||||
|
||||
var allUserRootChildren = _libraryManager.GetUserRootFolder()
|
||||
.GetChildren(user, true)
|
||||
.OfType<Folder>()
|
||||
.ToList();
|
||||
|
||||
return new LibraryUpdateInfo
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_libraryChangedSyncLock)
|
||||
{
|
||||
if (LibraryUpdateTimer is null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
|
||||
_itemsUpdated.Add(e.Item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the ItemRemoved event of the libraryManager control.
|
||||
/// </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 OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_libraryChangedSyncLock)
|
||||
{
|
||||
if (LibraryUpdateTimer is null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
|
||||
if (e.Parent is Folder parent)
|
||||
{
|
||||
_foldersRemovedFrom.Add(parent);
|
||||
}
|
||||
|
||||
_itemsRemoved.Add(e.Item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Libraries the update timer callback.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
private void LibraryUpdateTimerCallback(object state)
|
||||
{
|
||||
lock (_libraryChangedSyncLock)
|
||||
{
|
||||
// Remove dupes in case some were saved multiple times
|
||||
var foldersAddedTo = _foldersAddedTo
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
var foldersRemovedFrom = _foldersRemovedFrom
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
var itemsUpdated = _itemsUpdated
|
||||
.Where(i => !_itemsAdded.Contains(i))
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
if (LibraryUpdateTimer is not null)
|
||||
{
|
||||
LibraryUpdateTimer.Dispose();
|
||||
LibraryUpdateTimer = null;
|
||||
}
|
||||
|
||||
_itemsAdded.Clear();
|
||||
_itemsRemoved.Clear();
|
||||
_itemsUpdated.Clear();
|
||||
_foldersAddedTo.Clear();
|
||||
_foldersRemovedFrom.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the change notifications.
|
||||
/// </summary>
|
||||
/// <param name="itemsAdded">The items added.</param>
|
||||
/// <param name="itemsUpdated">The items updated.</param>
|
||||
/// <param name="itemsRemoved">The items removed.</param>
|
||||
/// <param name="foldersAddedTo">The folders added to.</param>
|
||||
/// <param name="foldersRemovedFrom">The folders removed from.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
private async Task SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken)
|
||||
{
|
||||
var userIds = _sessionManager.Sessions
|
||||
.Select(i => i.UserId)
|
||||
.Where(i => !i.Equals(default))
|
||||
ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
|
||||
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
.ToArray(),
|
||||
ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
|
||||
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
|
||||
.Distinct()
|
||||
.ToArray(),
|
||||
ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true))
|
||||
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
|
||||
.Distinct()
|
||||
.ToArray(),
|
||||
FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
|
||||
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
|
||||
.Distinct()
|
||||
.ToArray(),
|
||||
FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
|
||||
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
|
||||
.Distinct()
|
||||
.ToArray(),
|
||||
CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var userId in userIds)
|
||||
{
|
||||
LibraryUpdateInfo info;
|
||||
|
||||
try
|
||||
{
|
||||
info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, userId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in GetLibraryUpdateInfo");
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.IsEmpty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending LibraryChanged message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the library update info.
|
||||
/// </summary>
|
||||
/// <param name="itemsAdded">The items added.</param>
|
||||
/// <param name="itemsUpdated">The items updated.</param>
|
||||
/// <param name="itemsRemoved">The items removed.</param>
|
||||
/// <param name="foldersAddedTo">The folders added to.</param>
|
||||
/// <param name="foldersRemovedFrom">The folders removed from.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <returns>LibraryUpdateInfo.</returns>
|
||||
private LibraryUpdateInfo GetLibraryUpdateInfo(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, Guid userId)
|
||||
private static bool FilterItem(BaseItem item)
|
||||
{
|
||||
if (!item.IsFolder && !item.HasPathProtocol)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
|
||||
var newAndRemoved = new List<BaseItem>();
|
||||
newAndRemoved.AddRange(foldersAddedTo);
|
||||
newAndRemoved.AddRange(foldersRemovedFrom);
|
||||
|
||||
var allUserRootChildren = _libraryManager.GetUserRootFolder().GetChildren(user, true).OfType<Folder>().ToList();
|
||||
|
||||
return new LibraryUpdateInfo
|
||||
{
|
||||
ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(),
|
||||
|
||||
ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(),
|
||||
|
||||
ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(),
|
||||
|
||||
FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(),
|
||||
|
||||
FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(),
|
||||
|
||||
CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray()
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool FilterItem(BaseItem item)
|
||||
if (item is IItemByName && item is not MusicArtist)
|
||||
{
|
||||
if (!item.IsFolder && !item.HasPathProtocol)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item is IItemByName && item is not MusicArtist)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.SourceType == SourceType.Library;
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
|
||||
{
|
||||
var list = new List<string>();
|
||||
return item.SourceType == SourceType.Library;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
// If the physical root changed, return the user root
|
||||
if (item is AggregateFolder)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
|
||||
{
|
||||
var list = new List<string>();
|
||||
|
||||
foreach (var folder in allUserRootChildren)
|
||||
{
|
||||
list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
return list.Distinct(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates the physical item to user library.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item.</typeparam>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>
|
||||
/// <returns>IEnumerable{``0}.</returns>
|
||||
private IEnumerable<T> TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
|
||||
where T : BaseItem
|
||||
foreach (var item in items)
|
||||
{
|
||||
// If the physical root changed, return the user root
|
||||
if (item is AggregateFolder)
|
||||
{
|
||||
return new[] { _libraryManager.GetUserRootFolder() as T };
|
||||
continue;
|
||||
}
|
||||
|
||||
// Return it only if it's in the user's library
|
||||
if (includeIfNotFound || item.IsVisibleStandalone(user))
|
||||
foreach (var folder in allUserRootChildren)
|
||||
{
|
||||
return new[] { item };
|
||||
list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return Array.Empty<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
return list.Distinct(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private IEnumerable<T> TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
|
||||
where T : BaseItem
|
||||
{
|
||||
// If the physical root changed, return the user root
|
||||
if (item is AggregateFolder)
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
return _libraryManager.GetUserRootFolder() is T t ? new[] { t } : Array.Empty<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
// Return it only if it's in the user's library
|
||||
if (includeIfNotFound || item.IsVisibleStandalone(user))
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
if (LibraryUpdateTimer is not null)
|
||||
{
|
||||
LibraryUpdateTimer.Dispose();
|
||||
LibraryUpdateTimer = null;
|
||||
}
|
||||
return new[] { item };
|
||||
}
|
||||
|
||||
_libraryManager.ItemAdded -= OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated -= OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved -= OnLibraryItemRemoved;
|
||||
return Array.Empty<T>();
|
||||
}
|
||||
|
||||
_providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted -= OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress -= OnProviderRefreshProgress;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_libraryManager.ItemAdded -= OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated -= OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved -= OnLibraryItemRemoved;
|
||||
|
||||
_providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted -= OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress -= OnProviderRefreshProgress;
|
||||
|
||||
if (_libraryUpdateTimer is not null)
|
||||
{
|
||||
_libraryUpdateTimer.Dispose();
|
||||
_libraryUpdateTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Udp;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
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
|
||||
{
|
||||
|
@ -29,13 +33,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
private readonly INetworkManager _networkManager;
|
||||
|
||||
/// <summary>
|
||||
/// The UDP server.
|
||||
/// </summary>
|
||||
private UdpServer? _udpServer;
|
||||
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.
|
||||
|
@ -44,16 +49,20 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
|
||||
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
public UdpServerEntryPoint(
|
||||
ILogger<UdpServerEntryPoint> logger,
|
||||
IServerApplicationHost appHost,
|
||||
IConfiguration configuration,
|
||||
IConfigurationManager configurationManager)
|
||||
IConfigurationManager configurationManager,
|
||||
INetworkManager networkManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_config = configuration;
|
||||
_configurationManager = configurationManager;
|
||||
_networkManager = networkManager;
|
||||
_udpServers = new List<UdpServer>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -68,8 +77,43 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
try
|
||||
{
|
||||
_udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
|
||||
_udpServer.Start(_cancellationTokenSource.Token);
|
||||
// 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 listener
|
||||
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, 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 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);
|
||||
server.Start(_cancellationTokenSource.Token);
|
||||
_udpServers.Add(server);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
|
@ -83,7 +127,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(this.GetType().Name);
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,9 +141,12 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource.Dispose();
|
||||
_udpServer?.Dispose();
|
||||
_udpServer = null;
|
||||
foreach (var server in _udpServers)
|
||||
{
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
_udpServers.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,29 +87,30 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
}
|
||||
}
|
||||
|
||||
private void UpdateTimerCallback(object? state)
|
||||
private async void UpdateTimerCallback(object? state)
|
||||
{
|
||||
List<KeyValuePair<Guid, List<BaseItem>>> changes;
|
||||
lock (_syncLock)
|
||||
{
|
||||
// Remove dupes in case some were saved multiple times
|
||||
var changes = _changedItems.ToList();
|
||||
changes = _changedItems.ToList();
|
||||
_changedItems.Clear();
|
||||
|
||||
SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
if (_updateTimer is not null)
|
||||
{
|
||||
_updateTimer.Dispose();
|
||||
_updateTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var pair in changes)
|
||||
foreach ((var key, var value) in changes)
|
||||
{
|
||||
await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false);
|
||||
await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -42,14 +43,17 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="socket">The socket.</param>
|
||||
/// <param name="authorizationInfo">The authorization information.</param>
|
||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||
public WebSocketConnection(
|
||||
ILogger<WebSocketConnection> logger,
|
||||
WebSocket socket,
|
||||
AuthorizationInfo authorizationInfo,
|
||||
IPAddress? remoteEndPoint)
|
||||
{
|
||||
_logger = logger;
|
||||
_socket = socket;
|
||||
AuthorizationInfo = authorizationInfo;
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
|
||||
_jsonOptions = JsonDefaults.Options;
|
||||
|
@ -59,47 +63,40 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
/// <inheritdoc />
|
||||
public event EventHandler<EventArgs>? Closed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote end point.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public AuthorizationInfo AuthorizationInfo { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPAddress? RemoteEndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive action.
|
||||
/// </summary>
|
||||
/// <value>The receive action.</value>
|
||||
/// <inheritdoc />
|
||||
public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last activity date.
|
||||
/// </summary>
|
||||
/// <value>The last activity date.</value>
|
||||
/// <inheritdoc />
|
||||
public DateTime LastActivityDate { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime LastKeepAliveDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state.
|
||||
/// </summary>
|
||||
/// <value>The state.</value>
|
||||
/// <inheritdoc />
|
||||
public WebSocketState State => _socket.State;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the message.</typeparam>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
||||
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ProcessAsync(CancellationToken cancellationToken = default)
|
||||
public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
||||
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ReceiveAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pipe = new Pipe();
|
||||
var writer = pipe.Writer;
|
||||
|
@ -171,7 +168,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
return;
|
||||
}
|
||||
|
||||
WebSocketMessage<object>? stub;
|
||||
InboundWebSocketMessage<object>? stub;
|
||||
long bytesConsumed;
|
||||
try
|
||||
{
|
||||
|
@ -212,10 +209,10 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
}
|
||||
}
|
||||
|
||||
internal WebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed)
|
||||
internal InboundWebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed)
|
||||
{
|
||||
var jsonReader = new Utf8JsonReader(bytes);
|
||||
var ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions);
|
||||
var ret = JsonSerializer.Deserialize<InboundWebSocketMessage<object>>(ref jsonReader, _jsonOptions);
|
||||
bytesConsumed = jsonReader.BytesConsumed;
|
||||
return ret;
|
||||
}
|
||||
|
@ -224,11 +221,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
{
|
||||
LastKeepAliveDate = DateTime.UtcNow;
|
||||
return SendAsync(
|
||||
new WebSocketMessage<string>
|
||||
{
|
||||
MessageId = Guid.NewGuid(),
|
||||
MessageType = SessionMessageType.KeepAlive
|
||||
},
|
||||
new OutboundKeepAliveMessage(),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,8 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
using var connection = new WebSocketConnection(
|
||||
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||
webSocket,
|
||||
context.GetNormalizedRemoteIp())
|
||||
authorizationInfo,
|
||||
context.GetNormalizedRemoteIP())
|
||||
{
|
||||
OnReceive = ProcessWebSocketMessageReceived
|
||||
};
|
||||
|
@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
await connection.ProcessAsync().ConfigureAwait(false);
|
||||
await connection.ReceiveAsync().ConfigureAwait(false);
|
||||
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
|
||||
}
|
||||
catch (Exception ex) // Otherwise ASP.Net will ignore the exception
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
public class ExtendedFileSystemInfo
|
||||
{
|
||||
public bool IsHidden { get; set; }
|
||||
|
||||
public bool IsReadOnly { get; set; }
|
||||
|
||||
public bool Exists { get; set; }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -15,29 +15,34 @@ 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 =
|
||||
{
|
||||
'\"', '<', '>', '|', '\0',
|
||||
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
|
||||
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
|
||||
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
|
||||
(char)31, ':', '*', '?', '\\', '/'
|
||||
};
|
||||
|
||||
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>
|
||||
|
@ -86,7 +91,7 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
|
||||
// unc path
|
||||
if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
|
||||
if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
return filePath;
|
||||
}
|
||||
|
@ -98,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)
|
||||
{
|
||||
|
@ -267,25 +274,6 @@ namespace Emby.Server.Implementations.IO
|
|||
return result;
|
||||
}
|
||||
|
||||
private static ExtendedFileSystemInfo GetExtendedFileSystemInfo(string path)
|
||||
{
|
||||
var result = new ExtendedFileSystemInfo();
|
||||
|
||||
var info = new FileInfo(path);
|
||||
|
||||
if (info.Exists)
|
||||
{
|
||||
result.Exists = true;
|
||||
|
||||
var attributes = info.Attributes;
|
||||
|
||||
result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
||||
result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a filename and removes invalid characters.
|
||||
/// </summary>
|
||||
|
@ -294,8 +282,7 @@ namespace Emby.Server.Implementations.IO
|
|||
/// <exception cref="ArgumentNullException">The filename is null.</exception>
|
||||
public string GetValidFilename(string filename)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
var first = filename.IndexOfAny(invalid);
|
||||
var first = filename.IndexOfAny(_invalidPathCharacters);
|
||||
if (first == -1)
|
||||
{
|
||||
// Fast path for clean strings
|
||||
|
@ -304,7 +291,7 @@ namespace Emby.Server.Implementations.IO
|
|||
|
||||
return string.Create(
|
||||
filename.Length,
|
||||
(filename, invalid, first),
|
||||
(filename, _invalidPathCharacters, first),
|
||||
(chars, state) =>
|
||||
{
|
||||
state.filename.AsSpan().CopyTo(chars);
|
||||
|
@ -312,7 +299,7 @@ namespace Emby.Server.Implementations.IO
|
|||
chars[state.first++] = ' ';
|
||||
|
||||
var len = chars.Length;
|
||||
foreach (var c in state.invalid)
|
||||
foreach (var c in state._invalidPathCharacters)
|
||||
{
|
||||
for (int i = state.first; i < len; i++)
|
||||
{
|
||||
|
@ -403,19 +390,18 @@ namespace Emby.Server.Implementations.IO
|
|||
return;
|
||||
}
|
||||
|
||||
var info = GetExtendedFileSystemInfo(path);
|
||||
var info = new FileInfo(path);
|
||||
|
||||
if (info.Exists && info.IsHidden != isHidden)
|
||||
if (info.Exists &&
|
||||
((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
|
||||
{
|
||||
if (isHidden)
|
||||
{
|
||||
File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden);
|
||||
File.SetAttributes(path, info.Attributes | FileAttributes.Hidden);
|
||||
}
|
||||
else
|
||||
{
|
||||
var attributes = File.GetAttributes(path);
|
||||
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
|
||||
File.SetAttributes(path, attributes);
|
||||
File.SetAttributes(path, info.Attributes & ~FileAttributes.Hidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -428,19 +414,20 @@ namespace Emby.Server.Implementations.IO
|
|||
return;
|
||||
}
|
||||
|
||||
var info = GetExtendedFileSystemInfo(path);
|
||||
var info = new FileInfo(path);
|
||||
|
||||
if (!info.Exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
|
||||
if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
|
||||
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var attributes = File.GetAttributes(path);
|
||||
var attributes = info.Attributes;
|
||||
|
||||
if (readOnly)
|
||||
{
|
||||
|
@ -448,7 +435,7 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
else
|
||||
{
|
||||
attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
|
||||
attributes &= ~FileAttributes.ReadOnly;
|
||||
}
|
||||
|
||||
if (isHidden)
|
||||
|
@ -457,17 +444,12 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
else
|
||||
{
|
||||
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
|
||||
attributes &= ~FileAttributes.Hidden;
|
||||
}
|
||||
|
||||
File.SetAttributes(path, attributes);
|
||||
}
|
||||
|
||||
private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
|
||||
{
|
||||
return attributes & ~attributesToRemove;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the files.
|
||||
/// </summary>
|
||||
|
@ -502,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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -31,9 +31,10 @@ namespace Emby.Server.Implementations.Images
|
|||
return _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Parent = item,
|
||||
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)
|
||||
|
|
|
@ -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[]
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
|
|||
// bts sync files
|
||||
"**/*.bts",
|
||||
"**/*.sync",
|
||||
|
||||
// zfs
|
||||
"**/.zfs/**",
|
||||
"**/.zfs"
|
||||
};
|
||||
|
||||
private static readonly GlobOptions _globOptions = new GlobOptions
|
||||
|
|
|
@ -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,8 +111,8 @@ 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(
|
||||
IServerApplicationHost appHost,
|
||||
ILoggerFactory loggerFactory,
|
||||
|
@ -127,8 +127,8 @@ namespace Emby.Server.Implementations.Library
|
|||
IMediaEncoder mediaEncoder,
|
||||
IItemRepository itemRepository,
|
||||
IImageProcessor imageProcessor,
|
||||
IMemoryCache memoryCache,
|
||||
NamingOptions namingOptions)
|
||||
NamingOptions namingOptions,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_logger = loggerFactory.CreateLogger<LibraryManager>();
|
||||
|
@ -143,10 +143,10 @@ 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);
|
||||
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
|
||||
|
||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||
|
||||
|
@ -298,7 +298,7 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
_memoryCache.Set(item.Id, item);
|
||||
_cache[item.Id] = item;
|
||||
}
|
||||
|
||||
public void DeleteItem(BaseItem item, DeleteOptions options)
|
||||
|
@ -356,8 +356,8 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
var children = item.IsFolder
|
||||
? ((Folder)item).GetRecursiveChildren(false).ToList()
|
||||
: new List<BaseItem>();
|
||||
? ((Folder)item).GetRecursiveChildren(false)
|
||||
: Array.Empty<BaseItem>();
|
||||
|
||||
foreach (var metadataPath in GetMetadataPaths(item, children))
|
||||
{
|
||||
|
@ -439,7 +439,7 @@ namespace Emby.Server.Implementations.Library
|
|||
_itemRepository.DeleteItem(child.Id);
|
||||
}
|
||||
|
||||
_memoryCache.Remove(item.Id);
|
||||
_cache.TryRemove(item.Id, out _);
|
||||
|
||||
ReportItemRemoved(item, parent);
|
||||
}
|
||||
|
@ -525,19 +525,19 @@ 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);
|
||||
}
|
||||
|
||||
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
|
||||
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
|
||||
{
|
||||
Parent = parent,
|
||||
FileInfo = fileInfo,
|
||||
|
@ -607,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();
|
||||
|
||||
|
@ -635,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);
|
||||
}
|
||||
|
@ -645,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();
|
||||
|
@ -675,7 +675,7 @@ namespace Emby.Server.Implementations.Library
|
|||
IReadOnlyList<FileSystemMetadata> fileList,
|
||||
IDirectoryService directoryService,
|
||||
Folder parent,
|
||||
string collectionType,
|
||||
CollectionType? collectionType,
|
||||
IItemResolver[] resolvers,
|
||||
LibraryOptions libraryOptions)
|
||||
{
|
||||
|
@ -838,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>
|
||||
|
@ -1161,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
|
||||
|
@ -1231,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;
|
||||
}
|
||||
|
@ -1253,7 +1246,7 @@ namespace Emby.Server.Implementations.Library
|
|||
var parent = GetItemById(query.ParentId);
|
||||
if (parent is not null)
|
||||
{
|
||||
SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
|
||||
SetTopParentIdsOrAncestors(query, new[] { parent });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1262,7 +1255,14 @@ namespace Emby.Server.Implementations.Library
|
|||
AddUserToQuery(query, query.User, allowExternalContent);
|
||||
}
|
||||
|
||||
return _itemRepository.GetItemList(query);
|
||||
var itemList = _itemRepository.GetItemList(query);
|
||||
var user = query.User;
|
||||
if (user is not null)
|
||||
{
|
||||
return itemList.Where(i => i.IsVisible(user)).ToList();
|
||||
}
|
||||
|
||||
return itemList;
|
||||
}
|
||||
|
||||
public List<BaseItem> GetItemList(InternalItemsQuery query)
|
||||
|
@ -1277,7 +1277,7 @@ namespace Emby.Server.Implementations.Library
|
|||
var parent = GetItemById(query.ParentId);
|
||||
if (parent is not null)
|
||||
{
|
||||
SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
|
||||
SetTopParentIdsOrAncestors(query, new[] { parent });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1435,7 +1435,7 @@ namespace Emby.Server.Implementations.Library
|
|||
var parent = GetItemById(query.ParentId);
|
||||
if (parent is not null)
|
||||
{
|
||||
SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
|
||||
SetTopParentIdsOrAncestors(query, new[] { parent });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1455,7 +1455,7 @@ namespace Emby.Server.Implementations.Library
|
|||
_itemRepository.GetItemList(query));
|
||||
}
|
||||
|
||||
private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents)
|
||||
private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
|
||||
{
|
||||
if (parents.All(i => i is ICollectionFolder || i is UserView))
|
||||
{
|
||||
|
@ -1501,6 +1501,12 @@ namespace Emby.Server.Implementations.Library
|
|||
});
|
||||
|
||||
query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
|
||||
|
||||
// Prevent searching in all libraries due to empty filter
|
||||
if (query.TopParentIds.Length == 0)
|
||||
{
|
||||
query.TopParentIds = new[] { Guid.NewGuid() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1508,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 };
|
||||
}
|
||||
|
@ -1537,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));
|
||||
}
|
||||
|
@ -1602,7 +1608,7 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
_logger.LogError(ex, "Error getting intros");
|
||||
|
||||
return new List<IntroInfo>();
|
||||
return Enumerable.Empty<IntroInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1672,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;
|
||||
|
||||
|
@ -1695,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;
|
||||
|
||||
|
@ -1730,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)
|
||||
|
@ -1877,7 +1883,7 @@ namespace Emby.Server.Implementations.Library
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
||||
size = new ImageDimensions(0, 0);
|
||||
size = default;
|
||||
image.Width = 0;
|
||||
image.Height = 0;
|
||||
}
|
||||
|
@ -2054,19 +2060,21 @@ namespace Emby.Server.Implementations.Library
|
|||
.Find(folder => folder is CollectionFolder) as CollectionFolder;
|
||||
}
|
||||
|
||||
return collectionFolder is null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
|
||||
return collectionFolder is null
|
||||
? new LibraryOptions()
|
||||
: collectionFolder.GetLibraryOptions();
|
||||
}
|
||||
|
||||
public string GetContentType(BaseItem item)
|
||||
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;
|
||||
}
|
||||
|
@ -2074,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)
|
||||
{
|
||||
|
@ -2108,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)
|
||||
{
|
||||
|
@ -2139,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);
|
||||
|
@ -2153,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));
|
||||
|
||||
|
@ -2199,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));
|
||||
|
||||
|
@ -2261,7 +2274,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
public UserView GetShadowView(
|
||||
BaseItem parent,
|
||||
string viewType,
|
||||
CollectionType? viewType,
|
||||
string sortName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(parent);
|
||||
|
@ -2269,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));
|
||||
|
||||
|
@ -2326,7 +2339,7 @@ namespace Emby.Server.Implementations.Library
|
|||
public UserView GetNamedView(
|
||||
string name,
|
||||
Guid parentId,
|
||||
string viewType,
|
||||
CollectionType? viewType,
|
||||
string sortName,
|
||||
string uniqueId)
|
||||
{
|
||||
|
@ -2335,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;
|
||||
|
@ -2370,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();
|
||||
|
@ -2741,9 +2754,7 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
})
|
||||
.Where(i => i is not null)
|
||||
.Where(i => query.User is null ?
|
||||
true :
|
||||
i.IsVisible(query.User))
|
||||
.Where(i => query.User is null || i.IsVisible(query.User))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
@ -2844,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);
|
||||
|
@ -2876,7 +2887,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
|
||||
{
|
||||
var personsToSave = new List<BaseItem>();
|
||||
List<BaseItem> personsToSave = null;
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
|
@ -2886,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;
|
||||
}
|
||||
|
@ -2918,12 +2938,12 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (saveEntity)
|
||||
{
|
||||
personsToSave.Add(personEntity);
|
||||
(personsToSave ??= new()).Add(personEntity);
|
||||
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (personsToSave.Count > 0)
|
||||
if (personsToSave is not null)
|
||||
{
|
||||
CreateItems(personsToSave, null, CancellationToken.None);
|
||||
}
|
||||
|
@ -3085,22 +3105,19 @@ namespace Emby.Server.Implementations.Library
|
|||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
var removeList = new List<NameValuePair>();
|
||||
List<NameValuePair> removeList = null;
|
||||
|
||||
foreach (var contentType in _configurationManager.Configuration.ContentTypes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contentType.Name))
|
||||
{
|
||||
removeList.Add(contentType);
|
||||
}
|
||||
else if (_fileSystem.AreEqual(path, contentType.Name)
|
||||
if (string.IsNullOrWhiteSpace(contentType.Name)
|
||||
|| _fileSystem.AreEqual(path, contentType.Name)
|
||||
|| _fileSystem.ContainsSubPath(path, contentType.Name))
|
||||
{
|
||||
removeList.Add(contentType);
|
||||
(removeList ??= new()).Add(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
if (removeList.Count > 0)
|
||||
if (removeList is not null)
|
||||
{
|
||||
_configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
|
||||
.Except(removeList)
|
||||
|
@ -3124,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))
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -154,8 +155,8 @@ namespace Emby.Server.Implementations.Library
|
|||
// If file is strm or main media stream is missing, force a metadata refresh with remote probing
|
||||
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
|
||||
&& (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
|
||||
|| (item.MediaType == MediaType.Video && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Video))
|
||||
|| (item.MediaType == MediaType.Audio && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio))))
|
||||
|| (item.MediaType == MediaType.Video && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Video))
|
||||
|| (item.MediaType == MediaType.Audio && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Audio))))
|
||||
{
|
||||
await item.RefreshMetadata(
|
||||
new MetadataRefreshOptions(_directoryService)
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using MediaBrowser.Common.Providers;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
|
@ -86,24 +87,8 @@ namespace Emby.Server.Implementations.Library
|
|||
return false;
|
||||
}
|
||||
|
||||
char oldDirectorySeparatorChar;
|
||||
char newDirectorySeparatorChar;
|
||||
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
|
||||
// The reasoning behind this is that a forward slash likely means it's a Linux path and
|
||||
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
|
||||
if (newSubPath.Contains('/', StringComparison.Ordinal))
|
||||
{
|
||||
oldDirectorySeparatorChar = '\\';
|
||||
newDirectorySeparatorChar = '/';
|
||||
}
|
||||
else
|
||||
{
|
||||
oldDirectorySeparatorChar = '/';
|
||||
newDirectorySeparatorChar = '\\';
|
||||
}
|
||||
|
||||
path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
|
||||
subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
|
||||
subPath = subPath.NormalizePath(out var newDirectorySeparatorChar);
|
||||
path = path.NormalizePath(newDirectorySeparatorChar);
|
||||
|
||||
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
|
||||
// when the sub path matches a similar but in-complete subpath
|
||||
|
@ -127,5 +112,82 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the full resolved path and normalizes path separators to the <see cref="Path.DirectorySeparatorChar"/>.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to canonicalize.</param>
|
||||
/// <returns>The fully expanded, normalized path.</returns>
|
||||
public static string Canonicalize(this string path)
|
||||
{
|
||||
return Path.GetFullPath(path).NormalizePath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the path's directory separator character to the currently defined <see cref="Path.DirectorySeparatorChar"/>.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to normalize.</param>
|
||||
/// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
|
||||
[return: NotNullIfNotNull(nameof(path))]
|
||||
public static string? NormalizePath(this string? path)
|
||||
{
|
||||
return path.NormalizePath(Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the path's directory separator character.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to normalize.</param>
|
||||
/// <param name="separator">The separator character the path now uses or <see langword="null"/>.</param>
|
||||
/// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
|
||||
[return: NotNullIfNotNull(nameof(path))]
|
||||
public static string? NormalizePath(this string? path, out char separator)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
separator = default;
|
||||
return path;
|
||||
}
|
||||
|
||||
var newSeparator = '\\';
|
||||
|
||||
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
|
||||
// The reasoning behind this is that a forward slash likely means it's a Linux path and
|
||||
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
|
||||
if (path.Contains('/', StringComparison.Ordinal))
|
||||
{
|
||||
newSeparator = '/';
|
||||
}
|
||||
|
||||
separator = newSeparator;
|
||||
|
||||
return path.NormalizePath(newSeparator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the path's directory separator character to the specified character.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to normalize.</param>
|
||||
/// <param name="newSeparator">The replacement directory separator character. Must be a valid directory separator.</param>
|
||||
/// <returns>The normalized path.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the new separator character is not a directory separator.</exception>
|
||||
[return: NotNullIfNotNull(nameof(path))]
|
||||
public static string? NormalizePath(this string? path, char newSeparator)
|
||||
{
|
||||
const char Bs = '\\';
|
||||
const char Fs = '/';
|
||||
|
||||
if (!(newSeparator == Bs || newSeparator == Fs))
|
||||
{
|
||||
throw new ArgumentException("The character must be a directory separator.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return newSeparator == Bs ? path.Replace(Fs, newSeparator) : path.Replace(Bs, newSeparator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -158,7 +158,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
|
||||
{
|
||||
var files = new List<FileSystemMetadata>();
|
||||
var items = new List<BaseItem>();
|
||||
var leftOver = new List<FileSystemMetadata>();
|
||||
|
||||
// Loop through each child file/folder and see if we find a video
|
||||
|
@ -180,7 +179,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
var result = new MultiItemResolverResult
|
||||
{
|
||||
ExtraFiles = leftOver,
|
||||
Items = items
|
||||
Items = new List<BaseItem>()
|
||||
};
|
||||
|
||||
var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent);
|
||||
|
@ -193,7 +192,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
continue;
|
||||
}
|
||||
|
||||
if (resolvedItem.Files.Count == 0)
|
||||
// Until multi-part books are handled letting files stack hides them from browsing in the client
|
||||
if (resolvedItem.Files.Count == 0 || resolvedItem.Extras.Count > 0 || resolvedItem.AlternateVersions.Count > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -25,16 +26,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
{
|
||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_namingOptions = namingOptions;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -51,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)
|
||||
|
@ -109,7 +113,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
}
|
||||
|
||||
// If args contains music it's a music album
|
||||
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
|
||||
if (ContainsMusic(args.FileSystemChildren, true, _directoryService))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,10 @@ 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;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -18,19 +20,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
public class MusicArtistResolver : ItemResolver<MusicArtist>
|
||||
{
|
||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||
private NamingOptions _namingOptions;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
|
||||
/// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public MusicArtistResolver(
|
||||
ILogger<MusicAlbumResolver> logger,
|
||||
NamingOptions namingOptions)
|
||||
NamingOptions namingOptions,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_namingOptions = namingOptions;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -59,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)
|
||||
|
@ -78,9 +84,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
return null;
|
||||
}
|
||||
|
||||
var directoryService = args.DirectoryService;
|
||||
|
||||
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
|
||||
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
|
||||
|
||||
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
|
||||
|
||||
|
@ -97,7 +101,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
}
|
||||
|
||||
// If we contain a music album assume we are an artist folder
|
||||
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
|
||||
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
|
||||
{
|
||||
// Stop once we see a music album
|
||||
state.Stop();
|
||||
|
|
|
@ -25,14 +25,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions)
|
||||
protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
NamingOptions = namingOptions;
|
||||
DirectoryService = directoryService;
|
||||
}
|
||||
|
||||
protected NamingOptions NamingOptions { get; }
|
||||
|
||||
protected IDirectoryService DirectoryService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
|
@ -65,13 +68,26 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
var filename = child.Name;
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
|
||||
if (IsDvdDirectory(child.FullName, filename, DirectoryService))
|
||||
{
|
||||
videoType = VideoType.Dvd;
|
||||
var videoTmp = new TVideoType
|
||||
{
|
||||
Path = args.Path,
|
||||
VideoType = VideoType.Dvd
|
||||
};
|
||||
Set3DFormat(videoTmp);
|
||||
return videoTmp;
|
||||
}
|
||||
else if (IsBluRayDirectory(filename))
|
||||
|
||||
if (IsBluRayDirectory(filename))
|
||||
{
|
||||
videoType = VideoType.BluRay;
|
||||
var videoTmp = new TVideoType
|
||||
{
|
||||
Path = args.Path,
|
||||
VideoType = VideoType.BluRay
|
||||
};
|
||||
Set3DFormat(videoTmp);
|
||||
return videoTmp;
|
||||
}
|
||||
}
|
||||
else if (IsDvdFile(filename))
|
||||
|
@ -247,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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,6 +4,8 @@ using System.IO;
|
|||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -14,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
/// <summary>
|
||||
/// Resolves a Path into a Video or Video subclass.
|
||||
/// </summary>
|
||||
internal class ExtraResolver
|
||||
internal class ExtraResolver : BaseVideoResolver<Video>
|
||||
{
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IItemResolver[] _trailerResolvers;
|
||||
|
@ -25,11 +27,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
|
||||
public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions)
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
_namingOptions = namingOptions;
|
||||
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) };
|
||||
_videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) };
|
||||
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
|
||||
_videoResolvers = new IItemResolver[] { this };
|
||||
}
|
||||
|
||||
protected override Video Resolve(ItemResolveArgs args)
|
||||
{
|
||||
return ResolveVideo<Video>(args, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
|
@ -18,8 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions)
|
||||
: base(logger, namingOptions)
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -24,17 +25,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
/// <summary>
|
||||
/// Class MovieResolver.
|
||||
/// </summary>
|
||||
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
|
||||
public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
|
||||
{
|
||||
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>
|
||||
|
@ -43,8 +44,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions)
|
||||
: base(logger, namingOptions)
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
}
|
||||
|
@ -55,11 +57,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.Fourth;
|
||||
|
||||
[GeneratedRegex(@"\bsample\b", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex IsIgnoredRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public MultiItemResolverResult ResolveMultiple(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
string collectionType,
|
||||
CollectionType? collectionType,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
var result = ResolveMultipleInternal(parent, files, collectionType);
|
||||
|
@ -95,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, args.DirectoryService, collectionType, false);
|
||||
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
|
||||
if (collectionType == CollectionType.HomeVideos)
|
||||
{
|
||||
movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||
movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
if (collectionType is null)
|
||||
{
|
||||
// Owned items will be caught by the video extra resolver
|
||||
if (args.Parent is null)
|
||||
|
@ -118,12 +123,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
return null;
|
||||
}
|
||||
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
||||
if (collectionType == CollectionType.Movies)
|
||||
{
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
|
||||
}
|
||||
|
||||
// ignore extras
|
||||
|
@ -142,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>())
|
||||
{
|
||||
|
@ -184,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)
|
||||
|
@ -218,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);
|
||||
}
|
||||
|
@ -235,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)
|
||||
|
@ -260,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
{
|
||||
leftOver.Add(child);
|
||||
}
|
||||
else if (!IsIgnored(child.Name))
|
||||
else if (!IsIgnoredRegex().IsMatch(child.Name))
|
||||
{
|
||||
files.Add(child);
|
||||
}
|
||||
|
@ -313,9 +316,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
return result;
|
||||
}
|
||||
|
||||
private static bool IsIgnored(ReadOnlySpan<char> filename)
|
||||
=> Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
|
||||
{
|
||||
for (var i = 0; i < result.Count; i++)
|
||||
|
@ -397,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
|
||||
|
@ -459,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;
|
||||
|
@ -561,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)
|
||||
{
|
||||
|
@ -571,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
{
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
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;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class PhotoResolver.
|
||||
/// </summary>
|
||||
public class PhotoResolver : ItemResolver<Photo>
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
private static readonly string[] _ignoreFiles = new[]
|
||||
{
|
||||
"folder",
|
||||
"thumb",
|
||||
|
@ -35,10 +36,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
"default"
|
||||
};
|
||||
|
||||
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PhotoResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
_namingOptions = namingOptions;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -46,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 = args.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)
|
||||
{
|
||||
|
@ -82,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
@ -30,7 +31,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
{
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
// It's a boxset if the path is a directory with [playlist] in it's the name
|
||||
// It's a boxset if the path is a directory with [playlist] in its name
|
||||
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
|
@ -42,7 +43,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
return new Playlist
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
|
||||
Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim(),
|
||||
OpenAccess = true
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -53,14 +55,15 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
return new Playlist
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = filename
|
||||
Name = filename,
|
||||
OpenAccess = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
|
@ -70,7 +73,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
Path = args.Path,
|
||||
Name = Path.GetFileNameWithoutExtension(args.Path),
|
||||
IsInMixedFolder = true,
|
||||
PlaylistMediaType = MediaType.Audio
|
||||
PlaylistMediaType = MediaType.Audio,
|
||||
OpenAccess = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
using System;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
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;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -20,8 +22,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions)
|
||||
: base(logger, namingOptions)
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -46,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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
@ -81,14 +81,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
if (season.IndexNumber.HasValue)
|
||||
{
|
||||
var seasonNumber = season.IndexNumber.Value;
|
||||
|
||||
season.Name = seasonNumber == 0 ?
|
||||
args.LibraryOptions.SeasonZeroDisplayName :
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("NameSeasonNumber"),
|
||||
seasonNumber,
|
||||
args.LibraryOptions.PreferredMetadataLanguage);
|
||||
if (string.IsNullOrEmpty(season.Name))
|
||||
{
|
||||
var seasonNames = series.SeasonNames;
|
||||
if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
|
||||
{
|
||||
season.Name = seasonName;
|
||||
}
|
||||
else
|
||||
{
|
||||
season.Name = seasonNumber == 0 ?
|
||||
args.LibraryOptions.SeasonZeroDisplayName :
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("NameSeasonNumber"),
|
||||
seasonNumber,
|
||||
args.LibraryOptions.PreferredMetadataLanguage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return season;
|
||||
|
|
|
@ -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"))
|
||||
{
|
||||
|
@ -184,6 +185,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
{
|
||||
var justName = Path.GetFileName(path.AsSpan());
|
||||
|
||||
var imdbId = justName.GetAttributeValue("imdbid");
|
||||
if (!string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.Imdb, imdbId);
|
||||
}
|
||||
|
||||
var tvdbId = justName.GetAttributeValue("tvdbid");
|
||||
if (!string.IsNullOrEmpty(tvdbId))
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
@ -46,10 +45,9 @@ namespace Emby.Server.Implementations.Library
|
|||
public Folder[] GetUserViews(UserViewQuery query)
|
||||
{
|
||||
var user = _userManager.GetUserById(query.UserId);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
throw new ArgumentException("User Id specified in the query does not exist.", nameof(query));
|
||||
throw new ArgumentException("User id specified in the query does not exist.", nameof(query));
|
||||
}
|
||||
|
||||
var folders = _libraryManager.GetUserRootFolder()
|
||||
|
@ -58,7 +56,6 @@ namespace Emby.Server.Implementations.Library
|
|||
.ToList();
|
||||
|
||||
var groupedFolders = new List<ICollectionFolder>();
|
||||
|
||||
var list = new List<Folder>();
|
||||
|
||||
foreach (var folder in folders)
|
||||
|
@ -66,6 +63,20 @@ namespace Emby.Server.Implementations.Library
|
|||
var collectionFolder = folder as ICollectionFolder;
|
||||
var folderViewType = collectionFolder?.CollectionType;
|
||||
|
||||
// Playlist library requires special handling because the folder only references user playlists
|
||||
if (folderViewType == CollectionType.Playlists)
|
||||
{
|
||||
var items = folder.GetItemList(new InternalItemsQuery(user)
|
||||
{
|
||||
ParentId = folder.ParentId
|
||||
});
|
||||
|
||||
if (!items.Any(item => item.IsVisible(user)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (UserView.IsUserSpecific(folder))
|
||||
{
|
||||
list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null));
|
||||
|
@ -78,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));
|
||||
}
|
||||
|
@ -90,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));
|
||||
}
|
||||
|
@ -111,10 +122,10 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (query.IncludeExternalContent)
|
||||
{
|
||||
var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery
|
||||
var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
|
||||
{
|
||||
UserId = query.UserId
|
||||
});
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
var channels = channelResult.Items;
|
||||
|
||||
|
@ -132,14 +143,12 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
|
||||
|
||||
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
|
||||
|
||||
return list
|
||||
.OrderBy(i =>
|
||||
{
|
||||
var index = Array.IndexOf(orders, i.Id);
|
||||
|
||||
if (index == -1
|
||||
&& i is UserView view
|
||||
&& !view.DisplayParentId.Equals(default))
|
||||
|
@ -154,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);
|
||||
|
||||
|
@ -170,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];
|
||||
}
|
||||
|
@ -190,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);
|
||||
}
|
||||
|
@ -270,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;
|
||||
}
|
||||
|
@ -286,7 +295,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (parents.Count == 0)
|
||||
{
|
||||
return new List<BaseItem>();
|
||||
return Array.Empty<BaseItem>();
|
||||
}
|
||||
|
||||
if (includeItemTypes.Length == 0)
|
||||
|
@ -296,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)
|
||||
{
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue