Merge branch 'master' into bug/authorization-header-issue
This commit is contained in:
commit
2b232df07f
|
@ -7,7 +7,7 @@ parameters:
|
|||
default: "ubuntu-latest"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 5.0.100
|
||||
default: 5.0.103
|
||||
|
||||
jobs:
|
||||
- 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:
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
DotNetSdkVersion: 5.0.100
|
||||
DotNetSdkVersion: 5.0.103
|
||||
|
||||
jobs:
|
||||
- job: Build
|
||||
|
|
|
@ -22,6 +22,12 @@ jobs:
|
|||
BuildConfiguration: ubuntu.armhf
|
||||
Linux.amd64:
|
||||
BuildConfiguration: linux.amd64
|
||||
Linux.amd64-musl:
|
||||
BuildConfiguration: linux.amd64-musl
|
||||
Linux.arm64:
|
||||
BuildConfiguration: linux.arm64
|
||||
Linux.armhf:
|
||||
BuildConfiguration: linux.armhf
|
||||
Windows.amd64:
|
||||
BuildConfiguration: windows.amd64
|
||||
MacOS:
|
||||
|
@ -154,7 +160,6 @@ jobs:
|
|||
dependsOn:
|
||||
- BuildPackage
|
||||
- BuildDocker
|
||||
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
@ -180,13 +185,14 @@ jobs:
|
|||
|
||||
- job: PublishNuget
|
||||
displayName: 'Publish NuGet packages'
|
||||
dependsOn:
|
||||
- BuildPackage
|
||||
condition: succeeded('BuildPackage')
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
variables:
|
||||
- name: JellyfinVersion
|
||||
value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Use .NET 5.0 sdk'
|
||||
|
@ -198,12 +204,19 @@ jobs:
|
|||
displayName: 'Build Stable Nuget packages'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
inputs:
|
||||
command: 'pack'
|
||||
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'
|
||||
versioningScheme: 'off'
|
||||
command: 'custom'
|
||||
projects: |
|
||||
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
|
||||
displayName: 'Build Unstable Nuget packages'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
inputs:
|
||||
command: 'custom'
|
||||
projects: |
|
||||
|
@ -226,7 +239,7 @@ jobs:
|
|||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
inputs:
|
||||
command: 'push'
|
||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
|
||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
|
||||
nuGetFeedType: 'external'
|
||||
publishFeedCredentials: 'NugetOrg'
|
||||
allowPackageConflicts: true # This ignores an error if the version already exists
|
||||
|
|
|
@ -10,7 +10,7 @@ parameters:
|
|||
default: "tests/**/*Tests.csproj"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 5.0.100
|
||||
default: 5.0.103
|
||||
|
||||
jobs:
|
||||
- job: Test
|
||||
|
@ -94,5 +94,5 @@ jobs:
|
|||
displayName: 'Publish OpenAPI Artifact'
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
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'
|
||||
|
|
|
@ -6,7 +6,7 @@ variables:
|
|||
- name: RestoreBuildProjects
|
||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
- name: DotNetSdkVersion
|
||||
value: 5.0.100
|
||||
value: 5.0.103
|
||||
|
||||
pr:
|
||||
autoCancel: true
|
||||
|
@ -61,6 +61,3 @@ jobs:
|
|||
|
||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
||||
- 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**
|
||||
<!-- 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. -->
|
||||
|
||||
**Screenshots**
|
||||
|
|
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
|
@ -7,3 +7,9 @@ updates:
|
|||
time: '12:00'
|
||||
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
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: '5.0.100'
|
||||
dotnet-version: '5.0.x'
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
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)
|
||||
- [chaosinnovator](https://github.com/chaosinnovator)
|
||||
- [ckcr4lyf](https://github.com/ckcr4lyf)
|
||||
- [cocool97](https://github.com/cocool97)
|
||||
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
||||
- [crankdoofus](https://github.com/crankdoofus)
|
||||
- [crobibero](https://github.com/crobibero)
|
||||
|
@ -49,6 +50,7 @@
|
|||
- [h1nk](https://github.com/h1nk)
|
||||
- [hawken93](https://github.com/hawken93)
|
||||
- [HelloWorld017](https://github.com/HelloWorld017)
|
||||
- [ikomhoog](https://github.com/ikomhoog)
|
||||
- [jftuga](https://github.com/jftuga)
|
||||
- [joern-h](https://github.com/joern-h)
|
||||
- [joshuaboniface](https://github.com/joshuaboniface)
|
||||
|
@ -68,6 +70,7 @@
|
|||
- [marius-luca-87](https://github.com/marius-luca-87)
|
||||
- [mark-monteiro](https://github.com/mark-monteiro)
|
||||
- [Matt07211](https://github.com/Matt07211)
|
||||
- [Maxr1998](https://github.com/Maxr1998)
|
||||
- [mcarlton00](https://github.com/mcarlton00)
|
||||
- [mitchfizz05](https://github.com/mitchfizz05)
|
||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||
|
@ -80,6 +83,7 @@
|
|||
- [nvllsvm](https://github.com/nvllsvm)
|
||||
- [nyanmisaka](https://github.com/nyanmisaka)
|
||||
- [OancaAndrei](https://github.com/OancaAndrei)
|
||||
- [obradovichv](https://github.com/obradovichv)
|
||||
- [oddstr13](https://github.com/oddstr13)
|
||||
- [orryverducci](https://github.com/orryverducci)
|
||||
- [petermcneil](https://github.com/petermcneil)
|
||||
|
@ -103,6 +107,7 @@
|
|||
- [shemanaev](https://github.com/shemanaev)
|
||||
- [skaro13](https://github.com/skaro13)
|
||||
- [sl1288](https://github.com/sl1288)
|
||||
- [Smith00101010](https://github.com/Smith00101010)
|
||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||
- [sparky8251](https://github.com/sparky8251)
|
||||
- [spookbits](https://github.com/spookbits)
|
||||
|
@ -141,6 +146,8 @@
|
|||
- [Pusta](https://github.com/pusta)
|
||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||
- [skyfrk](https://github.com/skyfrk)
|
||||
- [ianjazz246](https://github.com/ianjazz246)
|
||||
- [peterspenler](https://github.com/peterspenler)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
ARG DOTNET_VERSION=5.0
|
||||
|
||||
FROM node:alpine as web-builder
|
||||
FROM node:lts-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm 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 - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& yarn install \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& 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
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
ARG DOTNET_VERSION=5.0
|
||||
|
||||
|
||||
FROM node:alpine as web-builder
|
||||
FROM node:lts-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm 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 - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& yarn install \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& mv dist /dist
|
||||
|
||||
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
ARG DOTNET_VERSION=5.0
|
||||
|
||||
|
||||
FROM node:alpine as web-builder
|
||||
FROM node:lts-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm 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 - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& yarn install \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& mv dist /dist
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna.Configuration
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#nullable enable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using Emby.Dlna.Configuration;
|
||||
|
|
|
@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
|
|||
}
|
||||
|
||||
/// <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))
|
||||
{
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
@ -7,7 +8,6 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Xml;
|
||||
using Emby.Dlna.Configuration;
|
||||
using Emby.Dlna.Didl;
|
||||
using Emby.Dlna.Service;
|
||||
using Jellyfin.Data.Entities;
|
||||
|
@ -121,7 +121,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
|
@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory
|
|||
/// <summary>
|
||||
/// Adds a "XSetBookmark" element to the xml document.
|
||||
/// </summary>
|
||||
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
||||
private void HandleXSetBookmark(IDictionary<string, string> sparams)
|
||||
/// <param name="sparams">The method parameters.</param>
|
||||
private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams)
|
||||
{
|
||||
var id = sparams["ObjectID"];
|
||||
|
||||
|
@ -305,35 +305,18 @@ namespace Emby.Dlna.ContentDirectory
|
|||
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>
|
||||
/// Builds the "Browse" xml response.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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 flag = sparams["BrowseFlag"];
|
||||
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
|
||||
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
|
||||
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
|
||||
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
|
||||
|
||||
var provided = 0;
|
||||
|
||||
|
@ -435,9 +418,9 @@ namespace Emby.Dlna.ContentDirectory
|
|||
/// Builds the response to the "X_BrowseByLetter request.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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
|
||||
HandleSearch(xmlWriter, sparams, deviceId);
|
||||
|
@ -447,13 +430,13 @@ namespace Emby.Dlna.ContentDirectory
|
|||
/// Builds a response to the "Search" request.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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 sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
|
||||
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
|
||||
var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty));
|
||||
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
|
||||
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
|
||||
|
||||
// sort example: dc:title, dc:date
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1602
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -96,6 +98,7 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
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))
|
||||
{
|
||||
// writer.WriteStartDocument();
|
||||
|
@ -207,7 +210,8 @@ namespace Emby.Dlna.Didl
|
|||
var targetWidth = streamInfo.TargetWidth;
|
||||
var targetHeight = streamInfo.TargetHeight;
|
||||
|
||||
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
|
||||
var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
|
||||
_profile,
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
|
@ -598,7 +602,8 @@ namespace Emby.Dlna.Didl
|
|||
? MimeTypes.GetMimeType(filename)
|
||||
: mediaProfile.MimeType;
|
||||
|
||||
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
|
||||
var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
|
||||
_profile,
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
targetAudioBitrate,
|
||||
|
@ -973,15 +978,28 @@ namespace Emby.Dlna.Didl
|
|||
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);
|
||||
if (!string.IsNullOrEmpty(_profile.AlbumArtPn))
|
||||
{
|
||||
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
|
||||
writer.WriteString(albumartUrlInfo.url);
|
||||
}
|
||||
|
||||
writer.WriteString(albumArtUrlInfo.url);
|
||||
writer.WriteFullEndElement();
|
||||
|
||||
// TOOD: Remove these default values
|
||||
var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
|
||||
// TODO: Remove these default values
|
||||
var iconUrlInfo = GetImageUrl(
|
||||
imageInfo,
|
||||
_profile.MaxIconWidth ?? 48,
|
||||
_profile.MaxIconHeight ?? 48,
|
||||
"jpg");
|
||||
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
|
||||
|
||||
if (!_profile.EnableAlbumArtInDidl)
|
||||
|
@ -1032,8 +1050,7 @@ namespace Emby.Dlna.Didl
|
|||
var width = albumartUrlInfo.width ?? maxWidth;
|
||||
var height = albumartUrlInfo.height ?? maxHeight;
|
||||
|
||||
var contentFeatures = new ContentFeatureBuilder(_profile)
|
||||
.BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
|
||||
var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
|
||||
|
||||
writer.WriteAttributeString(
|
||||
"protocolInfo",
|
||||
|
@ -1205,8 +1222,7 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
if (width.HasValue && height.HasValue)
|
||||
{
|
||||
var newSize = DrawingUtils.Resize(
|
||||
new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
|
||||
var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
|
||||
|
||||
width = newSize.Width;
|
||||
height = newSize.Height;
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
public class StringWriterWithEncoding : StringWriter
|
||||
{
|
||||
private readonly Encoding _encoding;
|
||||
private readonly Encoding? _encoding;
|
||||
|
||||
public StringWriterWithEncoding()
|
||||
{
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#nullable enable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -7,12 +9,14 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Profiles;
|
||||
using Emby.Dlna.Server;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
|
@ -32,9 +36,9 @@ namespace Emby.Dlna
|
|||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<DlnaManager> _logger;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
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);
|
||||
|
||||
|
@ -43,14 +47,12 @@ namespace Emby.Dlna
|
|||
IFileSystem fileSystem,
|
||||
IApplicationPaths appPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IServerApplicationHost appHost)
|
||||
{
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
_logger = loggerFactory.CreateLogger<DlnaManager>();
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
|
@ -111,7 +113,7 @@ namespace Emby.Dlna
|
|||
|
||||
if (profile != null)
|
||||
{
|
||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -138,80 +140,45 @@ namespace Emby.Dlna
|
|||
_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))
|
||||
{
|
||||
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -333,7 +300,12 @@ namespace Emby.Dlna
|
|||
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);
|
||||
}
|
||||
|
@ -395,7 +367,8 @@ namespace Emby.Dlna
|
|||
{
|
||||
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);
|
||||
}
|
||||
|
@ -495,9 +468,9 @@ namespace Emby.Dlna
|
|||
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)
|
||||
|
|
|
@ -21,11 +21,11 @@
|
|||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<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="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
|
@ -78,9 +78,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -5,10 +7,10 @@ using System.Globalization;
|
|||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.PlayTo;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using Jellyfin.Networking.Manager;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
|
@ -52,6 +54,8 @@ namespace Emby.Dlna.Main
|
|||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly object _syncLock = new object();
|
||||
private readonly NetworkConfiguration _netConfig;
|
||||
private readonly bool _disabled;
|
||||
|
||||
private PlayToManager _manager;
|
||||
private SsdpDevicePublisher _publisher;
|
||||
|
@ -122,10 +126,23 @@ namespace Emby.Dlna.Main
|
|||
httpClientFactory,
|
||||
config);
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the dlna server is enabled.
|
||||
/// </summary>
|
||||
public static bool Enabled { get; private set; }
|
||||
|
||||
public IContentDirectory ContentDirectory { get; private set; }
|
||||
|
||||
public IConnectionManager ConnectionManager { get; private set; }
|
||||
|
@ -136,6 +153,12 @@ namespace Emby.Dlna.Main
|
|||
{
|
||||
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
||||
|
||||
if (_disabled)
|
||||
{
|
||||
// No use starting as dlna won't work, as we're running purely on HTTPS.
|
||||
return;
|
||||
}
|
||||
|
||||
ReloadComponents();
|
||||
|
||||
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
||||
|
@ -152,6 +175,7 @@ namespace Emby.Dlna.Main
|
|||
private void ReloadComponents()
|
||||
{
|
||||
var options = _config.GetDlnaConfiguration();
|
||||
Enabled = options.EnableServer;
|
||||
|
||||
StartSsdpHandler();
|
||||
|
||||
|
@ -205,9 +229,12 @@ namespace Emby.Dlna.Main
|
|||
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (communicationsServer != null)
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting device discovery");
|
||||
|
@ -290,12 +317,18 @@ namespace Emby.Dlna.Main
|
|||
|
||||
_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
|
||||
{
|
||||
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,
|
||||
PrefixLength = address.PrefixLength,
|
||||
FriendlyName = "Jellyfin",
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||
}
|
||||
|
||||
/// <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))
|
||||
{
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -219,7 +221,7 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
|
@ -235,7 +237,13 @@ namespace Emby.Dlna.PlayTo
|
|||
_logger.LogDebug("Setting mute");
|
||||
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);
|
||||
|
||||
IsMuted = mute;
|
||||
|
@ -253,7 +261,7 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
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)
|
||||
{
|
||||
return;
|
||||
|
@ -270,7 +278,13 @@ namespace Emby.Dlna.PlayTo
|
|||
// Remote control will perform better
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -278,7 +292,7 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
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)
|
||||
{
|
||||
return;
|
||||
|
@ -291,7 +305,13 @@ namespace Emby.Dlna.PlayTo
|
|||
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);
|
||||
|
||||
RestartTimer(true);
|
||||
|
@ -305,7 +325,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
_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)
|
||||
{
|
||||
return;
|
||||
|
@ -325,14 +345,21 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
|
||||
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -343,6 +370,42 @@ namespace Emby.Dlna.PlayTo
|
|||
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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
|
@ -378,6 +441,10 @@ namespace Emby.Dlna.PlayTo
|
|||
public async Task SetPlay(CancellationToken cancellationToken)
|
||||
{
|
||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (avCommands == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
@ -388,7 +455,7 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
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)
|
||||
{
|
||||
return;
|
||||
|
@ -396,7 +463,13 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
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);
|
||||
|
||||
RestartTimer(true);
|
||||
|
@ -406,7 +479,7 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
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)
|
||||
{
|
||||
return;
|
||||
|
@ -414,7 +487,13 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
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);
|
||||
|
||||
TransportState = TransportState.Paused;
|
||||
|
@ -528,7 +607,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
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)
|
||||
{
|
||||
return;
|
||||
|
@ -578,7 +657,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
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)
|
||||
{
|
||||
return;
|
||||
|
@ -665,6 +744,10 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
|
||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (rendererCommands == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
|
@ -733,6 +816,11 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (rendererCommands == null)
|
||||
{
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
|
@ -914,6 +1002,10 @@ namespace Emby.Dlna.PlayTo
|
|||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
||||
|
||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
if (document == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
AvCommands = TransportCommands.Create(document);
|
||||
return AvCommands;
|
||||
|
@ -942,6 +1034,10 @@ namespace Emby.Dlna.PlayTo
|
|||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
||||
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
|
||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
if (document == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RendererCommands = TransportCommands.Create(document);
|
||||
return RendererCommands;
|
||||
|
@ -973,6 +1069,10 @@ namespace Emby.Dlna.PlayTo
|
|||
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
|
||||
|
||||
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
|
||||
if (document == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var friendlyNames = new List<string>();
|
||||
|
||||
|
@ -990,7 +1090,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
var deviceProperties = new DeviceInfo()
|
||||
{
|
||||
Name = string.Join(" ", friendlyNames),
|
||||
Name = string.Join(' ', friendlyNames),
|
||||
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -102,6 +104,22 @@ namespace Emby.Dlna.PlayTo
|
|||
_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()
|
||||
{
|
||||
try
|
||||
|
@ -132,7 +150,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -156,6 +174,15 @@ namespace Emby.Dlna.PlayTo
|
|||
var newItemProgress = GetProgressInfo(streamInfo);
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -425,6 +452,11 @@ namespace Emby.Dlna.PlayTo
|
|||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -499,8 +531,8 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
||||
{
|
||||
return new ContentFeatureBuilder(profile)
|
||||
.BuildAudioHeader(
|
||||
return ContentFeatureBuilder.BuildAudioHeader(
|
||||
profile,
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioBitrate,
|
||||
|
@ -514,8 +546,8 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
if (streamInfo.MediaType == DlnaProfileType.Video)
|
||||
{
|
||||
var list = new ContentFeatureBuilder(profile)
|
||||
.BuildVideoHeader(
|
||||
var list = ContentFeatureBuilder.BuildVideoHeader(
|
||||
profile,
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
|
@ -623,6 +655,9 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
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;
|
||||
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);
|
||||
|
||||
// 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))
|
||||
{
|
||||
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);
|
||||
|
||||
// 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)
|
||||
{
|
||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||
|
@ -777,7 +820,7 @@ namespace Emby.Dlna.PlayTo
|
|||
var currentWait = 0;
|
||||
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
|
||||
{
|
||||
await Task.Delay(Interval).ConfigureAwait(false);
|
||||
await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
|
||||
currentWait += Interval;
|
||||
}
|
||||
|
||||
|
@ -826,7 +869,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (name == SessionMessageType.PlayState)
|
||||
if (name == SessionMessageType.Playstate)
|
||||
{
|
||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||
}
|
||||
|
@ -896,16 +939,16 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
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];
|
||||
|
||||
if (string.Equals(part, "audio", 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.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
||||
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
||||
|
||||
// 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.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
||||
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
||||
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
||||
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -178,12 +180,17 @@ namespace Emby.Dlna.PlayTo
|
|||
if (controller == null)
|
||||
{
|
||||
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;
|
||||
|
||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
|
||||
|
||||
controller = new PlayToController(
|
||||
sessionInfo,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
|
@ -45,10 +46,10 @@ namespace Emby.Dlna.PlayTo
|
|||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return XDocument.Parse(
|
||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
||||
LoadOptions.PreserveWhitespace);
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.PreserveWhitespace,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||
|
@ -94,10 +95,17 @@ namespace Emby.Dlna.PlayTo
|
|||
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
||||
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);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return XDocument.Parse(
|
||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
||||
LoadOptions.PreserveWhitespace);
|
||||
try
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.PreserveWhitespace,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
||||
|
|
|
@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
|
|||
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 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)
|
||||
{
|
||||
|
@ -48,7 +46,7 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
var serviceAction = new ServiceAction
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
};
|
||||
|
||||
var argumentList = serviceAction.ArgumentList;
|
||||
|
@ -70,9 +68,9 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
return new Argument
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
|
||||
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
|
||||
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -91,8 +89,8 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
return new StateVariable
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
|
||||
AllowedValues = allowedValues
|
||||
};
|
||||
}
|
||||
|
@ -168,7 +166,7 @@ namespace Emby.Dlna.PlayTo
|
|||
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));
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1602
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
|
@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
public DefaultProfile()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
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:*";
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
|||
|
||||
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",
|
||||
|
||||
Headers = new[]
|
||||
|
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
|||
new HttpHeaderInfo
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
|||
|
||||
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",
|
||||
|
||||
Headers = new[]
|
||||
|
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
|||
new HttpHeaderInfo
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
|||
|
||||
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",
|
||||
|
||||
Headers = new[]
|
||||
|
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
|||
new HttpHeaderInfo
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
|||
|
||||
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",
|
||||
|
||||
Headers = new[]
|
||||
|
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
|||
new HttpHeaderInfo
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
|||
|
||||
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",
|
||||
|
||||
Headers = new[]
|
||||
|
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
|||
new 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).*",
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
|
|||
Container = "ts,mpegts",
|
||||
Type = DlnaProfileType.Video,
|
||||
VideoCodec = "mpeg1video,mpeg2video,h264",
|
||||
AudioCodec = "ac3,mp2,mp3,aac"
|
||||
AudioCodec = "aac,ac3,mp2"
|
||||
},
|
||||
new DirectPlayProfile
|
||||
{
|
||||
|
@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Container = "ts",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
AudioCodec = "aac,ac3,mp2",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
new TranscodingProfile
|
||||
|
|
|
@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
|
|||
Container = "ts,mpegts",
|
||||
Type = DlnaProfileType.Video,
|
||||
VideoCodec = "mpeg1video,mpeg2video,h264",
|
||||
AudioCodec = "ac3,mp2,mp3,aac"
|
||||
AudioCodec = "aac,ac3,mp2"
|
||||
},
|
||||
new DirectPlayProfile
|
||||
{
|
||||
|
@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Container = "ts",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "mp3",
|
||||
AudioCodec = "aac,ac3,mp2",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
new TranscodingProfile
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2010)</Name>
|
||||
<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>
|
||||
<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>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2011)</Name>
|
||||
<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>
|
||||
<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>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2012)</Name>
|
||||
<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>
|
||||
<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>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2013)</Name>
|
||||
<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>
|
||||
<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>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2014)</Name>
|
||||
<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>
|
||||
<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>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<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="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
||||
|
@ -46,7 +46,7 @@
|
|||
</DirectPlayProfiles>
|
||||
<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="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" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<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="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
||||
|
@ -46,7 +46,7 @@
|
|||
</DirectPlayProfiles>
|
||||
<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="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" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
|
|
|
@ -250,7 +250,8 @@ namespace Emby.Dlna.Server
|
|||
|
||||
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()
|
||||
|
|
|
@ -47,9 +47,9 @@ namespace Emby.Dlna.Service
|
|||
|
||||
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()
|
||||
{
|
||||
|
@ -151,7 +151,7 @@ namespace Emby.Dlna.Service
|
|||
|
||||
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
||||
{
|
||||
string namespaceURI = null, localName = null;
|
||||
string? namespaceURI = null, localName = null;
|
||||
|
||||
await reader.MoveToContentAsync().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)
|
||||
{
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -69,7 +71,7 @@ namespace Emby.Dlna.Ssdp
|
|||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_listenerCount > 0 && _deviceLocator == null)
|
||||
if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
|
||||
{
|
||||
_deviceLocator = new SsdpDeviceLocator(_commsServer);
|
||||
|
||||
|
@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp
|
|||
{
|
||||
Location = e.DiscoveredDevice.DescriptionLocation,
|
||||
Headers = headers,
|
||||
LocalIpAddress = e.LocalIpAddress
|
||||
RemoteIpAddress = e.RemoteIpAddress
|
||||
});
|
||||
|
||||
DeviceDiscoveredInternal?.Invoke(this, args);
|
||||
|
|
|
@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp
|
|||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
|
||||
<!-- Code analysers-->
|
||||
<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="StyleCop.Analyzers" Version="1.1.118" 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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
|
@ -171,21 +172,31 @@ namespace Emby.Drawing
|
|||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||
}
|
||||
|
||||
ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
|
||||
int quality = options.Quality;
|
||||
|
||||
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
|
||||
{
|
||||
if (!File.Exists(cacheFilePath))
|
||||
{
|
||||
if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
|
||||
{
|
||||
options.CropWhiteSpace = false;
|
||||
}
|
||||
|
||||
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
|
||||
|
||||
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
|
||||
|
@ -246,48 +257,111 @@ namespace Emby.Drawing
|
|||
/// <summary>
|
||||
/// Gets the cache file path based on a set of parameters.
|
||||
/// </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
|
||||
+ "width=" + outputSize.Width
|
||||
+ "height=" + outputSize.Height
|
||||
+ "quality=" + quality
|
||||
+ "datemodified=" + dateModified.Ticks
|
||||
+ "f=" + format;
|
||||
var filename = new StringBuilder(256);
|
||||
filename.Append(originalPath);
|
||||
|
||||
filename.Append(",quality=");
|
||||
filename.Append(quality);
|
||||
|
||||
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)
|
||||
{
|
||||
filename += "pl=true";
|
||||
filename.Append(",pl=true");
|
||||
}
|
||||
|
||||
if (percentPlayed > 0)
|
||||
{
|
||||
filename += "p=" + percentPlayed;
|
||||
filename.Append(",p=");
|
||||
filename.Append(percentPlayed);
|
||||
}
|
||||
|
||||
if (unwatchedCount.HasValue)
|
||||
{
|
||||
filename += "p=" + unwatchedCount.Value;
|
||||
filename.Append(",p=");
|
||||
filename.Append(unwatchedCount.Value);
|
||||
}
|
||||
|
||||
if (blur.HasValue)
|
||||
{
|
||||
filename += "blur=" + blur.Value;
|
||||
filename.Append(",blur=");
|
||||
filename.Append(blur.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(backgroundColor))
|
||||
{
|
||||
filename += "b=" + backgroundColor;
|
||||
filename.Append(",b=");
|
||||
filename.Append(backgroundColor);
|
||||
}
|
||||
|
||||
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 />
|
||||
|
@ -352,8 +426,13 @@ namespace Emby.Drawing
|
|||
}
|
||||
|
||||
/// <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()
|
||||
.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ namespace Emby.Drawing
|
|||
=> throw new NotImplementedException();
|
||||
|
||||
/// <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();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
|
||||
namespace Emby.Naming.Audio
|
||||
{
|
||||
|
@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
|
|||
/// <returns>True if file at path is audio file.</returns>
|
||||
public static bool IsAudioFile(string path, NamingOptions options)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
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="extras">List of extra 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;
|
||||
Year = year;
|
||||
Files = files ?? new List<AudioBookFileInfo>();
|
||||
Extras = extras ?? new List<AudioBookFileInfo>();
|
||||
AlternateVersions = alternateVersions ?? new List<AudioBookFileInfo>();
|
||||
Files = files;
|
||||
Extras = extras;
|
||||
AlternateVersions = alternateVersions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook
|
|||
|
||||
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -282,7 +282,13 @@ namespace Emby.Naming.Common
|
|||
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/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
|
||||
// /server/anything_1996.11.14.mp4
|
||||
|
@ -299,11 +305,6 @@ namespace Emby.Naming.Common
|
|||
|
||||
// *** 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]+)[^\\\/]*$")
|
||||
{
|
||||
IsNamed = true
|
||||
|
@ -587,7 +588,7 @@ namespace Emby.Naming.Common
|
|||
AudioBookNamesExpressions = new[]
|
||||
{
|
||||
// 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*$"
|
||||
};
|
||||
|
||||
|
|
|
@ -23,17 +23,18 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharedVersion.cs" />
|
||||
<Compile Include="../SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.7.0</VersionPrefix>
|
||||
<VersionPrefix>10.8.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
@ -44,7 +45,6 @@
|
|||
|
||||
<!-- Code Analyzers-->
|
||||
<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="StyleCop.Analyzers" Version="1.1.118" 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)
|
||||
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
||||
|
||||
if (!parsingResult.Success && !isStub)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new EpisodeInfo(path)
|
||||
{
|
||||
Container = container,
|
||||
|
|
|
@ -60,7 +60,7 @@ namespace Emby.Naming.TV
|
|||
bool supportSpecialAliases,
|
||||
bool supportNumericSeasonFolders)
|
||||
{
|
||||
var filename = Path.GetFileName(path) ?? string.Empty;
|
||||
string filename = Path.GetFileName(path);
|
||||
|
||||
if (supportSpecialAliases)
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
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="newName">Parsing result string.</param>
|
||||
/// <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;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
|
@ -41,7 +48,7 @@ namespace Emby.Naming.Video
|
|||
return true;
|
||||
}
|
||||
|
||||
newName = string.Empty;
|
||||
newName = ReadOnlySpan<char>.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,36 +29,33 @@ namespace Emby.Naming.Video
|
|||
/// <param name="path">Path to file.</param>
|
||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||
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();
|
||||
|
||||
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
|
||||
{
|
||||
var rule = _options.VideoExtraRules[i];
|
||||
if (rule.MediaType == MediaType.Audio)
|
||||
{
|
||||
if (!AudioFileParser.IsAudioFile(path, _options))
|
||||
{
|
||||
return result;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (rule.MediaType == MediaType.Video)
|
||||
{
|
||||
if (!new VideoResolver(_options).IsVideoFile(path))
|
||||
{
|
||||
return result;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var pathSpan = path.AsSpan();
|
||||
if (rule.RuleType == ExtraRuleType.Filename)
|
||||
{
|
||||
var filename = Path.GetFileNameWithoutExtension(path);
|
||||
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
||||
|
||||
if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.ExtraType = rule.ExtraType;
|
||||
result.Rule = rule;
|
||||
|
@ -66,9 +63,9 @@ namespace Emby.Naming.Video
|
|||
}
|
||||
else if (rule.RuleType == ExtraRuleType.Suffix)
|
||||
{
|
||||
var filename = Path.GetFileNameWithoutExtension(path);
|
||||
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
||||
|
||||
if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
|
||||
if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.ExtraType = rule.ExtraType;
|
||||
result.Rule = rule;
|
||||
|
@ -88,14 +85,20 @@ namespace Emby.Naming.Video
|
|||
}
|
||||
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
||||
{
|
||||
var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
|
||||
if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -216,26 +216,26 @@ namespace Emby.Naming.Video
|
|||
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 (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
|
||||
{
|
||||
testFilename = cleanName.ToString();
|
||||
}
|
||||
|
||||
// Remove the folder name before cleaning as we don't care about cleaning that part
|
||||
if (folderName.Length <= testFilename.Length)
|
||||
{
|
||||
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)
|
||||
|| testFilename[0].Equals('-')
|
||||
|| testFilename[0].Equals('_')
|
||||
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
|
||||
|| testFilename[0] == '-'
|
||||
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
{
|
||||
|
@ -58,15 +59,15 @@ namespace Emby.Naming.Video
|
|||
}
|
||||
|
||||
bool isStub = false;
|
||||
string? container = null;
|
||||
ReadOnlySpan<char> container = ReadOnlySpan<char>.Empty;
|
||||
string? stubType = null;
|
||||
|
||||
if (!isDirectory)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
|
||||
// Check supported extensions
|
||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// It's not supported. Check stub extensions
|
||||
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
||||
|
@ -85,9 +86,7 @@ namespace Emby.Naming.Video
|
|||
|
||||
var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
|
||||
|
||||
var name = isDirectory
|
||||
? Path.GetFileName(path)
|
||||
: Path.GetFileNameWithoutExtension(path);
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
int? year = null;
|
||||
|
||||
|
@ -106,7 +105,7 @@ namespace Emby.Naming.Video
|
|||
|
||||
return new VideoFileInfo(
|
||||
path: path,
|
||||
container: container,
|
||||
container: container.IsEmpty ? null : container.ToString(),
|
||||
isStub: isStub,
|
||||
name: name,
|
||||
year: year,
|
||||
|
@ -125,8 +124,8 @@ namespace Emby.Naming.Video
|
|||
/// <returns>True if is video file.</returns>
|
||||
public bool IsVideoFile(string path)
|
||||
{
|
||||
var extension = Path.GetExtension(path) ?? string.Empty;
|
||||
return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
return _options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -136,8 +135,8 @@ namespace Emby.Naming.Video
|
|||
/// <returns>True if is video file stub.</returns>
|
||||
public bool IsStubFile(string path)
|
||||
{
|
||||
var extension = Path.GetExtension(path) ?? string.Empty;
|
||||
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
return _options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -146,7 +145,7 @@ namespace Emby.Naming.Video
|
|||
/// <param name="name">Raw name.</param>
|
||||
/// <param name="newName">Clean name.</param>
|
||||
/// <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);
|
||||
}
|
||||
|
|
|
@ -75,10 +75,6 @@ namespace Emby.Notifications
|
|||
Type = NotificationType.VideoPlaybackStopped.ToString()
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.CameraImageUploaded.ToString()
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.UserLockedOut.ToString()
|
||||
},
|
||||
|
@ -114,10 +110,6 @@ namespace Emby.Notifications
|
|||
{
|
||||
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)
|
||||
{
|
||||
note.Category = _localization.GetLocalizedString("User");
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -25,14 +27,9 @@
|
|||
|
||||
<!-- Code analyzers-->
|
||||
<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="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -24,18 +24,15 @@
|
|||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<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="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||
CachePath = cacheDirectoryPath;
|
||||
WebPath = webDirectoryPath;
|
||||
|
||||
DataPath = Path.Combine(ProgramDataPath, "data");
|
||||
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||
/// Gets the folder path to the data directory.
|
||||
/// </summary>
|
||||
/// <value>The data directory.</value>
|
||||
public string DataPath
|
||||
{
|
||||
get => _dataPath;
|
||||
private set => _dataPath = Directory.CreateDirectory(value).FullName;
|
||||
}
|
||||
public string DataPath => _dataPath;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string VirtualDataPath => "%AppDataPath%";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
|
|||
|
||||
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 IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
||||
|
||||
|
@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase
|
|||
/// </summary>
|
||||
private bool _configurationLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// The _configuration sync lock.
|
||||
/// </summary>
|
||||
private readonly object _configurationSyncLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// The _configuration.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.AppBase
|
||||
|
@ -36,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
|
|||
}
|
||||
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);
|
||||
|
@ -53,7 +51,8 @@ namespace Emby.Server.Implementations.AppBase
|
|||
|
||||
Directory.CreateDirectory(directory);
|
||||
// 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);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna;
|
||||
|
@ -42,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
|
|||
using Emby.Server.Implementations.Session;
|
||||
using Emby.Server.Implementations.SyncPlay;
|
||||
using Emby.Server.Implementations.TV;
|
||||
using Emby.Server.Implementations.Udp;
|
||||
using Emby.Server.Implementations.Updates;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
|
@ -97,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
|
|||
using MediaBrowser.XbmcMetadata.Providers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Prometheus.DotNetRuntime;
|
||||
|
@ -116,10 +119,12 @@ namespace Emby.Server.Implementations
|
|||
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
||||
|
||||
private readonly IFileSystem _fileSystemManager;
|
||||
private readonly IConfiguration _startupConfig;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IStartupOptions _startupOptions;
|
||||
private readonly IPluginManager _pluginManager;
|
||||
|
||||
private List<Type> _creatingInstances;
|
||||
private IMediaEncoder _mediaEncoder;
|
||||
private ISessionManager _sessionManager;
|
||||
private string[] _urlPrefixes;
|
||||
|
@ -181,16 +186,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
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>
|
||||
/// Gets the logger factory.
|
||||
/// </summary>
|
||||
|
@ -217,7 +212,7 @@ namespace Emby.Server.Implementations
|
|||
/// Gets or sets the configuration manager.
|
||||
/// </summary>
|
||||
/// <value>The configuration manager.</value>
|
||||
protected IConfigurationManager ConfigurationManager { get; set; }
|
||||
public ServerConfigurationManager ConfigurationManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the service provider.
|
||||
|
@ -235,10 +230,9 @@ namespace Emby.Server.Implementations
|
|||
public int HttpsPort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server configuration manager.
|
||||
/// Gets the value of the PublishedServerUrl setting.
|
||||
/// </summary>
|
||||
/// <value>The server configuration manager.</value>
|
||||
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
|
||||
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
||||
|
||||
/// <summary>
|
||||
/// 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="loggerFactory">Instance of the <see cref="ILoggerFactory"/> 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="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||
public ApplicationHost(
|
||||
IServerApplicationPaths applicationPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IStartupOptions options,
|
||||
IConfiguration startupConfig,
|
||||
IFileSystem fileSystem,
|
||||
IServiceCollection serviceCollection)
|
||||
{
|
||||
_xmlSerializer = new MyXmlSerializer();
|
||||
_jsonSerializer = new JsonSerializer();
|
||||
|
||||
ServiceCollection = serviceCollection;
|
||||
|
||||
ApplicationPaths = applicationPaths;
|
||||
LoggerFactory = loggerFactory;
|
||||
_startupOptions = options;
|
||||
_startupConfig = startupConfig;
|
||||
_fileSystemManager = fileSystem;
|
||||
|
||||
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>());
|
||||
ServiceCollection = serviceCollection;
|
||||
|
||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||
|
||||
_startupOptions = options;
|
||||
|
||||
// Initialize runtime stat collection
|
||||
if (ServerConfigurationManager.Configuration.EnableMetrics)
|
||||
{
|
||||
DotNetRuntimeStatsBuilder.Default().StartCollecting();
|
||||
}
|
||||
|
||||
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;
|
||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||
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>
|
||||
|
@ -306,9 +285,9 @@ namespace Emby.Server.Implementations
|
|||
if (!File.Exists(path))
|
||||
{
|
||||
var networkSettings = new NetworkConfiguration();
|
||||
ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
|
||||
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
|
||||
_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
|
||||
{
|
||||
if (_deviceId == null)
|
||||
{
|
||||
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
|
||||
}
|
||||
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
|
||||
|
||||
return _deviceId.Value;
|
||||
}
|
||||
|
@ -381,7 +357,7 @@ namespace Emby.Server.Implementations
|
|||
/// <summary>
|
||||
/// Creates an instance of type and resolves all constructor dependencies.
|
||||
/// </summary>
|
||||
/// /// <typeparam name="T">The type.</typeparam>
|
||||
/// <typeparam name="T">The type.</typeparam>
|
||||
/// <returns>T.</returns>
|
||||
public T CreateInstance<T>()
|
||||
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
|
||||
|
@ -393,16 +369,38 @@ namespace Emby.Server.Implementations
|
|||
/// <returns>System.Object.</returns>
|
||||
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
|
||||
{
|
||||
_creatingInstances.Add(type);
|
||||
Logger.LogDebug("Creating instance of {Type}", type);
|
||||
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error creating {Type}", type);
|
||||
// If this is a plugin fail it.
|
||||
_pluginManager.FailPlugin(type.Assembly);
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_creatingInstances.Remove(type);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -412,11 +410,7 @@ namespace Emby.Server.Implementations
|
|||
/// <returns>``0.</returns>
|
||||
public T Resolve<T>() => ServiceProvider.GetService<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the export types.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type.</typeparam>
|
||||
/// <returns>IEnumerable{Type}.</returns>
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Type> GetExportTypes<T>()
|
||||
{
|
||||
var currentType = typeof(T);
|
||||
|
@ -445,17 +439,40 @@ namespace Emby.Server.Implementations
|
|||
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>
|
||||
/// Runs the startup tasks.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Task" />.</returns>
|
||||
public async Task RunStartupTasksAsync()
|
||||
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Logger.LogInformation("Running startup tasks");
|
||||
|
||||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||
|
||||
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
||||
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
|
||||
|
||||
_mediaEncoder.SetFFmpegPath();
|
||||
|
||||
|
@ -463,14 +480,21 @@ namespace Emby.Server.Implementations
|
|||
|
||||
var entryPoints = GetExports<IServerEntryPoint>();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
|
||||
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
|
||||
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||
|
||||
Logger.LogInformation("Core startup complete");
|
||||
CoreStartupHasCompleted = true;
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
stopWatch.Restart();
|
||||
|
||||
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
||||
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||
stopWatch.Stop();
|
||||
|
@ -494,7 +518,21 @@ namespace Emby.Server.Implementations
|
|||
/// <inheritdoc/>
|
||||
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;
|
||||
HttpsPort = networkConfiguration.HttpsPortNumber;
|
||||
|
||||
|
@ -505,11 +543,16 @@ namespace Emby.Server.Implementations
|
|||
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
||||
}
|
||||
|
||||
DiscoverTypes();
|
||||
CertificateInfo = new CertificateInfo
|
||||
{
|
||||
Path = networkConfiguration.CertificatePath,
|
||||
Password = networkConfiguration.CertificatePassword
|
||||
};
|
||||
Certificate = GetCertificate(CertificateInfo);
|
||||
|
||||
RegisterServices();
|
||||
|
||||
RegisterPluginServices();
|
||||
_pluginManager.RegisterServices(ServiceCollection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -521,13 +564,12 @@ namespace Emby.Server.Implementations
|
|||
|
||||
ServiceCollection.AddMemoryCache();
|
||||
|
||||
ServiceCollection.AddSingleton(ConfigurationManager);
|
||||
ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
|
||||
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
|
||||
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
||||
|
||||
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
||||
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||
|
||||
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
|
||||
|
||||
ServiceCollection.AddSingleton(_fileSystemManager);
|
||||
ServiceCollection.AddSingleton<TmdbClientManager>();
|
||||
|
||||
|
@ -550,8 +592,6 @@ namespace Emby.Server.Implementations
|
|||
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
||||
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
||||
|
||||
ServiceCollection.AddSingleton(ServerConfigurationManager);
|
||||
|
||||
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||
|
@ -563,12 +603,8 @@ namespace Emby.Server.Implementations
|
|||
|
||||
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<EncodingHelper>();
|
||||
|
||||
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
||||
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<EncodingHelper>();
|
||||
|
||||
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
|
||||
ServiceCollection.AddSingleton<TranscodingJobHelper>();
|
||||
ServiceCollection.AddScoped<MediaInfoHelper>();
|
||||
ServiceCollection.AddScoped<AudioHelper>();
|
||||
ServiceCollection.AddScoped<DynamicHlsHelper>();
|
||||
|
||||
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -714,7 +750,7 @@ namespace Emby.Server.Implementations
|
|||
// Don't use an empty string 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;
|
||||
if (!localCert.HasPrivateKey)
|
||||
{
|
||||
|
@ -738,7 +774,7 @@ namespace Emby.Server.Implementations
|
|||
{
|
||||
// For now there's no real way to inject these properly
|
||||
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
||||
BaseItem.ConfigurationManager = ServerConfigurationManager;
|
||||
BaseItem.ConfigurationManager = ConfigurationManager;
|
||||
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
||||
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||
|
@ -752,7 +788,6 @@ namespace Emby.Server.Implementations
|
|||
UserView.CollectionManager = Resolve<ICollectionManager>();
|
||||
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
|
||||
CollectionFolder.XmlSerializer = _xmlSerializer;
|
||||
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
|
||||
CollectionFolder.ApplicationHost = this;
|
||||
}
|
||||
|
||||
|
@ -761,41 +796,13 @@ namespace Emby.Server.Implementations
|
|||
/// </summary>
|
||||
private void FindParts()
|
||||
{
|
||||
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
|
||||
if (!ConfigurationManager.Configuration.IsPortAuthorized)
|
||||
{
|
||||
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
|
||||
ConfigurationManager.Configuration.IsPortAuthorized = true;
|
||||
ConfigurationManager.SaveConfiguration();
|
||||
}
|
||||
|
||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||
_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);
|
||||
}
|
||||
}
|
||||
_pluginManager.CreatePlugins();
|
||||
|
||||
_urlPrefixes = GetUrlPrefixes().ToArray();
|
||||
|
||||
|
@ -834,22 +841,6 @@ namespace Emby.Server.Implementations
|
|||
_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)
|
||||
{
|
||||
foreach (var ass in assemblies)
|
||||
|
@ -862,11 +853,13 @@ namespace Emby.Server.Implementations
|
|||
catch (FileNotFoundException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
|
||||
_pluginManager.FailPlugin(ass);
|
||||
continue;
|
||||
}
|
||||
catch (TypeLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
|
||||
_pluginManager.FailPlugin(ass);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -912,19 +905,19 @@ namespace Emby.Server.Implementations
|
|||
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var requiresRestart = false;
|
||||
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||
|
||||
// Don't do anything if these haven't been set yet
|
||||
if (HttpPort != 0 && HttpsPort != 0)
|
||||
{
|
||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
||||
// Need to restart if ports have changed
|
||||
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
||||
networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||
{
|
||||
if (ServerConfigurationManager.Configuration.IsPortAuthorized)
|
||||
if (ConfigurationManager.Configuration.IsPortAuthorized)
|
||||
{
|
||||
ServerConfigurationManager.Configuration.IsPortAuthorized = false;
|
||||
ServerConfigurationManager.SaveConfiguration();
|
||||
ConfigurationManager.Configuration.IsPortAuthorized = false;
|
||||
ConfigurationManager.SaveConfiguration();
|
||||
|
||||
requiresRestart = true;
|
||||
}
|
||||
|
@ -936,10 +929,7 @@ namespace Emby.Server.Implementations
|
|||
requiresRestart = true;
|
||||
}
|
||||
|
||||
var currentCertPath = CertificateInfo?.Path;
|
||||
var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
|
||||
|
||||
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
|
||||
if (ValidateSslCertificate(networkConfiguration))
|
||||
{
|
||||
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>
|
||||
/// Notifies that the kernel that a change has been made that requires a restart.
|
||||
/// </summary>
|
||||
|
@ -1005,129 +1022,15 @@ namespace Emby.Server.Implementations
|
|||
|
||||
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>
|
||||
/// Gets the composable part assemblies.
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{Assembly}.</returns>
|
||||
protected IEnumerable<Assembly> GetComposablePartAssemblies()
|
||||
{
|
||||
if (Directory.Exists(ApplicationPaths.PluginsPath))
|
||||
foreach (var p in _pluginManager.LoadAssemblies())
|
||||
{
|
||||
_pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList();
|
||||
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;
|
||||
}
|
||||
}
|
||||
yield return p;
|
||||
}
|
||||
|
||||
// Include composable parts in the Model assembly
|
||||
|
@ -1230,16 +1133,16 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
||||
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
||||
{
|
||||
// Published server ends with a /
|
||||
if (_startupOptions.PublishedServerUrl != null)
|
||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||
{
|
||||
// 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);
|
||||
|
@ -1256,10 +1159,10 @@ namespace Emby.Server.Implementations
|
|||
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
||||
{
|
||||
// Published server ends with a /
|
||||
if (_startupOptions.PublishedServerUrl != null)
|
||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||
{
|
||||
// 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);
|
||||
|
@ -1276,10 +1179,10 @@ namespace Emby.Server.Implementations
|
|||
public string GetSmartApiUrl(string hostname, int? port = null)
|
||||
{
|
||||
// Published server ends with a /
|
||||
if (_startupOptions.PublishedServerUrl != null)
|
||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||
{
|
||||
// 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);
|
||||
|
@ -1314,14 +1217,14 @@ namespace Emby.Server.Implementations
|
|||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
||||
Host = host,
|
||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
||||
Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||
}.ToString().TrimEnd('/');
|
||||
}
|
||||
|
||||
public string FriendlyName =>
|
||||
string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
|
||||
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
|
||||
? Environment.MachineName
|
||||
: ServerConfigurationManager.Configuration.ServerName;
|
||||
: ConfigurationManager.Configuration.ServerName;
|
||||
|
||||
/// <summary>
|
||||
/// 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()
|
||||
{
|
||||
var assemblies = _allConcreteTypes
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
@ -21,7 +25,6 @@ using MediaBrowser.Model.Dto;
|
|||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
|
@ -44,10 +47,10 @@ namespace Emby.Server.Implementations.Channels
|
|||
private readonly ILogger<ChannelManager> _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 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="fileSystem">The filesystem.</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="memoryCache">The memory cache.</param>
|
||||
public ChannelManager(
|
||||
|
@ -70,7 +72,6 @@ namespace Emby.Server.Implementations.Channels
|
|||
IServerConfigurationManager config,
|
||||
IFileSystem fileSystem,
|
||||
IUserDataManager userDataManager,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IProviderManager providerManager,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
|
@ -81,7 +82,6 @@ namespace Emby.Server.Implementations.Channels
|
|||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
_userDataManager = userDataManager;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_providerManager = providerManager;
|
||||
_memoryCache = memoryCache;
|
||||
}
|
||||
|
@ -337,21 +337,23 @@ namespace Emby.Server.Implementations.Channels
|
|||
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");
|
||||
|
||||
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
|
||||
{
|
||||
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");
|
||||
|
||||
|
@ -370,7 +372,8 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
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 />
|
||||
|
@ -812,7 +815,8 @@ namespace Emby.Server.Implementations.Channels
|
|||
{
|
||||
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)
|
||||
{
|
||||
return null;
|
||||
|
@ -834,7 +838,8 @@ namespace Emby.Server.Implementations.Channels
|
|||
{
|
||||
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)
|
||||
{
|
||||
return null;
|
||||
|
@ -865,7 +870,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
|
||||
}
|
||||
|
||||
CacheResponse(result, cachePath);
|
||||
await CacheResponse(result, cachePath);
|
||||
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -1176,11 +1182,11 @@ namespace Emby.Server.Implementations.Channels
|
|||
{
|
||||
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
|
||||
{
|
||||
SaveMediaSources(item, new List<MediaSourceInfo>());
|
||||
await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveMediaSources(item, info.MediaSources);
|
||||
await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections
|
|||
return null;
|
||||
})
|
||||
.Where(i => i != null)
|
||||
.GroupBy(x => x.Id)
|
||||
.GroupBy(x => x!.Id) // We removed the null values
|
||||
.Select(x => x.First())
|
||||
.ToList();
|
||||
.ToList()!; // Again... the list doesn't contain any null values
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
@ -8,11 +9,9 @@ using System.Threading.Tasks;
|
|||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
@ -107,7 +106,7 @@ namespace Emby.Server.Implementations.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();
|
||||
}
|
||||
|
@ -124,7 +123,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
|
||||
private IEnumerable<BoxSet> GetCollections(User user)
|
||||
{
|
||||
var folder = GetCollectionsFolder(false).Result;
|
||||
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
|
||||
|
||||
return folder == null
|
||||
? Enumerable.Empty<BoxSet>()
|
||||
|
@ -167,7 +166,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
|
||||
parentFolder.AddChild(collection, CancellationToken.None);
|
||||
|
||||
if (options.ItemIdList.Length > 0)
|
||||
if (options.ItemIdList.Count > 0)
|
||||
{
|
||||
await AddToCollectionAsync(
|
||||
collection.Id,
|
||||
|
@ -251,11 +250,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
|
||||
if (fireEvent)
|
||||
{
|
||||
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs
|
||||
{
|
||||
Collection = collection,
|
||||
ItemsChanged = itemList
|
||||
});
|
||||
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -307,11 +302,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
},
|
||||
RefreshPriority.High);
|
||||
|
||||
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
|
||||
{
|
||||
Collection = collection,
|
||||
ItemsChanged = itemList
|
||||
});
|
||||
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -319,11 +310,11 @@ namespace Emby.Server.Implementations.Collections
|
|||
{
|
||||
var results = new Dictionary<Guid, BaseItem>();
|
||||
|
||||
var allBoxsets = GetCollections(user).ToList();
|
||||
var allBoxSets = GetCollections(user).ToList();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!(item is ISupportsBoxSetGrouping))
|
||||
if (item is not ISupportsBoxSetGrouping)
|
||||
{
|
||||
results[item.Id] = item;
|
||||
}
|
||||
|
@ -331,20 +322,44 @@ namespace Emby.Server.Implementations.Collections
|
|||
{
|
||||
var itemId = item.Id;
|
||||
|
||||
var currentBoxSets = allBoxsets
|
||||
.Where(i => i.ContainsLinkedChildByItemId(itemId))
|
||||
.ToList();
|
||||
var itemIsInBoxSet = false;
|
||||
foreach (var boxSet in allBoxSets)
|
||||
{
|
||||
if (!boxSet.ContainsLinkedChildByItemId(itemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentBoxSets.Count > 0)
|
||||
itemIsInBoxSet = true;
|
||||
|
||||
results.TryAdd(boxSet.Id, boxSet);
|
||||
}
|
||||
|
||||
// skip any item that is in a box set
|
||||
if (itemIsInBoxSet)
|
||||
{
|
||||
foreach (var boxset in currentBoxSets)
|
||||
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)
|
||||
{
|
||||
results[boxset.Id] = boxset;
|
||||
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.Globalization;
|
||||
using System.IO;
|
||||
|
@ -88,38 +90,12 @@ namespace Emby.Server.Implementations.Configuration
|
|||
var newConfig = (ServerConfiguration)newConfiguration;
|
||||
|
||||
ValidateMetadataPath(newConfig);
|
||||
ValidateSslCertificate(newConfig);
|
||||
|
||||
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
|
||||
|
||||
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>
|
||||
/// Validates the metadata path.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||
|
||||
namespace Emby.Server.Implementations
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
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(name);
|
||||
columnNames.Add(columnName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user