Merge branch 'master' into bug/authorization-header-issue
This commit is contained in:
commit
2b232df07f
|
@ -7,7 +7,7 @@ parameters:
|
||||||
default: "ubuntu-latest"
|
default: "ubuntu-latest"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.100
|
default: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: CompatibilityCheck
|
- job: CompatibilityCheck
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
parameters:
|
|
||||||
- name: LinuxImage
|
|
||||||
type: string
|
|
||||||
default: "ubuntu-latest"
|
|
||||||
- name: GeneratorVersion
|
|
||||||
type: string
|
|
||||||
default: "5.0.0-beta2"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- job: GenerateApiClients
|
|
||||||
displayName: 'Generate Api Clients'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
|
||||||
dependsOn: Test
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: "${{ parameters.LinuxImage }}"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: DownloadPipelineArtifact@2
|
|
||||||
displayName: 'Download OpenAPI Spec Artifact'
|
|
||||||
inputs:
|
|
||||||
source: 'current'
|
|
||||||
artifact: "OpenAPI Spec"
|
|
||||||
path: "$(System.ArtifactsDirectory)/openapispec"
|
|
||||||
runVersion: "latest"
|
|
||||||
|
|
||||||
- task: CmdLine@2
|
|
||||||
displayName: 'Download OpenApi Generator'
|
|
||||||
inputs:
|
|
||||||
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
|
|
||||||
|
|
||||||
## Authenticate with npm registry
|
|
||||||
- task: npmAuthenticate@0
|
|
||||||
inputs:
|
|
||||||
workingFile: ./.npmrc
|
|
||||||
customEndpoint: 'jellyfin-bot for NPM'
|
|
||||||
|
|
||||||
## Generate npm api client
|
|
||||||
- task: CmdLine@2
|
|
||||||
displayName: 'Build stable typescript axios client'
|
|
||||||
inputs:
|
|
||||||
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
|
|
||||||
|
|
||||||
## Run npm install
|
|
||||||
- task: Npm@1
|
|
||||||
displayName: 'Install npm dependencies'
|
|
||||||
inputs:
|
|
||||||
command: install
|
|
||||||
workingDir: ./apiclient/generated/typescript/axios
|
|
||||||
|
|
||||||
## Publish npm packages
|
|
||||||
- task: Npm@1
|
|
||||||
displayName: 'Publish stable typescript axios client'
|
|
||||||
inputs:
|
|
||||||
command: custom
|
|
||||||
customCommand: publish --access public
|
|
||||||
publishRegistry: useExternalRegistry
|
|
||||||
publishEndpoint: 'jellyfin-bot for NPM'
|
|
||||||
workingDir: ./apiclient/generated/typescript/axios
|
|
|
@ -1,7 +1,7 @@
|
||||||
parameters:
|
parameters:
|
||||||
LinuxImage: 'ubuntu-latest'
|
LinuxImage: 'ubuntu-latest'
|
||||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
DotNetSdkVersion: 5.0.100
|
DotNetSdkVersion: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Build
|
- job: Build
|
||||||
|
|
|
@ -22,6 +22,12 @@ jobs:
|
||||||
BuildConfiguration: ubuntu.armhf
|
BuildConfiguration: ubuntu.armhf
|
||||||
Linux.amd64:
|
Linux.amd64:
|
||||||
BuildConfiguration: linux.amd64
|
BuildConfiguration: linux.amd64
|
||||||
|
Linux.amd64-musl:
|
||||||
|
BuildConfiguration: linux.amd64-musl
|
||||||
|
Linux.arm64:
|
||||||
|
BuildConfiguration: linux.arm64
|
||||||
|
Linux.armhf:
|
||||||
|
BuildConfiguration: linux.armhf
|
||||||
Windows.amd64:
|
Windows.amd64:
|
||||||
BuildConfiguration: windows.amd64
|
BuildConfiguration: windows.amd64
|
||||||
MacOS:
|
MacOS:
|
||||||
|
@ -154,7 +160,6 @@ jobs:
|
||||||
dependsOn:
|
dependsOn:
|
||||||
- BuildPackage
|
- BuildPackage
|
||||||
- BuildDocker
|
- BuildDocker
|
||||||
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
|
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
@ -180,13 +185,14 @@ jobs:
|
||||||
|
|
||||||
- job: PublishNuget
|
- job: PublishNuget
|
||||||
displayName: 'Publish NuGet packages'
|
displayName: 'Publish NuGet packages'
|
||||||
dependsOn:
|
|
||||||
- BuildPackage
|
|
||||||
condition: succeeded('BuildPackage')
|
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: JellyfinVersion
|
||||||
|
value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Use .NET 5.0 sdk'
|
displayName: 'Use .NET 5.0 sdk'
|
||||||
|
@ -198,12 +204,19 @@ jobs:
|
||||||
displayName: 'Build Stable Nuget packages'
|
displayName: 'Build Stable Nuget packages'
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
inputs:
|
inputs:
|
||||||
command: 'pack'
|
command: 'custom'
|
||||||
packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
|
projects: |
|
||||||
versioningScheme: 'off'
|
Jellyfin.Data/Jellyfin.Data.csproj
|
||||||
|
MediaBrowser.Common/MediaBrowser.Common.csproj
|
||||||
|
MediaBrowser.Controller/MediaBrowser.Controller.csproj
|
||||||
|
MediaBrowser.Model/MediaBrowser.Model.csproj
|
||||||
|
Emby.Naming/Emby.Naming.csproj
|
||||||
|
custom: 'pack'
|
||||||
|
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
- task: DotNetCoreCLI@2
|
||||||
displayName: 'Build Unstable Nuget packages'
|
displayName: 'Build Unstable Nuget packages'
|
||||||
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||||
inputs:
|
inputs:
|
||||||
command: 'custom'
|
command: 'custom'
|
||||||
projects: |
|
projects: |
|
||||||
|
@ -226,7 +239,7 @@ jobs:
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
inputs:
|
inputs:
|
||||||
command: 'push'
|
command: 'push'
|
||||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
|
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
|
||||||
nuGetFeedType: 'external'
|
nuGetFeedType: 'external'
|
||||||
publishFeedCredentials: 'NugetOrg'
|
publishFeedCredentials: 'NugetOrg'
|
||||||
allowPackageConflicts: true # This ignores an error if the version already exists
|
allowPackageConflicts: true # This ignores an error if the version already exists
|
||||||
|
|
|
@ -10,7 +10,7 @@ parameters:
|
||||||
default: "tests/**/*Tests.csproj"
|
default: "tests/**/*Tests.csproj"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.100
|
default: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Test
|
- job: Test
|
||||||
|
@ -94,5 +94,5 @@ jobs:
|
||||||
displayName: 'Publish OpenAPI Artifact'
|
displayName: 'Publish OpenAPI Artifact'
|
||||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||||
inputs:
|
inputs:
|
||||||
targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json"
|
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
|
||||||
artifactName: 'OpenAPI Spec'
|
artifactName: 'OpenAPI Spec'
|
||||||
|
|
|
@ -6,7 +6,7 @@ variables:
|
||||||
- name: RestoreBuildProjects
|
- name: RestoreBuildProjects
|
||||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
value: 5.0.100
|
value: 5.0.103
|
||||||
|
|
||||||
pr:
|
pr:
|
||||||
autoCancel: true
|
autoCancel: true
|
||||||
|
@ -61,6 +61,3 @@ jobs:
|
||||||
|
|
||||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
||||||
- template: azure-pipelines-package.yml
|
- template: azure-pipelines-package.yml
|
||||||
|
|
||||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
|
||||||
- template: azure-pipelines-api-client.yml
|
|
||||||
|
|
30
.drone.yml
30
.drone.yml
|
@ -1,30 +0,0 @@
|
||||||
---
|
|
||||||
kind: pipeline
|
|
||||||
name: build-debug
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: submodules
|
|
||||||
image: docker:git
|
|
||||||
commands:
|
|
||||||
- git submodule update --init --recursive
|
|
||||||
|
|
||||||
- name: build
|
|
||||||
image: microsoft/dotnet:2-sdk
|
|
||||||
commands:
|
|
||||||
- dotnet publish "Jellyfin.Server" --configuration Debug --output "../ci/ci-debug"
|
|
||||||
|
|
||||||
---
|
|
||||||
kind: pipeline
|
|
||||||
name: build-release
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: submodules
|
|
||||||
image: docker:git
|
|
||||||
commands:
|
|
||||||
- git submodule update --init --recursive
|
|
||||||
|
|
||||||
- name: build
|
|
||||||
image: microsoft/dotnet:2-sdk
|
|
||||||
commands:
|
|
||||||
- dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release"
|
|
||||||
|
|
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -33,7 +33,13 @@ assignees: ''
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
<!-- A clear and concise description of what you expected to happen. -->
|
<!-- A clear and concise description of what you expected to happen. -->
|
||||||
|
|
||||||
**Logs**
|
**Server Logs**
|
||||||
|
<!-- Please paste any log errors. -->
|
||||||
|
|
||||||
|
**FFmpeg Logs**
|
||||||
|
<!-- Please paste any log errors. -->
|
||||||
|
|
||||||
|
**Browser Console Logs**
|
||||||
<!-- Please paste any log errors. -->
|
<!-- Please paste any log errors. -->
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
|
|
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
|
@ -7,3 +7,9 @@ updates:
|
||||||
time: '12:00'
|
time: '12:00'
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
time: '12:00'
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
|
43
.github/label-commenter-config.yml
vendored
Normal file
43
.github/label-commenter-config.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
comment:
|
||||||
|
header: Hello!
|
||||||
|
footer: "\
|
||||||
|
---\n\n
|
||||||
|
> This is an automated comment created by the [peaceiris/actions-label-commenter]. \
|
||||||
|
Responding to the bot or mentioning it won't have any effect.\n\n
|
||||||
|
[peaceiris/actions-label-commenter]: https://github.com/peaceiris/actions-label-commenter
|
||||||
|
"
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- name: stable backport
|
||||||
|
labeled:
|
||||||
|
pr:
|
||||||
|
body: |
|
||||||
|
This pull request has been tagged as a stable backport. It will be cherry-picked into the next stable point release.
|
||||||
|
|
||||||
|
Please observe the following:
|
||||||
|
|
||||||
|
* Any dependent PRs that this PR requires **must** be tagged for stable backporting as well.
|
||||||
|
|
||||||
|
* Any issue(s) this PR fixes or closes **should** target the current stable release or a previous stable release to which a fix has not yet entered the current stable release.
|
||||||
|
|
||||||
|
* This PR **must** be test cherry-picked against the current release branch (`release-X.Y.z` where X and Y are numbers). It must apply cleanly, or a diff of the expected change must be provided.
|
||||||
|
|
||||||
|
To do this, run the following commands from your local copy of the Jellyfin repository:
|
||||||
|
|
||||||
|
1. `git checkout master`
|
||||||
|
|
||||||
|
1. `git merge --no-ff <myPullRequestBranch>`
|
||||||
|
|
||||||
|
1. `git log` -> `commit xxxxxxxxx`, grab hash
|
||||||
|
|
||||||
|
1. `git checkout release-X.Y.z` replacing X and Y with the *current* stable version (e.g. `release-10.7.z`)
|
||||||
|
|
||||||
|
1. `git cherry-pick -sx -m1 <hash>`
|
||||||
|
|
||||||
|
Ensure the `cherry-pick` applies cleanly. If it does not, fix any merge conflicts *preserving as much of the original code as possible*, and make note of the resulting diff.
|
||||||
|
|
||||||
|
Test your changes with a build to ensure they are successful. If not, adjust the diff accordingly.
|
||||||
|
|
||||||
|
**Do not** push your merges to either branch. Use `git reset --hard HEAD~1` to revert both branches to their original state.
|
||||||
|
|
||||||
|
Reply to this PR with a comment beginning "Cherry-pick test completed." and including the merge-conflict-fixing diff(s) if applicable.
|
64
.github/workflows/automation.yml
vendored
Normal file
64
.github/workflows/automation.yml
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
name: Automation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Does PR has the stable backport label?
|
||||||
|
uses: Dreamcodeio/does-pr-has-label@v1.2
|
||||||
|
id: checkLabel
|
||||||
|
with:
|
||||||
|
label: stable backport
|
||||||
|
|
||||||
|
- name: Remove from 'Current Release' project
|
||||||
|
uses: alex-page/github-project-automation-plus@v0.7.1
|
||||||
|
if: (github.event.pull_request || github.event.issue.pull_request) && !steps.checkLabel.outputs.hasLabel
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
project: Current Release
|
||||||
|
action: delete
|
||||||
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Add to 'Release Next' project
|
||||||
|
uses: alex-page/github-project-automation-plus@v0.7.1
|
||||||
|
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
project: Release Next
|
||||||
|
column: In progress
|
||||||
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Add to 'Current Release' project
|
||||||
|
uses: alex-page/github-project-automation-plus@v0.7.1
|
||||||
|
if: (github.event.pull_request || github.event.issue.pull_request) && steps.checkLabel.outputs.hasLabel
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
project: Current Release
|
||||||
|
column: In progress
|
||||||
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Check number of comments from the team member
|
||||||
|
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
|
||||||
|
id: member_comments
|
||||||
|
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
||||||
|
|
||||||
|
- name: Move issue to needs triage
|
||||||
|
uses: alex-page/github-project-automation-plus@v0.7.1
|
||||||
|
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
project: Issue Triage for Main Repo
|
||||||
|
column: Needs triage
|
||||||
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Add issue to triage project
|
||||||
|
uses: alex-page/github-project-automation-plus@v0.7.1
|
||||||
|
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
project: Issue Triage for Main Repo
|
||||||
|
column: Pending response
|
||||||
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
96
.github/workflows/check-backport.yml
vendored
Normal file
96
.github/workflows/check-backport.yml
vendored
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
name: Stable Backport Check
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types:
|
||||||
|
- created
|
||||||
|
- edited
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-backport:
|
||||||
|
name: Check Backport
|
||||||
|
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Notify as seen
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ github.event.comment.id }}
|
||||||
|
reactions: eyes
|
||||||
|
|
||||||
|
- name: Checkout the latest code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Notify as running
|
||||||
|
id: comment_running
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
Running backport tests...
|
||||||
|
|
||||||
|
- name: Perform test backport
|
||||||
|
id: run_tests
|
||||||
|
run: |
|
||||||
|
set +o errexit
|
||||||
|
git config --global user.name "Jellyfin Bot"
|
||||||
|
git config --global user.email "team@jellyfin.org"
|
||||||
|
CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
|
||||||
|
git checkout master
|
||||||
|
git merge --no-ff ${CURRENT_BRANCH}
|
||||||
|
MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
|
||||||
|
git fetch --all
|
||||||
|
CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
|
||||||
|
stable_branch="Current stable release branch: ${CURRENT_STABLE}"
|
||||||
|
echo ${stable_branch}
|
||||||
|
echo ::set-output name=branch::${stable_branch}
|
||||||
|
git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
|
||||||
|
git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
|
||||||
|
retcode=$?
|
||||||
|
cat output.txt | grep -v 'hint:'
|
||||||
|
output="$( grep -v 'hint:' output.txt )"
|
||||||
|
output="${output//'%'/'%25'}"
|
||||||
|
output="${output//$'\n'/'%0A'}"
|
||||||
|
output="${output//$'\r'/'%0D'}"
|
||||||
|
echo ::set-output name=output::$output
|
||||||
|
exit ${retcode}
|
||||||
|
|
||||||
|
- name: Notify with result success
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null && success() }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
||||||
|
body: |
|
||||||
|
${{ steps.run_tests.outputs.branch }}
|
||||||
|
Output from `git cherry-pick`:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
${{ steps.run_tests.outputs.output }}
|
||||||
|
reactions: hooray
|
||||||
|
|
||||||
|
- name: Notify with result failure
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null && failure() }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
||||||
|
body: |
|
||||||
|
${{ steps.run_tests.outputs.branch }}
|
||||||
|
Output from `git cherry-pick`:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
${{ steps.run_tests.outputs.output }}
|
||||||
|
reactions: confused
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '5.0.100'
|
dotnet-version: '5.0.x'
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
|
|
24
.github/workflows/label-commenter.yml
vendored
Normal file
24
.github/workflows/label-commenter.yml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
name: Label Commenter
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
- unlabeled
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
- unlabeled
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
|
||||||
|
- name: Label Commenter
|
||||||
|
uses: peaceiris/actions-label-commenter@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.JF_BOT_TOKEN }}
|
17
.github/workflows/merge-conflicts.yml
vendored
Normal file
17
.github/workflows/merge-conflicts.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: 'Merge Conflicts'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- synchronize
|
||||||
|
jobs:
|
||||||
|
triage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
||||||
|
with:
|
||||||
|
dirtyLabel: 'merge conflict'
|
||||||
|
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
30
.github/workflows/rebase.yml
vendored
Normal file
30
.github/workflows/rebase.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
name: Automatic Rebase
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types:
|
||||||
|
- created
|
||||||
|
- edited
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rebase:
|
||||||
|
name: Rebase
|
||||||
|
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Notify as seen
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ github.event.comment.id }}
|
||||||
|
reactions: '+1'
|
||||||
|
|
||||||
|
- name: Checkout the latest code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Automatic Rebase
|
||||||
|
uses: cirrus-actions/rebase@1.4
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
|
@ -17,6 +17,7 @@
|
||||||
- [bugfixin](https://github.com/bugfixin)
|
- [bugfixin](https://github.com/bugfixin)
|
||||||
- [chaosinnovator](https://github.com/chaosinnovator)
|
- [chaosinnovator](https://github.com/chaosinnovator)
|
||||||
- [ckcr4lyf](https://github.com/ckcr4lyf)
|
- [ckcr4lyf](https://github.com/ckcr4lyf)
|
||||||
|
- [cocool97](https://github.com/cocool97)
|
||||||
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
||||||
- [crankdoofus](https://github.com/crankdoofus)
|
- [crankdoofus](https://github.com/crankdoofus)
|
||||||
- [crobibero](https://github.com/crobibero)
|
- [crobibero](https://github.com/crobibero)
|
||||||
|
@ -49,6 +50,7 @@
|
||||||
- [h1nk](https://github.com/h1nk)
|
- [h1nk](https://github.com/h1nk)
|
||||||
- [hawken93](https://github.com/hawken93)
|
- [hawken93](https://github.com/hawken93)
|
||||||
- [HelloWorld017](https://github.com/HelloWorld017)
|
- [HelloWorld017](https://github.com/HelloWorld017)
|
||||||
|
- [ikomhoog](https://github.com/ikomhoog)
|
||||||
- [jftuga](https://github.com/jftuga)
|
- [jftuga](https://github.com/jftuga)
|
||||||
- [joern-h](https://github.com/joern-h)
|
- [joern-h](https://github.com/joern-h)
|
||||||
- [joshuaboniface](https://github.com/joshuaboniface)
|
- [joshuaboniface](https://github.com/joshuaboniface)
|
||||||
|
@ -68,6 +70,7 @@
|
||||||
- [marius-luca-87](https://github.com/marius-luca-87)
|
- [marius-luca-87](https://github.com/marius-luca-87)
|
||||||
- [mark-monteiro](https://github.com/mark-monteiro)
|
- [mark-monteiro](https://github.com/mark-monteiro)
|
||||||
- [Matt07211](https://github.com/Matt07211)
|
- [Matt07211](https://github.com/Matt07211)
|
||||||
|
- [Maxr1998](https://github.com/Maxr1998)
|
||||||
- [mcarlton00](https://github.com/mcarlton00)
|
- [mcarlton00](https://github.com/mcarlton00)
|
||||||
- [mitchfizz05](https://github.com/mitchfizz05)
|
- [mitchfizz05](https://github.com/mitchfizz05)
|
||||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||||
|
@ -80,6 +83,7 @@
|
||||||
- [nvllsvm](https://github.com/nvllsvm)
|
- [nvllsvm](https://github.com/nvllsvm)
|
||||||
- [nyanmisaka](https://github.com/nyanmisaka)
|
- [nyanmisaka](https://github.com/nyanmisaka)
|
||||||
- [OancaAndrei](https://github.com/OancaAndrei)
|
- [OancaAndrei](https://github.com/OancaAndrei)
|
||||||
|
- [obradovichv](https://github.com/obradovichv)
|
||||||
- [oddstr13](https://github.com/oddstr13)
|
- [oddstr13](https://github.com/oddstr13)
|
||||||
- [orryverducci](https://github.com/orryverducci)
|
- [orryverducci](https://github.com/orryverducci)
|
||||||
- [petermcneil](https://github.com/petermcneil)
|
- [petermcneil](https://github.com/petermcneil)
|
||||||
|
@ -103,10 +107,11 @@
|
||||||
- [shemanaev](https://github.com/shemanaev)
|
- [shemanaev](https://github.com/shemanaev)
|
||||||
- [skaro13](https://github.com/skaro13)
|
- [skaro13](https://github.com/skaro13)
|
||||||
- [sl1288](https://github.com/sl1288)
|
- [sl1288](https://github.com/sl1288)
|
||||||
|
- [Smith00101010](https://github.com/Smith00101010)
|
||||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||||
- [sparky8251](https://github.com/sparky8251)
|
- [sparky8251](https://github.com/sparky8251)
|
||||||
- [spookbits](https://github.com/spookbits)
|
- [spookbits](https://github.com/spookbits)
|
||||||
- [ssenart] (https://github.com/ssenart)
|
- [ssenart](https://github.com/ssenart)
|
||||||
- [stanionascu](https://github.com/stanionascu)
|
- [stanionascu](https://github.com/stanionascu)
|
||||||
- [stevehayles](https://github.com/stevehayles)
|
- [stevehayles](https://github.com/stevehayles)
|
||||||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||||
|
@ -141,6 +146,8 @@
|
||||||
- [Pusta](https://github.com/pusta)
|
- [Pusta](https://github.com/pusta)
|
||||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||||
- [skyfrk](https://github.com/skyfrk)
|
- [skyfrk](https://github.com/skyfrk)
|
||||||
|
- [ianjazz246](https://github.com/ianjazz246)
|
||||||
|
- [peterspenler](https://github.com/peterspenler)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=5.0
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
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 python \
|
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 - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& yarn install \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder
|
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=5.0
|
||||||
|
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
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 python \
|
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 - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& yarn install \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=5.0
|
||||||
|
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
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 python \
|
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 - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& yarn install \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
namespace Emby.Dlna.Configuration
|
namespace Emby.Dlna.Configuration
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#nullable enable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using Emby.Dlna.Configuration;
|
using Emby.Dlna.Configuration;
|
||||||
|
|
|
@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||||
{
|
{
|
||||||
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -7,7 +8,6 @@ using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Emby.Dlna.Configuration;
|
|
||||||
using Emby.Dlna.Didl;
|
using Emby.Dlna.Didl;
|
||||||
using Emby.Dlna.Service;
|
using Emby.Dlna.Service;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
@ -121,7 +121,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||||
{
|
{
|
||||||
if (xmlWriter == null)
|
if (xmlWriter == null)
|
||||||
{
|
{
|
||||||
|
@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a "XSetBookmark" element to the xml document.
|
/// Adds a "XSetBookmark" element to the xml document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
/// <param name="sparams">The method parameters.</param>
|
||||||
private void HandleXSetBookmark(IDictionary<string, string> sparams)
|
private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams)
|
||||||
{
|
{
|
||||||
var id = sparams["ObjectID"];
|
var id = sparams["ObjectID"];
|
||||||
|
|
||||||
|
@ -305,35 +305,18 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
|
||||||
/// <param name="key">The key.</param>
|
|
||||||
/// <param name="defaultValue">The defaultValue.</param>
|
|
||||||
/// <returns>The <see cref="string"/>.</returns>
|
|
||||||
public static string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue)
|
|
||||||
{
|
|
||||||
if (sparams != null && sparams.TryGetValue(key, out string val))
|
|
||||||
{
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds the "Browse" xml response.
|
/// Builds the "Browse" xml response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||||
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
/// <param name="sparams">The method parameters.</param>
|
||||||
/// <param name="deviceId">The device Id to use.</param>
|
/// <param name="deviceId">The device Id to use.</param>
|
||||||
private void HandleBrowse(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
|
private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
|
||||||
{
|
{
|
||||||
var id = sparams["ObjectID"];
|
var id = sparams["ObjectID"];
|
||||||
var flag = sparams["BrowseFlag"];
|
var flag = sparams["BrowseFlag"];
|
||||||
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
|
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
|
||||||
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
|
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
|
||||||
|
|
||||||
var provided = 0;
|
var provided = 0;
|
||||||
|
|
||||||
|
@ -435,9 +418,9 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
/// Builds the response to the "X_BrowseByLetter request.
|
/// Builds the response to the "X_BrowseByLetter request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||||
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
/// <param name="sparams">The method parameters.</param>
|
||||||
/// <param name="deviceId">The device id.</param>
|
/// <param name="deviceId">The device id.</param>
|
||||||
private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
|
private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
|
||||||
{
|
{
|
||||||
// TODO: Implement this method
|
// TODO: Implement this method
|
||||||
HandleSearch(xmlWriter, sparams, deviceId);
|
HandleSearch(xmlWriter, sparams, deviceId);
|
||||||
|
@ -447,13 +430,13 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
/// Builds a response to the "Search" request.
|
/// Builds a response to the "Search" request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param>
|
/// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param>
|
||||||
/// <param name="sparams">The sparams<see cref="IDictionary"/>.</param>
|
/// <param name="sparams">The method parameters.</param>
|
||||||
/// <param name="deviceId">The deviceId<see cref="string"/>.</param>
|
/// <param name="deviceId">The deviceId<see cref="string"/>.</param>
|
||||||
private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
|
private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
|
||||||
{
|
{
|
||||||
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
|
var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty));
|
||||||
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
|
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
|
||||||
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
|
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
|
||||||
|
|
||||||
// sort example: dc:title, dc:date
|
// sort example: dc:title, dc:date
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
#pragma warning disable SA1602
|
|
||||||
|
|
||||||
namespace Emby.Dlna.ContentDirectory
|
namespace Emby.Dlna.ContentDirectory
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -96,6 +98,7 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
|
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
|
||||||
{
|
{
|
||||||
|
// If this using are changed to single lines, then write.Flush needs to be appended before the return.
|
||||||
using (var writer = XmlWriter.Create(builder, settings))
|
using (var writer = XmlWriter.Create(builder, settings))
|
||||||
{
|
{
|
||||||
// writer.WriteStartDocument();
|
// writer.WriteStartDocument();
|
||||||
|
@ -207,7 +210,8 @@ namespace Emby.Dlna.Didl
|
||||||
var targetWidth = streamInfo.TargetWidth;
|
var targetWidth = streamInfo.TargetWidth;
|
||||||
var targetHeight = streamInfo.TargetHeight;
|
var targetHeight = streamInfo.TargetHeight;
|
||||||
|
|
||||||
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
|
var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
|
||||||
|
_profile,
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
|
@ -598,7 +602,8 @@ namespace Emby.Dlna.Didl
|
||||||
? MimeTypes.GetMimeType(filename)
|
? MimeTypes.GetMimeType(filename)
|
||||||
: mediaProfile.MimeType;
|
: mediaProfile.MimeType;
|
||||||
|
|
||||||
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
|
var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
|
||||||
|
_profile,
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
targetAudioBitrate,
|
targetAudioBitrate,
|
||||||
|
@ -973,15 +978,28 @@ namespace Emby.Dlna.Didl
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
|
// TODO: Remove these default values
|
||||||
|
var albumArtUrlInfo = GetImageUrl(
|
||||||
|
imageInfo,
|
||||||
|
_profile.MaxAlbumArtWidth ?? 10000,
|
||||||
|
_profile.MaxAlbumArtHeight ?? 10000,
|
||||||
|
"jpg");
|
||||||
|
|
||||||
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
|
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
|
||||||
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
|
if (!string.IsNullOrEmpty(_profile.AlbumArtPn))
|
||||||
writer.WriteString(albumartUrlInfo.url);
|
{
|
||||||
|
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteString(albumArtUrlInfo.url);
|
||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
|
|
||||||
// TOOD: Remove these default values
|
// TODO: Remove these default values
|
||||||
var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
|
var iconUrlInfo = GetImageUrl(
|
||||||
|
imageInfo,
|
||||||
|
_profile.MaxIconWidth ?? 48,
|
||||||
|
_profile.MaxIconHeight ?? 48,
|
||||||
|
"jpg");
|
||||||
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
|
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
|
||||||
|
|
||||||
if (!_profile.EnableAlbumArtInDidl)
|
if (!_profile.EnableAlbumArtInDidl)
|
||||||
|
@ -1032,8 +1050,7 @@ namespace Emby.Dlna.Didl
|
||||||
var width = albumartUrlInfo.width ?? maxWidth;
|
var width = albumartUrlInfo.width ?? maxWidth;
|
||||||
var height = albumartUrlInfo.height ?? maxHeight;
|
var height = albumartUrlInfo.height ?? maxHeight;
|
||||||
|
|
||||||
var contentFeatures = new ContentFeatureBuilder(_profile)
|
var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
|
||||||
.BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
|
|
||||||
|
|
||||||
writer.WriteAttributeString(
|
writer.WriteAttributeString(
|
||||||
"protocolInfo",
|
"protocolInfo",
|
||||||
|
@ -1205,8 +1222,7 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
if (width.HasValue && height.HasValue)
|
if (width.HasValue && height.HasValue)
|
||||||
{
|
{
|
||||||
var newSize = DrawingUtils.Resize(
|
var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
|
||||||
new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
|
|
||||||
|
|
||||||
width = newSize.Width;
|
width = newSize.Width;
|
||||||
height = newSize.Height;
|
height = newSize.Height;
|
||||||
|
|
|
@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
|
||||||
{
|
{
|
||||||
public class StringWriterWithEncoding : StringWriter
|
public class StringWriterWithEncoding : StringWriter
|
||||||
{
|
{
|
||||||
private readonly Encoding _encoding;
|
private readonly Encoding? _encoding;
|
||||||
|
|
||||||
public StringWriterWithEncoding()
|
public StringWriterWithEncoding()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#nullable enable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -7,12 +9,14 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna.Profiles;
|
using Emby.Dlna.Profiles;
|
||||||
using Emby.Dlna.Server;
|
using Emby.Dlna.Server;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
|
@ -32,9 +36,9 @@ namespace Emby.Dlna
|
||||||
private readonly IXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly ILogger<DlnaManager> _logger;
|
private readonly ILogger<DlnaManager> _logger;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||||
|
|
||||||
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
@ -43,14 +47,12 @@ namespace Emby.Dlna
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IApplicationPaths appPaths,
|
IApplicationPaths appPaths,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IServerApplicationHost appHost)
|
IServerApplicationHost appHost)
|
||||||
{
|
{
|
||||||
_xmlSerializer = xmlSerializer;
|
_xmlSerializer = xmlSerializer;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
_logger = loggerFactory.CreateLogger<DlnaManager>();
|
_logger = loggerFactory.CreateLogger<DlnaManager>();
|
||||||
_jsonSerializer = jsonSerializer;
|
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +113,7 @@ namespace Emby.Dlna
|
||||||
|
|
||||||
if (profile != null)
|
if (profile != null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -126,92 +128,57 @@ namespace Emby.Dlna
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
||||||
builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
|
builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
|
||||||
builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
|
builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
|
||||||
builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
|
builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
|
||||||
builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
|
builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
|
||||||
builder.Append("ModelName:").AppendLine(profile.ModelName);
|
builder.Append("ModelName: ").AppendLine(profile.ModelName);
|
||||||
builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
|
builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
|
||||||
builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
|
builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
|
||||||
builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
|
builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
|
||||||
|
|
||||||
_logger.LogInformation(builder.ToString());
|
_logger.LogInformation(builder.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
/// <summary>
|
||||||
|
/// Attempts to match a device with a profile.
|
||||||
|
/// Rules:
|
||||||
|
/// - If the profile field has no value, the field matches irregardless of its contents.
|
||||||
|
/// - the profile field can be an exact match, or a reg exp.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
|
||||||
|
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
|
||||||
|
/// <returns><b>True</b> if they match.</returns>
|
||||||
|
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
|
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
|
||||||
{
|
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
|
||||||
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
|
||||||
{
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
|
||||||
return false;
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
|
||||||
}
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
|
||||||
}
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
|
||||||
|
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
|
||||||
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
|
|
||||||
{
|
|
||||||
if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelName))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
|
|
||||||
{
|
|
||||||
if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(pattern))
|
||||||
|
{
|
||||||
|
// In profile identification: An empty pattern matches anything.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
// The profile contains a value, and the device doesn't.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
|
@ -333,7 +300,12 @@ namespace Emby.Dlna
|
||||||
throw new ArgumentNullException(nameof(id));
|
throw new ArgumentNullException(nameof(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (info == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return ParseProfileFile(info.Path, info.Info.Type);
|
return ParseProfileFile(info.Path, info.Info.Type);
|
||||||
}
|
}
|
||||||
|
@ -395,7 +367,8 @@ namespace Emby.Dlna
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(systemProfilesPath);
|
Directory.CreateDirectory(systemProfilesPath);
|
||||||
|
|
||||||
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -495,9 +468,9 @@ namespace Emby.Dlna
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = _jsonSerializer.SerializeToString(profile);
|
var json = JsonSerializer.Serialize(profile, _jsonOptions);
|
||||||
|
|
||||||
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
|
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
|
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
|
||||||
|
@ -553,7 +526,7 @@ namespace Emby.Dlna
|
||||||
|
|
||||||
private void DumpProfiles()
|
private void DumpProfiles()
|
||||||
{
|
{
|
||||||
DeviceProfile[] list = new []
|
DeviceProfile[] list = new[]
|
||||||
{
|
{
|
||||||
new SamsungSmartTvProfile(),
|
new SamsungSmartTvProfile(),
|
||||||
new XboxOneProfile(),
|
new XboxOneProfile(),
|
||||||
|
|
|
@ -21,11 +21,11 @@
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
|
@ -78,9 +78,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -5,10 +7,10 @@ using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna.PlayTo;
|
using Emby.Dlna.PlayTo;
|
||||||
using Emby.Dlna.Ssdp;
|
using Emby.Dlna.Ssdp;
|
||||||
|
using Jellyfin.Networking.Configuration;
|
||||||
using Jellyfin.Networking.Manager;
|
using Jellyfin.Networking.Manager;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
|
@ -52,6 +54,8 @@ namespace Emby.Dlna.Main
|
||||||
private readonly ISocketFactory _socketFactory;
|
private readonly ISocketFactory _socketFactory;
|
||||||
private readonly INetworkManager _networkManager;
|
private readonly INetworkManager _networkManager;
|
||||||
private readonly object _syncLock = new object();
|
private readonly object _syncLock = new object();
|
||||||
|
private readonly NetworkConfiguration _netConfig;
|
||||||
|
private readonly bool _disabled;
|
||||||
|
|
||||||
private PlayToManager _manager;
|
private PlayToManager _manager;
|
||||||
private SsdpDevicePublisher _publisher;
|
private SsdpDevicePublisher _publisher;
|
||||||
|
@ -122,10 +126,23 @@ namespace Emby.Dlna.Main
|
||||||
httpClientFactory,
|
httpClientFactory,
|
||||||
config);
|
config);
|
||||||
Current = this;
|
Current = this;
|
||||||
|
|
||||||
|
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
||||||
|
_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; }
|
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 IContentDirectory ContentDirectory { get; private set; }
|
||||||
|
|
||||||
public IConnectionManager ConnectionManager { get; private set; }
|
public IConnectionManager ConnectionManager { get; private set; }
|
||||||
|
@ -136,6 +153,12 @@ namespace Emby.Dlna.Main
|
||||||
{
|
{
|
||||||
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
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();
|
ReloadComponents();
|
||||||
|
|
||||||
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
||||||
|
@ -152,6 +175,7 @@ namespace Emby.Dlna.Main
|
||||||
private void ReloadComponents()
|
private void ReloadComponents()
|
||||||
{
|
{
|
||||||
var options = _config.GetDlnaConfiguration();
|
var options = _config.GetDlnaConfiguration();
|
||||||
|
Enabled = options.EnableServer;
|
||||||
|
|
||||||
StartSsdpHandler();
|
StartSsdpHandler();
|
||||||
|
|
||||||
|
@ -206,7 +230,10 @@ namespace Emby.Dlna.Main
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
if (communicationsServer != null)
|
||||||
|
{
|
||||||
|
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -290,12 +317,18 @@ namespace Emby.Dlna.Main
|
||||||
|
|
||||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
||||||
|
|
||||||
var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
|
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
|
||||||
|
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
|
||||||
|
{
|
||||||
|
// DLNA will only work over http, so we must reset to http:// : {port}.
|
||||||
|
uri.Scheme = "http";
|
||||||
|
uri.Port = _netConfig.HttpServerPortNumber;
|
||||||
|
}
|
||||||
|
|
||||||
var device = new SsdpRootDevice
|
var device = new SsdpRootDevice
|
||||||
{
|
{
|
||||||
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
|
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
|
||||||
Location = uri, // Must point to the URL that serves your devices UPnP description document.
|
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
|
||||||
Address = address.Address,
|
Address = address.Address,
|
||||||
PrefixLength = address.PrefixLength,
|
PrefixLength = address.PrefixLength,
|
||||||
FriendlyName = "Jellyfin",
|
FriendlyName = "Jellyfin",
|
||||||
|
|
|
@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||||
{
|
{
|
||||||
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
using Emby.Dlna.Service;
|
using Emby.Dlna.Service;
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -219,7 +221,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
@ -235,7 +237,13 @@ namespace Emby.Dlna.PlayTo
|
||||||
_logger.LogDebug("Setting mute");
|
_logger.LogDebug("Setting mute");
|
||||||
var value = mute ? 1 : 0;
|
var value = mute ? 1 : 0;
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
rendererCommands.BuildPost(command, service.ServiceType, value),
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
IsMuted = mute;
|
IsMuted = mute;
|
||||||
|
@ -253,7 +261,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -270,7 +278,13 @@ namespace Emby.Dlna.PlayTo
|
||||||
// Remote control will perform better
|
// Remote control will perform better
|
||||||
Volume = value;
|
Volume = value;
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
rendererCommands.BuildPost(command, service.ServiceType, value),
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,7 +292,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -291,7 +305,13 @@ namespace Emby.Dlna.PlayTo
|
||||||
throw new InvalidOperationException("Unable to find service");
|
throw new InvalidOperationException("Unable to find service");
|
||||||
}
|
}
|
||||||
|
|
||||||
await new SsdpHttpClient(_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"))
|
await new SsdpHttpClient(_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"),
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
|
@ -305,7 +325,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -325,14 +345,21 @@ namespace Emby.Dlna.PlayTo
|
||||||
}
|
}
|
||||||
|
|
||||||
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
post,
|
||||||
|
header: header,
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
await Task.Delay(50).ConfigureAwait(false);
|
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
|
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
@ -343,6 +370,42 @@ namespace Emby.Dlna.PlayTo
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
url = url.Replace("&", "&", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
_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));
|
||||||
|
if (command == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dictionary = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "NextURI", url },
|
||||||
|
{ "NextURIMetaData", CreateDidlMeta(metaData) }
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
|
if (service == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unable to find service");
|
||||||
|
}
|
||||||
|
|
||||||
|
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
||||||
|
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
private static string CreateDidlMeta(string value)
|
private static string CreateDidlMeta(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
|
@ -378,6 +441,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
public async Task SetPlay(CancellationToken cancellationToken)
|
public async Task SetPlay(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (avCommands == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -388,7 +455,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -396,7 +463,13 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
avCommands.BuildPost(command, service.ServiceType, 1),
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
|
@ -406,7 +479,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -414,7 +487,13 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
avCommands.BuildPost(command, service.ServiceType, 1),
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
TransportState = TransportState.Paused;
|
TransportState = TransportState.Paused;
|
||||||
|
@ -528,7 +607,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -578,7 +657,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -665,6 +744,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
}
|
}
|
||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (rendererCommands == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
|
@ -733,6 +816,11 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (rendererCommands == null)
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
|
@ -914,6 +1002,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
||||||
|
|
||||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
AvCommands = TransportCommands.Create(document);
|
AvCommands = TransportCommands.Create(document);
|
||||||
return AvCommands;
|
return AvCommands;
|
||||||
|
@ -942,6 +1034,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
||||||
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
|
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
|
||||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
RendererCommands = TransportCommands.Create(document);
|
RendererCommands = TransportCommands.Create(document);
|
||||||
return RendererCommands;
|
return RendererCommands;
|
||||||
|
@ -973,6 +1069,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
|
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
|
||||||
|
|
||||||
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
|
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var friendlyNames = new List<string>();
|
var friendlyNames = new List<string>();
|
||||||
|
|
||||||
|
@ -990,7 +1090,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var deviceProperties = new DeviceInfo()
|
var deviceProperties = new DeviceInfo()
|
||||||
{
|
{
|
||||||
Name = string.Join(" ", friendlyNames),
|
Name = string.Join(' ', friendlyNames),
|
||||||
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
|
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -102,6 +104,22 @@ namespace Emby.Dlna.PlayTo
|
||||||
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||||
|
*/
|
||||||
|
private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
|
||||||
|
{
|
||||||
|
// The current playing item is indeed in the play list and we are not yet at the end of the playlist.
|
||||||
|
var nextItemIndex = currentPlayListItemIndex + 1;
|
||||||
|
var nextItem = _playlist[nextItemIndex];
|
||||||
|
|
||||||
|
// Send the SetNextAvTransport message.
|
||||||
|
await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDeviceUnavailable()
|
private void OnDeviceUnavailable()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -132,7 +150,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -156,6 +174,15 @@ namespace Emby.Dlna.PlayTo
|
||||||
var newItemProgress = GetProgressInfo(streamInfo);
|
var newItemProgress = GetProgressInfo(streamInfo);
|
||||||
|
|
||||||
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||||
|
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
|
||||||
|
if (currentItemIndex >= 0)
|
||||||
|
{
|
||||||
|
_currentPlaylistIndex = currentItemIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -425,6 +452,11 @@ namespace Emby.Dlna.PlayTo
|
||||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||||
|
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,8 +531,8 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
||||||
{
|
{
|
||||||
return new ContentFeatureBuilder(profile)
|
return ContentFeatureBuilder.BuildAudioHeader(
|
||||||
.BuildAudioHeader(
|
profile,
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioBitrate,
|
streamInfo.TargetAudioBitrate,
|
||||||
|
@ -514,8 +546,8 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
if (streamInfo.MediaType == DlnaProfileType.Video)
|
if (streamInfo.MediaType == DlnaProfileType.Video)
|
||||||
{
|
{
|
||||||
var list = new ContentFeatureBuilder(profile)
|
var list = ContentFeatureBuilder.BuildVideoHeader(
|
||||||
.BuildVideoHeader(
|
profile,
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
|
@ -623,6 +655,9 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
await SendNextTrackMessage(index, cancellationToken);
|
||||||
|
|
||||||
var streamInfo = currentitem.StreamInfo;
|
var streamInfo = currentitem.StreamInfo;
|
||||||
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
||||||
{
|
{
|
||||||
|
@ -736,6 +771,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||||
|
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||||
|
|
||||||
if (EnableClientSideSeek(newItem.StreamInfo))
|
if (EnableClientSideSeek(newItem.StreamInfo))
|
||||||
{
|
{
|
||||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
@ -761,6 +800,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||||
|
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||||
|
|
||||||
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
||||||
{
|
{
|
||||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
@ -777,7 +820,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
var currentWait = 0;
|
var currentWait = 0;
|
||||||
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
|
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
|
||||||
{
|
{
|
||||||
await Task.Delay(Interval).ConfigureAwait(false);
|
await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
|
||||||
currentWait += Interval;
|
currentWait += Interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -826,7 +869,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name == SessionMessageType.PlayState)
|
if (name == SessionMessageType.Playstate)
|
||||||
{
|
{
|
||||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -896,16 +939,16 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var parts = url.Split('/');
|
var parts = url.Split('/');
|
||||||
|
|
||||||
for (var i = 0; i < parts.Length; i++)
|
for (var i = 0; i < parts.Length - 1; i++)
|
||||||
{
|
{
|
||||||
var part = parts[i];
|
var part = parts[i];
|
||||||
|
|
||||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (parts.Length > i + 1)
|
if (Guid.TryParse(parts[i + 1], out var result))
|
||||||
{
|
{
|
||||||
return Guid.Parse(parts[i + 1]);
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -943,11 +986,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
||||||
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
||||||
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
||||||
|
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
||||||
// Be careful, IsDirectStream==true by default (Static != false or not in query).
|
|
||||||
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
|
|
||||||
request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
||||||
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
||||||
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -178,12 +180,17 @@ namespace Emby.Dlna.PlayTo
|
||||||
if (controller == null)
|
if (controller == null)
|
||||||
{
|
{
|
||||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (device == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Ignoring device as xml response is invalid.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string deviceName = device.Properties.Name;
|
string deviceName = device.Properties.Name;
|
||||||
|
|
||||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||||
|
|
||||||
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
|
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
|
||||||
|
|
||||||
controller = new PlayToController(
|
controller = new PlayToController(
|
||||||
sessionInfo,
|
sessionInfo,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -45,10 +46,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
return await XDocument.LoadAsync(
|
||||||
return XDocument.Parse(
|
stream,
|
||||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
LoadOptions.PreserveWhitespace,
|
||||||
LoadOptions.PreserveWhitespace);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||||
|
@ -94,10 +95,17 @@ namespace Emby.Dlna.PlayTo
|
||||||
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
try
|
||||||
return XDocument.Parse(
|
{
|
||||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
return await XDocument.LoadAsync(
|
||||||
LoadOptions.PreserveWhitespace);
|
stream,
|
||||||
|
LoadOptions.PreserveWhitespace,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
||||||
|
|
|
@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
public class TransportCommands
|
public class TransportCommands
|
||||||
{
|
{
|
||||||
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
|
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
|
||||||
private List<StateVariable> _stateVariables = new List<StateVariable>();
|
|
||||||
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
|
|
||||||
|
|
||||||
public List<StateVariable> StateVariables => _stateVariables;
|
public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
|
||||||
|
|
||||||
public List<ServiceAction> ServiceActions => _serviceActions;
|
public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
|
||||||
|
|
||||||
public static TransportCommands Create(XDocument document)
|
public static TransportCommands Create(XDocument document)
|
||||||
{
|
{
|
||||||
|
@ -48,7 +46,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var serviceAction = new ServiceAction
|
var serviceAction = new ServiceAction
|
||||||
{
|
{
|
||||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
var argumentList = serviceAction.ArgumentList;
|
var argumentList = serviceAction.ArgumentList;
|
||||||
|
@ -70,9 +68,9 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
return new Argument
|
return new Argument
|
||||||
{
|
{
|
||||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||||
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
|
Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
|
||||||
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
|
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,8 +89,8 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
return new StateVariable
|
return new StateVariable
|
||||||
{
|
{
|
||||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||||
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
|
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
|
||||||
AllowedValues = allowedValues
|
AllowedValues = allowedValues
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -168,7 +166,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
|
private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
|
||||||
{
|
{
|
||||||
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
|
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
#pragma warning disable SA1602
|
|
||||||
|
|
||||||
namespace Emby.Dlna.PlayTo
|
namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
||||||
|
@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
|
||||||
{
|
{
|
||||||
public DefaultProfile()
|
public DefaultProfile()
|
||||||
{
|
{
|
||||||
|
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||||
Name = "Generic Device";
|
Name = "Generic Device";
|
||||||
|
|
||||||
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
|
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
|
||||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
||||||
|
|
||||||
Identification = new DeviceIdentification
|
Identification = new DeviceIdentification
|
||||||
{
|
{
|
||||||
FriendlyName = @"KDL-\d{2}[EHLNPB]X\d[01]\d.*",
|
FriendlyName = @"KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*",
|
||||||
Manufacturer = "Sony",
|
Manufacturer = "Sony",
|
||||||
|
|
||||||
Headers = new[]
|
Headers = new[]
|
||||||
|
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
||||||
new HttpHeaderInfo
|
new HttpHeaderInfo
|
||||||
{
|
{
|
||||||
Name = "X-AV-Client-Info",
|
Name = "X-AV-Client-Info",
|
||||||
Value = @".*KDL-\d{2}[EHLNPB]X\d[01]\d.*",
|
Value = @".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*",
|
||||||
Match = HeaderMatchType.Regex
|
Match = HeaderMatchType.Regex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
||||||
|
|
||||||
Identification = new DeviceIdentification
|
Identification = new DeviceIdentification
|
||||||
{
|
{
|
||||||
FriendlyName = @"KDL-\d{2}([A-Z]X\d2\d|CX400).*",
|
FriendlyName = @"KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*",
|
||||||
Manufacturer = "Sony",
|
Manufacturer = "Sony",
|
||||||
|
|
||||||
Headers = new[]
|
Headers = new[]
|
||||||
|
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
||||||
new HttpHeaderInfo
|
new HttpHeaderInfo
|
||||||
{
|
{
|
||||||
Name = "X-AV-Client-Info",
|
Name = "X-AV-Client-Info",
|
||||||
Value = @".*KDL-\d{2}([A-Z]X\d2\d|CX400).*",
|
Value = @".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*",
|
||||||
Match = HeaderMatchType.Regex
|
Match = HeaderMatchType.Regex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
||||||
|
|
||||||
Identification = new DeviceIdentification
|
Identification = new DeviceIdentification
|
||||||
{
|
{
|
||||||
FriendlyName = @"KDL-\d{2}[A-Z]X\d5(\d|G).*",
|
FriendlyName = @"KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*",
|
||||||
Manufacturer = "Sony",
|
Manufacturer = "Sony",
|
||||||
|
|
||||||
Headers = new[]
|
Headers = new[]
|
||||||
|
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
||||||
new HttpHeaderInfo
|
new HttpHeaderInfo
|
||||||
{
|
{
|
||||||
Name = "X-AV-Client-Info",
|
Name = "X-AV-Client-Info",
|
||||||
Value = @".*KDL-\d{2}[A-Z]X\d5(\d|G).*",
|
Value = @".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*",
|
||||||
Match = HeaderMatchType.Regex
|
Match = HeaderMatchType.Regex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
||||||
|
|
||||||
Identification = new DeviceIdentification
|
Identification = new DeviceIdentification
|
||||||
{
|
{
|
||||||
FriendlyName = @"KDL-\d{2}[WR][5689]\d{2}A.*",
|
FriendlyName = @"KDL-[0-9]{2}[WR][5689][0-9]{2}A.*",
|
||||||
Manufacturer = "Sony",
|
Manufacturer = "Sony",
|
||||||
|
|
||||||
Headers = new[]
|
Headers = new[]
|
||||||
|
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
||||||
new HttpHeaderInfo
|
new HttpHeaderInfo
|
||||||
{
|
{
|
||||||
Name = "X-AV-Client-Info",
|
Name = "X-AV-Client-Info",
|
||||||
Value = @".*KDL-\d{2}[WR][5689]\d{2}A.*",
|
Value = @".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*",
|
||||||
Match = HeaderMatchType.Regex
|
Match = HeaderMatchType.Regex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
||||||
|
|
||||||
Identification = new DeviceIdentification
|
Identification = new DeviceIdentification
|
||||||
{
|
{
|
||||||
FriendlyName = @"(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*",
|
FriendlyName = @"(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*",
|
||||||
Manufacturer = "Sony",
|
Manufacturer = "Sony",
|
||||||
|
|
||||||
Headers = new[]
|
Headers = new[]
|
||||||
|
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
||||||
new HttpHeaderInfo
|
new HttpHeaderInfo
|
||||||
{
|
{
|
||||||
Name = "X-AV-Client-Info",
|
Name = "X-AV-Client-Info",
|
||||||
Value = @".*(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*",
|
Value = @".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*",
|
||||||
Match = HeaderMatchType.Regex
|
Match = HeaderMatchType.Regex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
|
||||||
Container = "ts,mpegts",
|
Container = "ts,mpegts",
|
||||||
Type = DlnaProfileType.Video,
|
Type = DlnaProfileType.Video,
|
||||||
VideoCodec = "mpeg1video,mpeg2video,h264",
|
VideoCodec = "mpeg1video,mpeg2video,h264",
|
||||||
AudioCodec = "ac3,mp2,mp3,aac"
|
AudioCodec = "aac,ac3,mp2"
|
||||||
},
|
},
|
||||||
new DirectPlayProfile
|
new DirectPlayProfile
|
||||||
{
|
{
|
||||||
|
@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles
|
||||||
{
|
{
|
||||||
Container = "ts",
|
Container = "ts",
|
||||||
VideoCodec = "h264",
|
VideoCodec = "h264",
|
||||||
AudioCodec = "ac3,aac,mp3",
|
AudioCodec = "aac,ac3,mp2",
|
||||||
Type = DlnaProfileType.Video
|
Type = DlnaProfileType.Video
|
||||||
},
|
},
|
||||||
new TranscodingProfile
|
new TranscodingProfile
|
||||||
|
|
|
@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
|
||||||
Container = "ts,mpegts",
|
Container = "ts,mpegts",
|
||||||
Type = DlnaProfileType.Video,
|
Type = DlnaProfileType.Video,
|
||||||
VideoCodec = "mpeg1video,mpeg2video,h264",
|
VideoCodec = "mpeg1video,mpeg2video,h264",
|
||||||
AudioCodec = "ac3,mp2,mp3,aac"
|
AudioCodec = "aac,ac3,mp2"
|
||||||
},
|
},
|
||||||
new DirectPlayProfile
|
new DirectPlayProfile
|
||||||
{
|
{
|
||||||
|
@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles
|
||||||
{
|
{
|
||||||
Container = "ts",
|
Container = "ts",
|
||||||
VideoCodec = "h264",
|
VideoCodec = "h264",
|
||||||
AudioCodec = "mp3",
|
AudioCodec = "aac,ac3,mp2",
|
||||||
Type = DlnaProfileType.Video
|
Type = DlnaProfileType.Video
|
||||||
},
|
},
|
||||||
new TranscodingProfile
|
new TranscodingProfile
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<Name>Sony Bravia (2010)</Name>
|
<Name>Sony Bravia (2010)</Name>
|
||||||
<Identification>
|
<Identification>
|
||||||
<FriendlyName>KDL-\d{2}[EHLNPB]X\d[01]\d.*</FriendlyName>
|
<FriendlyName>KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*</FriendlyName>
|
||||||
<Manufacturer>Sony</Manufacturer>
|
<Manufacturer>Sony</Manufacturer>
|
||||||
<Headers>
|
<Headers>
|
||||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[EHLNPB]X\d[01]\d.*" match="Regex" />
|
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*" match="Regex" />
|
||||||
</Headers>
|
</Headers>
|
||||||
</Identification>
|
</Identification>
|
||||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<Name>Sony Bravia (2011)</Name>
|
<Name>Sony Bravia (2011)</Name>
|
||||||
<Identification>
|
<Identification>
|
||||||
<FriendlyName>KDL-\d{2}([A-Z]X\d2\d|CX400).*</FriendlyName>
|
<FriendlyName>KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*</FriendlyName>
|
||||||
<Manufacturer>Sony</Manufacturer>
|
<Manufacturer>Sony</Manufacturer>
|
||||||
<Headers>
|
<Headers>
|
||||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}([A-Z]X\d2\d|CX400).*" match="Regex" />
|
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*" match="Regex" />
|
||||||
</Headers>
|
</Headers>
|
||||||
</Identification>
|
</Identification>
|
||||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<Name>Sony Bravia (2012)</Name>
|
<Name>Sony Bravia (2012)</Name>
|
||||||
<Identification>
|
<Identification>
|
||||||
<FriendlyName>KDL-\d{2}[A-Z]X\d5(\d|G).*</FriendlyName>
|
<FriendlyName>KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*</FriendlyName>
|
||||||
<Manufacturer>Sony</Manufacturer>
|
<Manufacturer>Sony</Manufacturer>
|
||||||
<Headers>
|
<Headers>
|
||||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[A-Z]X\d5(\d|G).*" match="Regex" />
|
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*" match="Regex" />
|
||||||
</Headers>
|
</Headers>
|
||||||
</Identification>
|
</Identification>
|
||||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<Name>Sony Bravia (2013)</Name>
|
<Name>Sony Bravia (2013)</Name>
|
||||||
<Identification>
|
<Identification>
|
||||||
<FriendlyName>KDL-\d{2}[WR][5689]\d{2}A.*</FriendlyName>
|
<FriendlyName>KDL-[0-9]{2}[WR][5689][0-9]{2}A.*</FriendlyName>
|
||||||
<Manufacturer>Sony</Manufacturer>
|
<Manufacturer>Sony</Manufacturer>
|
||||||
<Headers>
|
<Headers>
|
||||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[WR][5689]\d{2}A.*" match="Regex" />
|
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*" match="Regex" />
|
||||||
</Headers>
|
</Headers>
|
||||||
</Identification>
|
</Identification>
|
||||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<Name>Sony Bravia (2014)</Name>
|
<Name>Sony Bravia (2014)</Name>
|
||||||
<Identification>
|
<Identification>
|
||||||
<FriendlyName>(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*</FriendlyName>
|
<FriendlyName>(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*</FriendlyName>
|
||||||
<Manufacturer>Sony</Manufacturer>
|
<Manufacturer>Sony</Manufacturer>
|
||||||
<Headers>
|
<Headers>
|
||||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*" match="Regex" />
|
<HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*" match="Regex" />
|
||||||
</Headers>
|
</Headers>
|
||||||
</Identification>
|
</Identification>
|
||||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
<XmlRootAttributes />
|
<XmlRootAttributes />
|
||||||
<DirectPlayProfiles>
|
<DirectPlayProfiles>
|
||||||
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
|
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
|
||||||
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
|
<DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
|
||||||
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||||
<DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
<DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
||||||
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
</DirectPlayProfiles>
|
</DirectPlayProfiles>
|
||||||
<TranscodingProfiles>
|
<TranscodingProfiles>
|
||||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||||
</TranscodingProfiles>
|
</TranscodingProfiles>
|
||||||
<ContainerProfiles>
|
<ContainerProfiles>
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
<XmlRootAttributes />
|
<XmlRootAttributes />
|
||||||
<DirectPlayProfiles>
|
<DirectPlayProfiles>
|
||||||
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
|
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
|
||||||
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
|
<DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
|
||||||
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||||
<DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
<DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
||||||
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
</DirectPlayProfiles>
|
</DirectPlayProfiles>
|
||||||
<TranscodingProfiles>
|
<TranscodingProfiles>
|
||||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||||
</TranscodingProfiles>
|
</TranscodingProfiles>
|
||||||
<ContainerProfiles>
|
<ContainerProfiles>
|
||||||
|
|
|
@ -250,7 +250,8 @@ namespace Emby.Dlna.Server
|
||||||
|
|
||||||
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
|
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
|
||||||
|
|
||||||
return SecurityElement.Escape(url);
|
// TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
|
||||||
|
return SecurityElement.Escape(url) ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<DeviceIcon> GetIcons()
|
private IEnumerable<DeviceIcon> GetIcons()
|
||||||
|
|
|
@ -47,9 +47,9 @@ namespace Emby.Dlna.Service
|
||||||
|
|
||||||
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
|
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
|
||||||
{
|
{
|
||||||
ControlRequestInfo requestInfo = null;
|
ControlRequestInfo? requestInfo = null;
|
||||||
|
|
||||||
using (var streamReader = new StreamReader(request.InputXml))
|
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
|
||||||
{
|
{
|
||||||
var readerSettings = new XmlReaderSettings()
|
var readerSettings = new XmlReaderSettings()
|
||||||
{
|
{
|
||||||
|
@ -151,7 +151,7 @@ namespace Emby.Dlna.Service
|
||||||
|
|
||||||
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
||||||
{
|
{
|
||||||
string namespaceURI = null, localName = null;
|
string? namespaceURI = null, localName = null;
|
||||||
|
|
||||||
await reader.MoveToContentAsync().ConfigureAwait(false);
|
await reader.MoveToContentAsync().ConfigureAwait(false);
|
||||||
await reader.ReadAsync().ConfigureAwait(false);
|
await reader.ReadAsync().ConfigureAwait(false);
|
||||||
|
@ -210,7 +210,7 @@ namespace Emby.Dlna.Service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
|
protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter);
|
||||||
|
|
||||||
private void LogRequest(ControlRequest request)
|
private void LogRequest(ControlRequest request)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -69,7 +71,7 @@ namespace Emby.Dlna.Ssdp
|
||||||
{
|
{
|
||||||
lock (_syncLock)
|
lock (_syncLock)
|
||||||
{
|
{
|
||||||
if (_listenerCount > 0 && _deviceLocator == null)
|
if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
|
||||||
{
|
{
|
||||||
_deviceLocator = new SsdpDeviceLocator(_commsServer);
|
_deviceLocator = new SsdpDeviceLocator(_commsServer);
|
||||||
|
|
||||||
|
@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp
|
||||||
{
|
{
|
||||||
Location = e.DiscoveredDevice.DescriptionLocation,
|
Location = e.DiscoveredDevice.DescriptionLocation,
|
||||||
Headers = headers,
|
Headers = headers,
|
||||||
LocalIpAddress = e.LocalIpAddress
|
RemoteIpAddress = e.RemoteIpAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
DeviceDiscoveredInternal?.Invoke(this, args);
|
DeviceDiscoveredInternal?.Invoke(this, args);
|
||||||
|
|
|
@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp
|
||||||
{
|
{
|
||||||
public static class SsdpExtensions
|
public static class SsdpExtensions
|
||||||
{
|
{
|
||||||
public static string GetValue(this XElement container, XName name)
|
public static string? GetValue(this XElement container, XName name)
|
||||||
{
|
{
|
||||||
var node = container.Element(name);
|
var node = container.Element(name);
|
||||||
|
|
||||||
return node?.Value;
|
return node?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetAttributeValue(this XElement container, XName name)
|
public static string? GetAttributeValue(this XElement container, XName name)
|
||||||
{
|
{
|
||||||
var node = container.Attribute(name);
|
var node = container.Attribute(name);
|
||||||
|
|
||||||
return node?.Value;
|
return node?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetDescendantValue(this XElement container, XName name)
|
public static string? GetDescendantValue(this XElement container, XName name)
|
||||||
=> container.Descendants(name).FirstOrDefault()?.Value;
|
=> container.Descendants(name).FirstOrDefault()?.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
|
|
||||||
<!-- Code analysers-->
|
<!-- Code analysers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
|
@ -171,21 +172,31 @@ namespace Emby.Drawing
|
||||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
|
|
||||||
int quality = options.Quality;
|
int quality = options.Quality;
|
||||||
|
|
||||||
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
|
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
|
||||||
string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
|
string cacheFilePath = GetCacheFilePath(
|
||||||
|
originalImagePath,
|
||||||
|
options.Width,
|
||||||
|
options.Height,
|
||||||
|
options.MaxWidth,
|
||||||
|
options.MaxHeight,
|
||||||
|
options.FillWidth,
|
||||||
|
options.FillHeight,
|
||||||
|
quality,
|
||||||
|
dateModified,
|
||||||
|
outputFormat,
|
||||||
|
options.AddPlayedIndicator,
|
||||||
|
options.PercentPlayed,
|
||||||
|
options.UnplayedCount,
|
||||||
|
options.Blur,
|
||||||
|
options.BackgroundColor,
|
||||||
|
options.ForegroundLayer);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(cacheFilePath))
|
if (!File.Exists(cacheFilePath))
|
||||||
{
|
{
|
||||||
if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
|
|
||||||
{
|
|
||||||
options.CropWhiteSpace = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
|
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
|
||||||
|
|
||||||
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
@ -246,48 +257,111 @@ namespace Emby.Drawing
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the cache file path based on a set of parameters.
|
/// Gets the cache file path based on a set of parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
|
private string GetCacheFilePath(
|
||||||
|
string originalPath,
|
||||||
|
int? width,
|
||||||
|
int? height,
|
||||||
|
int? maxWidth,
|
||||||
|
int? maxHeight,
|
||||||
|
int? fillWidth,
|
||||||
|
int? fillHeight,
|
||||||
|
int quality,
|
||||||
|
DateTime dateModified,
|
||||||
|
ImageFormat format,
|
||||||
|
bool addPlayedIndicator,
|
||||||
|
double percentPlayed,
|
||||||
|
int? unwatchedCount,
|
||||||
|
int? blur,
|
||||||
|
string backgroundColor,
|
||||||
|
string foregroundLayer)
|
||||||
{
|
{
|
||||||
var filename = originalPath
|
var filename = new StringBuilder(256);
|
||||||
+ "width=" + outputSize.Width
|
filename.Append(originalPath);
|
||||||
+ "height=" + outputSize.Height
|
|
||||||
+ "quality=" + quality
|
filename.Append(",quality=");
|
||||||
+ "datemodified=" + dateModified.Ticks
|
filename.Append(quality);
|
||||||
+ "f=" + format;
|
|
||||||
|
filename.Append(",datemodified=");
|
||||||
|
filename.Append(dateModified.Ticks);
|
||||||
|
|
||||||
|
filename.Append(",f=");
|
||||||
|
filename.Append(format);
|
||||||
|
|
||||||
|
if (width.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",width=");
|
||||||
|
filename.Append(width.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",height=");
|
||||||
|
filename.Append(height.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxWidth.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",maxwidth=");
|
||||||
|
filename.Append(maxWidth.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxHeight.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",maxheight=");
|
||||||
|
filename.Append(maxHeight.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fillWidth.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",fillwidth=");
|
||||||
|
filename.Append(fillWidth.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fillHeight.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",fillheight=");
|
||||||
|
filename.Append(fillHeight.Value);
|
||||||
|
}
|
||||||
|
|
||||||
if (addPlayedIndicator)
|
if (addPlayedIndicator)
|
||||||
{
|
{
|
||||||
filename += "pl=true";
|
filename.Append(",pl=true");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (percentPlayed > 0)
|
if (percentPlayed > 0)
|
||||||
{
|
{
|
||||||
filename += "p=" + percentPlayed;
|
filename.Append(",p=");
|
||||||
|
filename.Append(percentPlayed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unwatchedCount.HasValue)
|
if (unwatchedCount.HasValue)
|
||||||
{
|
{
|
||||||
filename += "p=" + unwatchedCount.Value;
|
filename.Append(",p=");
|
||||||
|
filename.Append(unwatchedCount.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blur.HasValue)
|
if (blur.HasValue)
|
||||||
{
|
{
|
||||||
filename += "blur=" + blur.Value;
|
filename.Append(",blur=");
|
||||||
|
filename.Append(blur.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(backgroundColor))
|
if (!string.IsNullOrEmpty(backgroundColor))
|
||||||
{
|
{
|
||||||
filename += "b=" + backgroundColor;
|
filename.Append(",b=");
|
||||||
|
filename.Append(backgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(foregroundLayer))
|
if (!string.IsNullOrEmpty(foregroundLayer))
|
||||||
{
|
{
|
||||||
filename += "fl=" + foregroundLayer;
|
filename.Append(",fl=");
|
||||||
|
filename.Append(foregroundLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
filename += "v=" + Version;
|
filename.Append(",v=");
|
||||||
|
filename.Append(Version);
|
||||||
|
|
||||||
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant());
|
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -352,8 +426,13 @@ namespace Emby.Drawing
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string GetImageCacheTag(User user)
|
public string? GetImageCacheTag(User user)
|
||||||
{
|
{
|
||||||
|
if (user.ProfileImage == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
|
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
|
||||||
.ToString("N", CultureInfo.InvariantCulture);
|
.ToString("N", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ namespace Emby.Drawing
|
||||||
=> throw new NotImplementedException();
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
|
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Audio
|
namespace Emby.Naming.Audio
|
||||||
{
|
{
|
||||||
|
@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
|
||||||
/// <returns>True if file at path is audio file.</returns>
|
/// <returns>True if file at path is audio file.</returns>
|
||||||
public static bool IsAudioFile(string path, NamingOptions options)
|
public static bool IsAudioFile(string path, NamingOptions options)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,13 @@ namespace Emby.Naming.AudioBook
|
||||||
/// <param name="files">List of files composing the actual audiobook.</param>
|
/// <param name="files">List of files composing the actual audiobook.</param>
|
||||||
/// <param name="extras">List of extra files.</param>
|
/// <param name="extras">List of extra files.</param>
|
||||||
/// <param name="alternateVersions">Alternative version of files.</param>
|
/// <param name="alternateVersions">Alternative version of files.</param>
|
||||||
public AudioBookInfo(string name, int? year, List<AudioBookFileInfo>? files, List<AudioBookFileInfo>? extras, List<AudioBookFileInfo>? alternateVersions)
|
public AudioBookInfo(string name, int? year, List<AudioBookFileInfo> files, List<AudioBookFileInfo> extras, List<AudioBookFileInfo> alternateVersions)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Year = year;
|
Year = year;
|
||||||
Files = files ?? new List<AudioBookFileInfo>();
|
Files = files;
|
||||||
Extras = extras ?? new List<AudioBookFileInfo>();
|
Extras = extras;
|
||||||
AlternateVersions = alternateVersions ?? new List<AudioBookFileInfo>();
|
AlternateVersions = alternateVersions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook
|
||||||
|
|
||||||
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
|
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
|
||||||
var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
|
var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
|
||||||
var nameWithReplacedDots = nameParserResult.Name.Replace(" ", ".");
|
var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.');
|
||||||
|
|
||||||
foreach (var group in groupedBy)
|
foreach (var group in groupedBy)
|
||||||
{
|
{
|
||||||
|
|
|
@ -282,7 +282,13 @@ namespace Emby.Naming.Common
|
||||||
SupportsAbsoluteEpisodeNumbers = true
|
SupportsAbsoluteEpisodeNumbers = true
|
||||||
},
|
},
|
||||||
|
|
||||||
// Case Closed (1996-2007)/Case Closed - 317.mkv
|
// Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
|
||||||
|
// [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
|
||||||
|
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
|
||||||
|
{
|
||||||
|
IsNamed = true
|
||||||
|
},
|
||||||
|
|
||||||
// /server/anything_102.mp4
|
// /server/anything_102.mp4
|
||||||
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
|
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
|
||||||
// /server/anything_1996.11.14.mp4
|
// /server/anything_1996.11.14.mp4
|
||||||
|
@ -299,11 +305,6 @@ namespace Emby.Naming.Common
|
||||||
|
|
||||||
// *** End Kodi Standard Naming
|
// *** End Kodi Standard Naming
|
||||||
|
|
||||||
// [bar] Foo - 1 [baz]
|
|
||||||
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
|
|
||||||
{
|
|
||||||
IsNamed = true
|
|
||||||
},
|
|
||||||
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
|
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
|
@ -587,7 +588,7 @@ namespace Emby.Naming.Common
|
||||||
AudioBookNamesExpressions = new[]
|
AudioBookNamesExpressions = new[]
|
||||||
{
|
{
|
||||||
// Detect year usually in brackets after name Batman (2020)
|
// Detect year usually in brackets after name Batman (2020)
|
||||||
@"^(?<name>.+?)\s*\(\s*(?<year>\d{4})\s*\)\s*$",
|
@"^(?<name>.+?)\s*\(\s*(?<year>[0-9]{4})\s*\)\s*$",
|
||||||
@"^\s*(?<name>[^ ].*?)\s*$"
|
@"^\s*(?<name>[^ ].*?)\s*$"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -23,17 +23,18 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\SharedVersion.cs" />
|
<Compile Include="../SharedVersion.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
|
||||||
|
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.7.0</VersionPrefix>
|
<VersionPrefix>10.8.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
@ -44,7 +45,6 @@
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
|
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
|
|
|
@ -68,6 +68,11 @@ namespace Emby.Naming.TV
|
||||||
var parsingResult = new EpisodePathParser(_options)
|
var parsingResult = new EpisodePathParser(_options)
|
||||||
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
||||||
|
|
||||||
|
if (!parsingResult.Success && !isStub)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return new EpisodeInfo(path)
|
return new EpisodeInfo(path)
|
||||||
{
|
{
|
||||||
Container = container,
|
Container = container,
|
||||||
|
|
|
@ -60,7 +60,7 @@ namespace Emby.Naming.TV
|
||||||
bool supportSpecialAliases,
|
bool supportSpecialAliases,
|
||||||
bool supportNumericSeasonFolders)
|
bool supportNumericSeasonFolders)
|
||||||
{
|
{
|
||||||
var filename = Path.GetFileName(path) ?? string.Empty;
|
string filename = Path.GetFileName(path);
|
||||||
|
|
||||||
if (supportSpecialAliases)
|
if (supportSpecialAliases)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
|
@ -16,8 +17,14 @@ namespace Emby.Naming.Video
|
||||||
/// <param name="expressions">List of regex to parse name and year from.</param>
|
/// <param name="expressions">List of regex to parse name and year from.</param>
|
||||||
/// <param name="newName">Parsing result string.</param>
|
/// <param name="newName">Parsing result string.</param>
|
||||||
/// <returns>True if parsing was successful.</returns>
|
/// <returns>True if parsing was successful.</returns>
|
||||||
public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
|
public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var len = expressions.Count;
|
var len = expressions.Count;
|
||||||
for (int i = 0; i < len; i++)
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
|
@ -41,7 +48,7 @@ namespace Emby.Naming.Video
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
newName = string.Empty;
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,72 +29,75 @@ namespace Emby.Naming.Video
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||||
public ExtraResult GetExtraInfo(string path)
|
public ExtraResult GetExtraInfo(string path)
|
||||||
{
|
|
||||||
return _options.VideoExtraRules
|
|
||||||
.Select(i => GetExtraInfo(path, i))
|
|
||||||
.FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ExtraResult GetExtraInfo(string path, ExtraRule rule)
|
|
||||||
{
|
{
|
||||||
var result = new ExtraResult();
|
var result = new ExtraResult();
|
||||||
|
|
||||||
if (rule.MediaType == MediaType.Audio)
|
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
|
||||||
{
|
{
|
||||||
if (!AudioFileParser.IsAudioFile(path, _options))
|
var rule = _options.VideoExtraRules[i];
|
||||||
|
if (rule.MediaType == MediaType.Audio)
|
||||||
|
{
|
||||||
|
if (!AudioFileParser.IsAudioFile(path, _options))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.MediaType == MediaType.Video)
|
||||||
|
{
|
||||||
|
if (!new VideoResolver(_options).IsVideoFile(path))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathSpan = path.AsSpan();
|
||||||
|
if (rule.RuleType == ExtraRuleType.Filename)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
||||||
|
|
||||||
|
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.RuleType == ExtraRuleType.Suffix)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
||||||
|
|
||||||
|
if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.RuleType == ExtraRuleType.Regex)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileName(path);
|
||||||
|
|
||||||
|
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
if (regex.IsMatch(filename))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
||||||
|
{
|
||||||
|
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
|
||||||
|
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.ExtraType != null)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (rule.MediaType == MediaType.Video)
|
|
||||||
{
|
|
||||||
if (!new VideoResolver(_options).IsVideoFile(path))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule.RuleType == ExtraRuleType.Filename)
|
|
||||||
{
|
|
||||||
var filename = Path.GetFileNameWithoutExtension(path);
|
|
||||||
|
|
||||||
if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.Suffix)
|
|
||||||
{
|
|
||||||
var filename = Path.GetFileNameWithoutExtension(path);
|
|
||||||
|
|
||||||
if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.Regex)
|
|
||||||
{
|
|
||||||
var filename = Path.GetFileName(path);
|
|
||||||
|
|
||||||
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
if (regex.IsMatch(filename))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
|
||||||
{
|
|
||||||
var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
|
|
||||||
if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,8 +185,8 @@ namespace Emby.Naming.Video
|
||||||
if (!string.IsNullOrEmpty(folderName)
|
if (!string.IsNullOrEmpty(folderName)
|
||||||
&& folderName.Length > 1
|
&& folderName.Length > 1
|
||||||
&& videos.All(i => i.Files.Count == 1
|
&& videos.All(i => i.Files.Count == 1
|
||||||
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path))
|
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path))
|
||||||
&& HaveSameYear(videos))
|
&& HaveSameYear(videos))
|
||||||
{
|
{
|
||||||
var ordered = videos.OrderBy(i => i.Name).ToList();
|
var ordered = videos.OrderBy(i => i.Name).ToList();
|
||||||
|
|
||||||
|
@ -216,26 +216,26 @@ namespace Emby.Naming.Video
|
||||||
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
|
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsEligibleForMultiVersion(string folderName, string? testFilename)
|
private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
|
||||||
{
|
{
|
||||||
testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty;
|
string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
|
||||||
|
|
||||||
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
|
// Remove the folder name before cleaning as we don't care about cleaning that part
|
||||||
{
|
|
||||||
testFilename = cleanName.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folderName.Length <= testFilename.Length)
|
if (folderName.Length <= testFilename.Length)
|
||||||
{
|
{
|
||||||
testFilename = testFilename.Substring(folderName.Length).Trim();
|
testFilename = testFilename.Substring(folderName.Length).Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
|
||||||
|
{
|
||||||
|
testFilename = cleanName.Trim().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CleanStringParser should have removed common keywords etc.
|
||||||
return string.IsNullOrEmpty(testFilename)
|
return string.IsNullOrEmpty(testFilename)
|
||||||
|| testFilename[0].Equals('-')
|
|| testFilename[0] == '-'
|
||||||
|| testFilename[0].Equals('_')
|
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
|
||||||
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
|
@ -58,15 +59,15 @@ namespace Emby.Naming.Video
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isStub = false;
|
bool isStub = false;
|
||||||
string? container = null;
|
ReadOnlySpan<char> container = ReadOnlySpan<char>.Empty;
|
||||||
string? stubType = null;
|
string? stubType = null;
|
||||||
|
|
||||||
if (!isDirectory)
|
if (!isDirectory)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
|
|
||||||
// Check supported extensions
|
// Check supported extensions
|
||||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// It's not supported. Check stub extensions
|
// It's not supported. Check stub extensions
|
||||||
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
||||||
|
@ -85,9 +86,7 @@ namespace Emby.Naming.Video
|
||||||
|
|
||||||
var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
|
var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
|
||||||
|
|
||||||
var name = isDirectory
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
? Path.GetFileName(path)
|
|
||||||
: Path.GetFileNameWithoutExtension(path);
|
|
||||||
|
|
||||||
int? year = null;
|
int? year = null;
|
||||||
|
|
||||||
|
@ -106,7 +105,7 @@ namespace Emby.Naming.Video
|
||||||
|
|
||||||
return new VideoFileInfo(
|
return new VideoFileInfo(
|
||||||
path: path,
|
path: path,
|
||||||
container: container,
|
container: container.IsEmpty ? null : container.ToString(),
|
||||||
isStub: isStub,
|
isStub: isStub,
|
||||||
name: name,
|
name: name,
|
||||||
year: year,
|
year: year,
|
||||||
|
@ -125,8 +124,8 @@ namespace Emby.Naming.Video
|
||||||
/// <returns>True if is video file.</returns>
|
/// <returns>True if is video file.</returns>
|
||||||
public bool IsVideoFile(string path)
|
public bool IsVideoFile(string path)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path) ?? string.Empty;
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
return _options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -136,8 +135,8 @@ namespace Emby.Naming.Video
|
||||||
/// <returns>True if is video file stub.</returns>
|
/// <returns>True if is video file stub.</returns>
|
||||||
public bool IsStubFile(string path)
|
public bool IsStubFile(string path)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path) ?? string.Empty;
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
return _options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -146,7 +145,7 @@ namespace Emby.Naming.Video
|
||||||
/// <param name="name">Raw name.</param>
|
/// <param name="name">Raw name.</param>
|
||||||
/// <param name="newName">Clean name.</param>
|
/// <param name="newName">Clean name.</param>
|
||||||
/// <returns>True if cleaning of name was successful.</returns>
|
/// <returns>True if cleaning of name was successful.</returns>
|
||||||
public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
|
public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
|
||||||
{
|
{
|
||||||
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
|
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,10 +75,6 @@ namespace Emby.Notifications
|
||||||
Type = NotificationType.VideoPlaybackStopped.ToString()
|
Type = NotificationType.VideoPlaybackStopped.ToString()
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
|
||||||
Type = NotificationType.CameraImageUploaded.ToString()
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
{
|
||||||
Type = NotificationType.UserLockedOut.ToString()
|
Type = NotificationType.UserLockedOut.ToString()
|
||||||
},
|
},
|
||||||
|
@ -114,10 +110,6 @@ namespace Emby.Notifications
|
||||||
{
|
{
|
||||||
note.Category = _localization.GetLocalizedString("Plugin");
|
note.Category = _localization.GetLocalizedString("Plugin");
|
||||||
}
|
}
|
||||||
else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
note.Category = _localization.GetLocalizedString("Sync");
|
|
||||||
}
|
|
||||||
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
|
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
|
||||||
{
|
{
|
||||||
note.Category = _localization.GetLocalizedString("User");
|
note.Category = _localization.GetLocalizedString("User");
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -25,14 +27,9 @@
|
||||||
|
|
||||||
<!-- Code analyzers-->
|
<!-- Code analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -24,18 +24,15 @@
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
CachePath = cacheDirectoryPath;
|
CachePath = cacheDirectoryPath;
|
||||||
WebPath = webDirectoryPath;
|
WebPath = webDirectoryPath;
|
||||||
|
|
||||||
DataPath = Path.Combine(ProgramDataPath, "data");
|
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
/// Gets the folder path to the data directory.
|
/// Gets the folder path to the data directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The data directory.</value>
|
/// <value>The data directory.</value>
|
||||||
public string DataPath
|
public string DataPath => _dataPath;
|
||||||
{
|
|
||||||
get => _dataPath;
|
|
||||||
private set => _dataPath = Directory.CreateDirectory(value).FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string VirtualDataPath => "%AppDataPath%";
|
public string VirtualDataPath => "%AppDataPath%";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The _configuration sync lock.
|
||||||
|
/// </summary>
|
||||||
|
private readonly object _configurationSyncLock = new object();
|
||||||
|
|
||||||
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
||||||
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
||||||
|
|
||||||
|
@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool _configurationLoaded;
|
private bool _configurationLoaded;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _configuration sync lock.
|
|
||||||
/// </summary>
|
|
||||||
private readonly object _configurationSyncLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _configuration.
|
/// The _configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.AppBase
|
namespace Emby.Server.Implementations.AppBase
|
||||||
|
@ -36,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
|
// Note: CreateInstance returns null for Nullable<T>, e.g. CreateInstance(typeof(int?)) returns null.
|
||||||
|
configuration = Activator.CreateInstance(type)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
||||||
|
@ -53,7 +51,8 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
|
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
// Save it after load in case we got new items
|
// Save it after load in case we got new items
|
||||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
fs.Write(newBytes, 0, newBytesLen);
|
fs.Write(newBytes, 0, newBytesLen);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna;
|
using Emby.Dlna;
|
||||||
|
@ -42,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
|
||||||
using Emby.Server.Implementations.Session;
|
using Emby.Server.Implementations.Session;
|
||||||
using Emby.Server.Implementations.SyncPlay;
|
using Emby.Server.Implementations.SyncPlay;
|
||||||
using Emby.Server.Implementations.TV;
|
using Emby.Server.Implementations.TV;
|
||||||
|
using Emby.Server.Implementations.Udp;
|
||||||
using Emby.Server.Implementations.Updates;
|
using Emby.Server.Implementations.Updates;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Networking.Configuration;
|
using Jellyfin.Networking.Configuration;
|
||||||
|
@ -97,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
|
||||||
using MediaBrowser.XbmcMetadata.Providers;
|
using MediaBrowser.XbmcMetadata.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Prometheus.DotNetRuntime;
|
using Prometheus.DotNetRuntime;
|
||||||
|
@ -116,10 +119,12 @@ namespace Emby.Server.Implementations
|
||||||
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystemManager;
|
private readonly IFileSystem _fileSystemManager;
|
||||||
|
private readonly IConfiguration _startupConfig;
|
||||||
private readonly IXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
|
||||||
private readonly IStartupOptions _startupOptions;
|
private readonly IStartupOptions _startupOptions;
|
||||||
|
private readonly IPluginManager _pluginManager;
|
||||||
|
|
||||||
|
private List<Type> _creatingInstances;
|
||||||
private IMediaEncoder _mediaEncoder;
|
private IMediaEncoder _mediaEncoder;
|
||||||
private ISessionManager _sessionManager;
|
private ISessionManager _sessionManager;
|
||||||
private string[] _urlPrefixes;
|
private string[] _urlPrefixes;
|
||||||
|
@ -181,16 +186,6 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
protected IServiceCollection ServiceCollection { get; }
|
protected IServiceCollection ServiceCollection { get; }
|
||||||
|
|
||||||
private IPlugin[] _plugins;
|
|
||||||
|
|
||||||
private IReadOnlyList<LocalPlugin> _pluginsManifests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the plugins.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The plugins.</value>
|
|
||||||
public IReadOnlyList<IPlugin> Plugins => _plugins;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the logger factory.
|
/// Gets the logger factory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -217,7 +212,7 @@ namespace Emby.Server.Implementations
|
||||||
/// Gets or sets the configuration manager.
|
/// Gets or sets the configuration manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The configuration manager.</value>
|
/// <value>The configuration manager.</value>
|
||||||
protected IConfigurationManager ConfigurationManager { get; set; }
|
public ServerConfigurationManager ConfigurationManager { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the service provider.
|
/// Gets or sets the service provider.
|
||||||
|
@ -235,10 +230,9 @@ namespace Emby.Server.Implementations
|
||||||
public int HttpsPort { get; private set; }
|
public int HttpsPort { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the server configuration manager.
|
/// Gets the value of the PublishedServerUrl setting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The server configuration manager.</value>
|
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
||||||
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
||||||
|
@ -246,54 +240,39 @@ namespace Emby.Server.Implementations
|
||||||
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
||||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||||
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
||||||
|
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||||
public ApplicationHost(
|
public ApplicationHost(
|
||||||
IServerApplicationPaths applicationPaths,
|
IServerApplicationPaths applicationPaths,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IStartupOptions options,
|
IStartupOptions options,
|
||||||
|
IConfiguration startupConfig,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IServiceCollection serviceCollection)
|
IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
_xmlSerializer = new MyXmlSerializer();
|
|
||||||
_jsonSerializer = new JsonSerializer();
|
|
||||||
|
|
||||||
ServiceCollection = serviceCollection;
|
|
||||||
|
|
||||||
ApplicationPaths = applicationPaths;
|
ApplicationPaths = applicationPaths;
|
||||||
LoggerFactory = loggerFactory;
|
LoggerFactory = loggerFactory;
|
||||||
|
_startupOptions = options;
|
||||||
|
_startupConfig = startupConfig;
|
||||||
_fileSystemManager = fileSystem;
|
_fileSystemManager = fileSystem;
|
||||||
|
ServiceCollection = serviceCollection;
|
||||||
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
|
|
||||||
// Have to migrate settings here as migration subsystem not yet initialised.
|
|
||||||
MigrateNetworkConfiguration();
|
|
||||||
|
|
||||||
// Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
|
|
||||||
ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
|
|
||||||
NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
|
||||||
|
|
||||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||||
|
|
||||||
_startupOptions = options;
|
|
||||||
|
|
||||||
// Initialize runtime stat collection
|
|
||||||
if (ServerConfigurationManager.Configuration.EnableMetrics)
|
|
||||||
{
|
|
||||||
DotNetRuntimeStatsBuilder.Default().StartCollecting();
|
|
||||||
}
|
|
||||||
|
|
||||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
||||||
|
|
||||||
CertificateInfo = new CertificateInfo
|
|
||||||
{
|
|
||||||
Path = ServerConfigurationManager.Configuration.CertificatePath,
|
|
||||||
Password = ServerConfigurationManager.Configuration.CertificatePassword
|
|
||||||
};
|
|
||||||
Certificate = GetCertificate(CertificateInfo);
|
|
||||||
|
|
||||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||||
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
||||||
|
|
||||||
|
_xmlSerializer = new MyXmlSerializer();
|
||||||
|
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
|
||||||
|
_pluginManager = new PluginManager(
|
||||||
|
LoggerFactory.CreateLogger<PluginManager>(),
|
||||||
|
this,
|
||||||
|
ConfigurationManager.Configuration,
|
||||||
|
ApplicationPaths.PluginsPath,
|
||||||
|
ApplicationVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -306,9 +285,9 @@ namespace Emby.Server.Implementations
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
var networkSettings = new NetworkConfiguration();
|
var networkSettings = new NetworkConfiguration();
|
||||||
ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
|
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
|
||||||
_xmlSerializer.SerializeToFile(networkSettings, path);
|
_xmlSerializer.SerializeToFile(networkSettings, path);
|
||||||
Logger?.LogDebug("Successfully migrated network settings.");
|
Logger.LogDebug("Successfully migrated network settings.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,10 +337,7 @@ namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_deviceId == null)
|
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
|
||||||
{
|
|
||||||
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _deviceId.Value;
|
return _deviceId.Value;
|
||||||
}
|
}
|
||||||
|
@ -381,7 +357,7 @@ namespace Emby.Server.Implementations
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instance of type and resolves all constructor dependencies.
|
/// Creates an instance of type and resolves all constructor dependencies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// /// <typeparam name="T">The type.</typeparam>
|
/// <typeparam name="T">The type.</typeparam>
|
||||||
/// <returns>T.</returns>
|
/// <returns>T.</returns>
|
||||||
public T CreateInstance<T>()
|
public T CreateInstance<T>()
|
||||||
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
|
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
|
||||||
|
@ -393,16 +369,38 @@ namespace Emby.Server.Implementations
|
||||||
/// <returns>System.Object.</returns>
|
/// <returns>System.Object.</returns>
|
||||||
protected object CreateInstanceSafe(Type type)
|
protected object CreateInstanceSafe(Type type)
|
||||||
{
|
{
|
||||||
|
_creatingInstances ??= new List<Type>();
|
||||||
|
|
||||||
|
if (_creatingInstances.IndexOf(type) != -1)
|
||||||
|
{
|
||||||
|
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
|
||||||
|
foreach (var entry in _creatingInstances)
|
||||||
|
{
|
||||||
|
Logger.LogError("Called from: {TypeName}", entry.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pluginManager.FailPlugin(type.Assembly);
|
||||||
|
|
||||||
|
throw new ExternalException("DI Loop detected.");
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_creatingInstances.Add(type);
|
||||||
Logger.LogDebug("Creating instance of {Type}", type);
|
Logger.LogDebug("Creating instance of {Type}", type);
|
||||||
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
|
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error creating {Type}", type);
|
Logger.LogError(ex, "Error creating {Type}", type);
|
||||||
|
// If this is a plugin fail it.
|
||||||
|
_pluginManager.FailPlugin(type.Assembly);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_creatingInstances.Remove(type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -412,11 +410,7 @@ namespace Emby.Server.Implementations
|
||||||
/// <returns>``0.</returns>
|
/// <returns>``0.</returns>
|
||||||
public T Resolve<T>() => ServiceProvider.GetService<T>();
|
public T Resolve<T>() => ServiceProvider.GetService<T>();
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the export types.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type.</typeparam>
|
|
||||||
/// <returns>IEnumerable{Type}.</returns>
|
|
||||||
public IEnumerable<Type> GetExportTypes<T>()
|
public IEnumerable<Type> GetExportTypes<T>()
|
||||||
{
|
{
|
||||||
var currentType = typeof(T);
|
var currentType = typeof(T);
|
||||||
|
@ -445,17 +439,40 @@ namespace Emby.Server.Implementations
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
|
||||||
|
{
|
||||||
|
// Convert to list so this isn't executed for each iteration
|
||||||
|
var parts = GetExportTypes<T>()
|
||||||
|
.Select(i => defaultFunc(i))
|
||||||
|
.Where(i => i != null)
|
||||||
|
.Cast<T>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (manageLifetime)
|
||||||
|
{
|
||||||
|
lock (_disposableParts)
|
||||||
|
{
|
||||||
|
_disposableParts.AddRange(parts.OfType<IDisposable>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the startup tasks.
|
/// Runs the startup tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns><see cref="Task" />.</returns>
|
/// <returns><see cref="Task" />.</returns>
|
||||||
public async Task RunStartupTasksAsync()
|
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
Logger.LogInformation("Running startup tasks");
|
Logger.LogInformation("Running startup tasks");
|
||||||
|
|
||||||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||||
|
|
||||||
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
||||||
|
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
|
||||||
|
|
||||||
_mediaEncoder.SetFFmpegPath();
|
_mediaEncoder.SetFFmpegPath();
|
||||||
|
|
||||||
|
@ -463,14 +480,21 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
var entryPoints = GetExports<IServerEntryPoint>();
|
var entryPoints = GetExports<IServerEntryPoint>();
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var stopWatch = new Stopwatch();
|
var stopWatch = new Stopwatch();
|
||||||
stopWatch.Start();
|
stopWatch.Start();
|
||||||
|
|
||||||
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
|
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
|
||||||
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||||
|
|
||||||
Logger.LogInformation("Core startup complete");
|
Logger.LogInformation("Core startup complete");
|
||||||
CoreStartupHasCompleted = true;
|
CoreStartupHasCompleted = true;
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
stopWatch.Restart();
|
stopWatch.Restart();
|
||||||
|
|
||||||
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
||||||
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
|
@ -494,7 +518,21 @@ namespace Emby.Server.Implementations
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Init()
|
public void Init()
|
||||||
{
|
{
|
||||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
DiscoverTypes();
|
||||||
|
|
||||||
|
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||||
|
|
||||||
|
// Have to migrate settings here as migration subsystem not yet initialised.
|
||||||
|
MigrateNetworkConfiguration();
|
||||||
|
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
||||||
|
|
||||||
|
// Initialize runtime stat collection
|
||||||
|
if (ConfigurationManager.Configuration.EnableMetrics)
|
||||||
|
{
|
||||||
|
DotNetRuntimeStatsBuilder.Default().StartCollecting();
|
||||||
|
}
|
||||||
|
|
||||||
|
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||||
HttpPort = networkConfiguration.HttpServerPortNumber;
|
HttpPort = networkConfiguration.HttpServerPortNumber;
|
||||||
HttpsPort = networkConfiguration.HttpsPortNumber;
|
HttpsPort = networkConfiguration.HttpsPortNumber;
|
||||||
|
|
||||||
|
@ -505,11 +543,16 @@ namespace Emby.Server.Implementations
|
||||||
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscoverTypes();
|
CertificateInfo = new CertificateInfo
|
||||||
|
{
|
||||||
|
Path = networkConfiguration.CertificatePath,
|
||||||
|
Password = networkConfiguration.CertificatePassword
|
||||||
|
};
|
||||||
|
Certificate = GetCertificate(CertificateInfo);
|
||||||
|
|
||||||
RegisterServices();
|
RegisterServices();
|
||||||
|
|
||||||
RegisterPluginServices();
|
_pluginManager.RegisterServices(ServiceCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -521,13 +564,12 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
ServiceCollection.AddMemoryCache();
|
ServiceCollection.AddMemoryCache();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton(ConfigurationManager);
|
ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
|
||||||
|
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
|
||||||
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
||||||
|
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
||||||
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
|
|
||||||
|
|
||||||
ServiceCollection.AddSingleton(_fileSystemManager);
|
ServiceCollection.AddSingleton(_fileSystemManager);
|
||||||
ServiceCollection.AddSingleton<TmdbClientManager>();
|
ServiceCollection.AddSingleton<TmdbClientManager>();
|
||||||
|
|
||||||
|
@ -550,8 +592,6 @@ namespace Emby.Server.Implementations
|
||||||
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
||||||
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
||||||
|
|
||||||
ServiceCollection.AddSingleton(ServerConfigurationManager);
|
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||||
|
@ -563,12 +603,8 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
||||||
|
|
||||||
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
|
||||||
ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
|
|
||||||
|
|
||||||
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
|
||||||
ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
|
|
||||||
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||||
|
ServiceCollection.AddSingleton<EncodingHelper>();
|
||||||
|
|
||||||
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
||||||
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
||||||
|
@ -633,14 +669,14 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<EncodingHelper>();
|
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<TranscodingJobHelper>();
|
ServiceCollection.AddSingleton<TranscodingJobHelper>();
|
||||||
ServiceCollection.AddScoped<MediaInfoHelper>();
|
ServiceCollection.AddScoped<MediaInfoHelper>();
|
||||||
ServiceCollection.AddScoped<AudioHelper>();
|
ServiceCollection.AddScoped<AudioHelper>();
|
||||||
ServiceCollection.AddScoped<DynamicHlsHelper>();
|
ServiceCollection.AddScoped<DynamicHlsHelper>();
|
||||||
|
|
||||||
|
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -714,7 +750,7 @@ namespace Emby.Server.Implementations
|
||||||
// Don't use an empty string password
|
// Don't use an empty string password
|
||||||
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
|
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
|
||||||
|
|
||||||
var localCert = new X509Certificate2(certificateLocation, password);
|
var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
|
||||||
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
|
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
|
||||||
if (!localCert.HasPrivateKey)
|
if (!localCert.HasPrivateKey)
|
||||||
{
|
{
|
||||||
|
@ -738,7 +774,7 @@ namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
// For now there's no real way to inject these properly
|
// For now there's no real way to inject these properly
|
||||||
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
||||||
BaseItem.ConfigurationManager = ServerConfigurationManager;
|
BaseItem.ConfigurationManager = ConfigurationManager;
|
||||||
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
||||||
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
||||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||||
|
@ -752,7 +788,6 @@ namespace Emby.Server.Implementations
|
||||||
UserView.CollectionManager = Resolve<ICollectionManager>();
|
UserView.CollectionManager = Resolve<ICollectionManager>();
|
||||||
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
|
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
|
||||||
CollectionFolder.XmlSerializer = _xmlSerializer;
|
CollectionFolder.XmlSerializer = _xmlSerializer;
|
||||||
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
|
|
||||||
CollectionFolder.ApplicationHost = this;
|
CollectionFolder.ApplicationHost = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -761,41 +796,13 @@ namespace Emby.Server.Implementations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void FindParts()
|
private void FindParts()
|
||||||
{
|
{
|
||||||
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
|
if (!ConfigurationManager.Configuration.IsPortAuthorized)
|
||||||
{
|
{
|
||||||
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
|
ConfigurationManager.Configuration.IsPortAuthorized = true;
|
||||||
ConfigurationManager.SaveConfiguration();
|
ConfigurationManager.SaveConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
_pluginManager.CreatePlugins();
|
||||||
_plugins = GetExports<IPlugin>()
|
|
||||||
.Where(i => i != null)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
if (Plugins != null)
|
|
||||||
{
|
|
||||||
foreach (var plugin in Plugins)
|
|
||||||
{
|
|
||||||
if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin)
|
|
||||||
{
|
|
||||||
// Ensure the version number matches the Plugin Manifest information.
|
|
||||||
foreach (var item in _pluginsManifests)
|
|
||||||
{
|
|
||||||
if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// Update version number to that of the manifest.
|
|
||||||
assemblyPlugin.SetAttributes(
|
|
||||||
plugin.AssemblyFilePath,
|
|
||||||
Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)),
|
|
||||||
item.Version);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_urlPrefixes = GetUrlPrefixes().ToArray();
|
_urlPrefixes = GetUrlPrefixes().ToArray();
|
||||||
|
|
||||||
|
@ -834,22 +841,6 @@ namespace Emby.Server.Implementations
|
||||||
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
|
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterPluginServices()
|
|
||||||
{
|
|
||||||
foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
|
|
||||||
instance.RegisterServices(ServiceCollection);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
|
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
|
||||||
{
|
{
|
||||||
foreach (var ass in assemblies)
|
foreach (var ass in assemblies)
|
||||||
|
@ -862,11 +853,13 @@ namespace Emby.Server.Implementations
|
||||||
catch (FileNotFoundException ex)
|
catch (FileNotFoundException ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
|
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
|
||||||
|
_pluginManager.FailPlugin(ass);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
catch (TypeLoadException ex)
|
catch (TypeLoadException ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
|
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
|
||||||
|
_pluginManager.FailPlugin(ass);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -912,19 +905,19 @@ namespace Emby.Server.Implementations
|
||||||
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var requiresRestart = false;
|
var requiresRestart = false;
|
||||||
|
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||||
|
|
||||||
// Don't do anything if these haven't been set yet
|
// Don't do anything if these haven't been set yet
|
||||||
if (HttpPort != 0 && HttpsPort != 0)
|
if (HttpPort != 0 && HttpsPort != 0)
|
||||||
{
|
{
|
||||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
|
||||||
// Need to restart if ports have changed
|
// Need to restart if ports have changed
|
||||||
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
||||||
networkConfiguration.HttpsPortNumber != HttpsPort)
|
networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||||
{
|
{
|
||||||
if (ServerConfigurationManager.Configuration.IsPortAuthorized)
|
if (ConfigurationManager.Configuration.IsPortAuthorized)
|
||||||
{
|
{
|
||||||
ServerConfigurationManager.Configuration.IsPortAuthorized = false;
|
ConfigurationManager.Configuration.IsPortAuthorized = false;
|
||||||
ServerConfigurationManager.SaveConfiguration();
|
ConfigurationManager.SaveConfiguration();
|
||||||
|
|
||||||
requiresRestart = true;
|
requiresRestart = true;
|
||||||
}
|
}
|
||||||
|
@ -936,10 +929,7 @@ namespace Emby.Server.Implementations
|
||||||
requiresRestart = true;
|
requiresRestart = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentCertPath = CertificateInfo?.Path;
|
if (ValidateSslCertificate(networkConfiguration))
|
||||||
var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
|
|
||||||
|
|
||||||
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
requiresRestart = true;
|
requiresRestart = true;
|
||||||
}
|
}
|
||||||
|
@ -952,6 +942,33 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the SSL certificate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="networkConfig">The new configuration.</param>
|
||||||
|
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
|
||||||
|
private bool ValidateSslCertificate(NetworkConfiguration networkConfig)
|
||||||
|
{
|
||||||
|
var newPath = networkConfig.CertificatePath;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(newPath)
|
||||||
|
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (File.Exists(newPath))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"Certificate file '{0}' does not exist.",
|
||||||
|
newPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Notifies that the kernel that a change has been made that requires a restart.
|
/// Notifies that the kernel that a change has been made that requires a restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1005,129 +1022,15 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
protected abstract void RestartInternal();
|
protected abstract void RestartInternal();
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
|
|
||||||
{
|
|
||||||
var minimumVersion = new Version(0, 0, 0, 1);
|
|
||||||
var versions = new List<LocalPlugin>();
|
|
||||||
if (!Directory.Exists(path))
|
|
||||||
{
|
|
||||||
// Plugin path doesn't exist, don't try to enumerate subfolders.
|
|
||||||
return Enumerable.Empty<LocalPlugin>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
|
|
||||||
|
|
||||||
foreach (var dir in directories)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var metafile = Path.Combine(dir, "meta.json");
|
|
||||||
if (File.Exists(metafile))
|
|
||||||
{
|
|
||||||
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
|
|
||||||
|
|
||||||
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
|
|
||||||
{
|
|
||||||
targetAbi = minimumVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Version.TryParse(manifest.Version, out var version))
|
|
||||||
{
|
|
||||||
version = minimumVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ApplicationVersion >= targetAbi)
|
|
||||||
{
|
|
||||||
// Only load Plugins if the plugin is built for this version or below.
|
|
||||||
versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No metafile, so lets see if the folder is versioned.
|
|
||||||
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
|
|
||||||
|
|
||||||
int versionIndex = dir.LastIndexOf('_');
|
|
||||||
if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion))
|
|
||||||
{
|
|
||||||
// Versioned folder.
|
|
||||||
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
|
|
||||||
versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string lastName = string.Empty;
|
|
||||||
versions.Sort(LocalPlugin.Compare);
|
|
||||||
// Traverse backwards through the list.
|
|
||||||
// The first item will be the latest version.
|
|
||||||
for (int x = versions.Count - 1; x >= 0; x--)
|
|
||||||
{
|
|
||||||
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
|
|
||||||
lastName = versions[x].Name;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(lastName) && cleanup)
|
|
||||||
{
|
|
||||||
// Attempt a cleanup of old folders.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Deleting {Path}", versions[x].Path);
|
|
||||||
Directory.Delete(versions[x].Path, true);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
versions.RemoveAt(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return versions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the composable part assemblies.
|
/// Gets the composable part assemblies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>IEnumerable{Assembly}.</returns>
|
/// <returns>IEnumerable{Assembly}.</returns>
|
||||||
protected IEnumerable<Assembly> GetComposablePartAssemblies()
|
protected IEnumerable<Assembly> GetComposablePartAssemblies()
|
||||||
{
|
{
|
||||||
if (Directory.Exists(ApplicationPaths.PluginsPath))
|
foreach (var p in _pluginManager.LoadAssemblies())
|
||||||
{
|
{
|
||||||
_pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList();
|
yield return p;
|
||||||
foreach (var plugin in _pluginsManifests)
|
|
||||||
{
|
|
||||||
foreach (var file in plugin.DllFiles)
|
|
||||||
{
|
|
||||||
Assembly plugAss;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
plugAss = Assembly.LoadFrom(file);
|
|
||||||
}
|
|
||||||
catch (FileLoadException ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Failed to load assembly {Path}", file);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
|
|
||||||
yield return plugAss;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include composable parts in the Model assembly
|
// Include composable parts in the Model assembly
|
||||||
|
@ -1230,16 +1133,16 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
||||||
|
@ -1256,10 +1159,10 @@ namespace Emby.Server.Implementations
|
||||||
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(request, out port);
|
string smart = NetManager.GetBindInterface(request, out port);
|
||||||
|
@ -1276,10 +1179,10 @@ namespace Emby.Server.Implementations
|
||||||
public string GetSmartApiUrl(string hostname, int? port = null)
|
public string GetSmartApiUrl(string hostname, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(hostname, out port);
|
string smart = NetManager.GetBindInterface(hostname, out port);
|
||||||
|
@ -1314,14 +1217,14 @@ namespace Emby.Server.Implementations
|
||||||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
||||||
Host = host,
|
Host = host,
|
||||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
||||||
Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
|
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||||
}.ToString().TrimEnd('/');
|
}.ToString().TrimEnd('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FriendlyName =>
|
public string FriendlyName =>
|
||||||
string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
|
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
|
||||||
? Environment.MachineName
|
? Environment.MachineName
|
||||||
: ServerConfigurationManager.Configuration.ServerName;
|
: ConfigurationManager.Configuration.ServerName;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shuts down.
|
/// Shuts down.
|
||||||
|
@ -1369,17 +1272,6 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes the plugin.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="plugin">The plugin.</param>
|
|
||||||
public void RemovePlugin(IPlugin plugin)
|
|
||||||
{
|
|
||||||
var list = _plugins.ToList();
|
|
||||||
list.Remove(plugin);
|
|
||||||
_plugins = list.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
||||||
{
|
{
|
||||||
var assemblies = _allConcreteTypes
|
var assemblies = _allConcreteTypes
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Common.Progress;
|
using MediaBrowser.Common.Progress;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
@ -21,7 +25,6 @@ using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||||
|
@ -44,10 +47,10 @@ namespace Emby.Server.Implementations.Channels
|
||||||
private readonly ILogger<ChannelManager> _logger;
|
private readonly ILogger<ChannelManager> _logger;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
|
||||||
private readonly IProviderManager _providerManager;
|
private readonly IProviderManager _providerManager;
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly IMemoryCache _memoryCache;
|
||||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
|
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
|
||||||
|
@ -59,7 +62,6 @@ namespace Emby.Server.Implementations.Channels
|
||||||
/// <param name="config">The server configuration manager.</param>
|
/// <param name="config">The server configuration manager.</param>
|
||||||
/// <param name="fileSystem">The filesystem.</param>
|
/// <param name="fileSystem">The filesystem.</param>
|
||||||
/// <param name="userDataManager">The user data manager.</param>
|
/// <param name="userDataManager">The user data manager.</param>
|
||||||
/// <param name="jsonSerializer">The JSON serializer.</param>
|
|
||||||
/// <param name="providerManager">The provider manager.</param>
|
/// <param name="providerManager">The provider manager.</param>
|
||||||
/// <param name="memoryCache">The memory cache.</param>
|
/// <param name="memoryCache">The memory cache.</param>
|
||||||
public ChannelManager(
|
public ChannelManager(
|
||||||
|
@ -70,7 +72,6 @@ namespace Emby.Server.Implementations.Channels
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IUserDataManager userDataManager,
|
IUserDataManager userDataManager,
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IProviderManager providerManager,
|
IProviderManager providerManager,
|
||||||
IMemoryCache memoryCache)
|
IMemoryCache memoryCache)
|
||||||
{
|
{
|
||||||
|
@ -81,7 +82,6 @@ namespace Emby.Server.Implementations.Channels
|
||||||
_config = config;
|
_config = config;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_userDataManager = userDataManager;
|
_userDataManager = userDataManager;
|
||||||
_jsonSerializer = jsonSerializer;
|
|
||||||
_providerManager = providerManager;
|
_providerManager = providerManager;
|
||||||
_memoryCache = memoryCache;
|
_memoryCache = memoryCache;
|
||||||
}
|
}
|
||||||
|
@ -337,21 +337,23 @@ namespace Emby.Server.Implementations.Channels
|
||||||
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
|
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<MediaSourceInfo> GetSavedMediaSources(BaseItem item)
|
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
|
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _jsonSerializer.DeserializeFromFile<List<MediaSourceInfo>>(path) ?? new List<MediaSourceInfo>();
|
var bytes = File.ReadAllBytes(path);
|
||||||
|
return JsonSerializer.Deserialize<MediaSourceInfo[]>(bytes, _jsonOptions)
|
||||||
|
?? Array.Empty<MediaSourceInfo>();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return new List<MediaSourceInfo>();
|
return Array.Empty<MediaSourceInfo>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
|
private async Task SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
|
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
|
||||||
|
|
||||||
|
@ -370,7 +372,8 @@ namespace Emby.Server.Implementations.Channels
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||||
|
|
||||||
_jsonSerializer.SerializeToFile(mediaSources, path);
|
await using FileStream createStream = File.Create(path);
|
||||||
|
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -812,7 +815,8 @@ namespace Emby.Server.Implementations.Channels
|
||||||
{
|
{
|
||||||
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
|
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
|
await using FileStream jsonStream = File.OpenRead(cachePath);
|
||||||
|
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||||
if (cachedResult != null)
|
if (cachedResult != null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
@ -834,7 +838,8 @@ namespace Emby.Server.Implementations.Channels
|
||||||
{
|
{
|
||||||
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
|
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
|
await using FileStream jsonStream = File.OpenRead(cachePath);
|
||||||
|
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||||
if (cachedResult != null)
|
if (cachedResult != null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
@ -865,7 +870,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
|
throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
|
||||||
}
|
}
|
||||||
|
|
||||||
CacheResponse(result, cachePath);
|
await CacheResponse(result, cachePath);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -875,13 +880,14 @@ namespace Emby.Server.Implementations.Channels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CacheResponse(object result, string path)
|
private async Task CacheResponse(object result, string path)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||||
|
|
||||||
_jsonSerializer.SerializeToFile(result, path);
|
await using FileStream createStream = File.Create(path);
|
||||||
|
await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -1176,11 +1182,11 @@ namespace Emby.Server.Implementations.Channels
|
||||||
{
|
{
|
||||||
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
|
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
|
||||||
{
|
{
|
||||||
SaveMediaSources(item, new List<MediaSourceInfo>());
|
await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SaveMediaSources(item, info.MediaSources);
|
await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.Where(i => i != null)
|
.Where(i => i != null)
|
||||||
.GroupBy(x => x.Id)
|
.GroupBy(x => x!.Id) // We removed the null values
|
||||||
.Select(x => x.First())
|
.Select(x => x.First())
|
||||||
.ToList();
|
.ToList()!; // Again... the list doesn't contain any null values
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -8,11 +9,9 @@ using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Collections;
|
using MediaBrowser.Controller.Collections;
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Plugins;
|
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
@ -107,7 +106,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
|
|
||||||
var name = _localizationManager.GetLocalizedString("Collections");
|
var name = _localizationManager.GetLocalizedString("Collections");
|
||||||
|
|
||||||
await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false);
|
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
|
||||||
|
|
||||||
return FindFolders(path).First();
|
return FindFolders(path).First();
|
||||||
}
|
}
|
||||||
|
@ -124,7 +123,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
|
|
||||||
private IEnumerable<BoxSet> GetCollections(User user)
|
private IEnumerable<BoxSet> GetCollections(User user)
|
||||||
{
|
{
|
||||||
var folder = GetCollectionsFolder(false).Result;
|
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
|
||||||
|
|
||||||
return folder == null
|
return folder == null
|
||||||
? Enumerable.Empty<BoxSet>()
|
? Enumerable.Empty<BoxSet>()
|
||||||
|
@ -167,7 +166,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
|
|
||||||
parentFolder.AddChild(collection, CancellationToken.None);
|
parentFolder.AddChild(collection, CancellationToken.None);
|
||||||
|
|
||||||
if (options.ItemIdList.Length > 0)
|
if (options.ItemIdList.Count > 0)
|
||||||
{
|
{
|
||||||
await AddToCollectionAsync(
|
await AddToCollectionAsync(
|
||||||
collection.Id,
|
collection.Id,
|
||||||
|
@ -251,11 +250,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
|
|
||||||
if (fireEvent)
|
if (fireEvent)
|
||||||
{
|
{
|
||||||
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs
|
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
|
||||||
{
|
|
||||||
Collection = collection,
|
|
||||||
ItemsChanged = itemList
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,11 +302,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
},
|
},
|
||||||
RefreshPriority.High);
|
RefreshPriority.High);
|
||||||
|
|
||||||
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
|
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
|
||||||
{
|
|
||||||
Collection = collection,
|
|
||||||
ItemsChanged = itemList
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -319,11 +310,11 @@ namespace Emby.Server.Implementations.Collections
|
||||||
{
|
{
|
||||||
var results = new Dictionary<Guid, BaseItem>();
|
var results = new Dictionary<Guid, BaseItem>();
|
||||||
|
|
||||||
var allBoxsets = GetCollections(user).ToList();
|
var allBoxSets = GetCollections(user).ToList();
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
if (!(item is ISupportsBoxSetGrouping))
|
if (item is not ISupportsBoxSetGrouping)
|
||||||
{
|
{
|
||||||
results[item.Id] = item;
|
results[item.Id] = item;
|
||||||
}
|
}
|
||||||
|
@ -331,20 +322,44 @@ namespace Emby.Server.Implementations.Collections
|
||||||
{
|
{
|
||||||
var itemId = item.Id;
|
var itemId = item.Id;
|
||||||
|
|
||||||
var currentBoxSets = allBoxsets
|
var itemIsInBoxSet = false;
|
||||||
.Where(i => i.ContainsLinkedChildByItemId(itemId))
|
foreach (var boxSet in allBoxSets)
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (currentBoxSets.Count > 0)
|
|
||||||
{
|
{
|
||||||
foreach (var boxset in currentBoxSets)
|
if (!boxSet.ContainsLinkedChildByItemId(itemId))
|
||||||
{
|
{
|
||||||
results[boxset.Id] = boxset;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemIsInBoxSet = true;
|
||||||
|
|
||||||
|
results.TryAdd(boxSet.Id, boxSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip any item that is in a box set
|
||||||
|
if (itemIsInBoxSet)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alreadyInResults = false;
|
||||||
|
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
|
||||||
|
if (item is Video video)
|
||||||
|
{
|
||||||
|
foreach (var childId in video.GetLocalAlternateVersionIds())
|
||||||
|
{
|
||||||
|
if (!results.ContainsKey(childId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
alreadyInResults = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (!alreadyInResults)
|
||||||
{
|
{
|
||||||
results[item.Id] = item;
|
results[itemId] = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -88,38 +90,12 @@ namespace Emby.Server.Implementations.Configuration
|
||||||
var newConfig = (ServerConfiguration)newConfiguration;
|
var newConfig = (ServerConfiguration)newConfiguration;
|
||||||
|
|
||||||
ValidateMetadataPath(newConfig);
|
ValidateMetadataPath(newConfig);
|
||||||
ValidateSslCertificate(newConfig);
|
|
||||||
|
|
||||||
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
|
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
|
||||||
|
|
||||||
base.ReplaceConfiguration(newConfiguration);
|
base.ReplaceConfiguration(newConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates the SSL certificate.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="newConfig">The new configuration.</param>
|
|
||||||
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
|
|
||||||
private void ValidateSslCertificate(BaseApplicationConfiguration newConfig)
|
|
||||||
{
|
|
||||||
var serverConfig = (ServerConfiguration)newConfig;
|
|
||||||
|
|
||||||
var newPath = serverConfig.CertificatePath;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(newPath)
|
|
||||||
&& !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (!File.Exists(newPath))
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException(
|
|
||||||
string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"Certificate file '{0}' does not exist.",
|
|
||||||
newPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates the metadata path.
|
/// Validates the metadata path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Server.Implementations.HttpServer;
|
|
||||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations
|
namespace Emby.Server.Implementations
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data
|
||||||
|
|
||||||
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
|
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
|
||||||
{
|
{
|
||||||
if (row[1].SQLiteType != SQLiteType.Null)
|
if (row.TryGetString(1, out var columnName))
|
||||||
{
|
{
|
||||||
var name = row[1].ToString();
|
columnNames.Add(columnName);
|
||||||
|
|
||||||
columnNames.Add(name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user