Merge branch 'master' into bug/authorization-header-issue

This commit is contained in:
Tommaso Stocchi 2021-06-03 17:15:32 +02:00 committed by GitHub
commit 2b232df07f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1151 changed files with 25088 additions and 14099 deletions

View File

@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest" default: "ubuntu-latest"
- name: DotNetSdkVersion - name: DotNetSdkVersion
type: string type: string
default: 5.0.100 default: 5.0.103
jobs: jobs:
- job: CompatibilityCheck - job: CompatibilityCheck

View File

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

View File

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

View File

@ -22,6 +22,12 @@ jobs:
BuildConfiguration: ubuntu.armhf BuildConfiguration: ubuntu.armhf
Linux.amd64: Linux.amd64:
BuildConfiguration: linux.amd64 BuildConfiguration: linux.amd64
Linux.amd64-musl:
BuildConfiguration: linux.amd64-musl
Linux.arm64:
BuildConfiguration: linux.arm64
Linux.armhf:
BuildConfiguration: linux.armhf
Windows.amd64: Windows.amd64:
BuildConfiguration: windows.amd64 BuildConfiguration: windows.amd64
MacOS: MacOS:
@ -154,7 +160,6 @@ jobs:
dependsOn: dependsOn:
- BuildPackage - BuildPackage
- BuildDocker - BuildDocker
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
@ -180,13 +185,14 @@ jobs:
- job: PublishNuget - job: PublishNuget
displayName: 'Publish NuGet packages' displayName: 'Publish NuGet packages'
dependsOn:
- BuildPackage
condition: succeeded('BuildPackage')
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
variables:
- name: JellyfinVersion
value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET 5.0 sdk' displayName: 'Use .NET 5.0 sdk'
@ -198,12 +204,19 @@ jobs:
displayName: 'Build Stable Nuget packages' displayName: 'Build Stable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs: inputs:
command: 'pack' command: 'custom'
packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj' projects: |
versioningScheme: 'off' Jellyfin.Data/Jellyfin.Data.csproj
MediaBrowser.Common/MediaBrowser.Common.csproj
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
custom: 'pack'
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Build Unstable Nuget packages' displayName: 'Build Unstable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs: inputs:
command: 'custom' command: 'custom'
projects: | projects: |
@ -226,7 +239,7 @@ jobs:
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs: inputs:
command: 'push' command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg' packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
nuGetFeedType: 'external' nuGetFeedType: 'external'
publishFeedCredentials: 'NugetOrg' publishFeedCredentials: 'NugetOrg'
allowPackageConflicts: true # This ignores an error if the version already exists allowPackageConflicts: true # This ignores an error if the version already exists

View File

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

View File

@ -6,7 +6,7 @@ variables:
- name: RestoreBuildProjects - name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj' value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion - name: DotNetSdkVersion
value: 5.0.100 value: 5.0.103
pr: pr:
autoCancel: true autoCancel: true
@ -61,6 +61,3 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml - template: azure-pipelines-package.yml
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-api-client.yml

View File

@ -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"

View File

@ -33,7 +33,13 @@ assignees: ''
**Expected behavior** **Expected behavior**
<!-- A clear and concise description of what you expected to happen. --> <!-- A clear and concise description of what you expected to happen. -->
**Logs** **Server Logs**
<!-- Please paste any log errors. -->
**FFmpeg Logs**
<!-- Please paste any log errors. -->
**Browser Console Logs**
<!-- Please paste any log errors. --> <!-- Please paste any log errors. -->
**Screenshots** **Screenshots**

View File

@ -7,3 +7,9 @@ updates:
time: '12:00' time: '12:00'
open-pull-requests-limit: 10 open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
time: '12:00'
open-pull-requests-limit: 10

43
.github/label-commenter-config.yml vendored Normal file
View 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
View 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
View 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

View File

@ -24,7 +24,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: '5.0.100' dotnet-version: '5.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1
with: with:

24
.github/workflows/label-commenter.yml vendored Normal file
View 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
View 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
View 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 }}

View File

@ -17,6 +17,7 @@
- [bugfixin](https://github.com/bugfixin) - [bugfixin](https://github.com/bugfixin)
- [chaosinnovator](https://github.com/chaosinnovator) - [chaosinnovator](https://github.com/chaosinnovator)
- [ckcr4lyf](https://github.com/ckcr4lyf) - [ckcr4lyf](https://github.com/ckcr4lyf)
- [cocool97](https://github.com/cocool97)
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear) - [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
- [crankdoofus](https://github.com/crankdoofus) - [crankdoofus](https://github.com/crankdoofus)
- [crobibero](https://github.com/crobibero) - [crobibero](https://github.com/crobibero)
@ -49,6 +50,7 @@
- [h1nk](https://github.com/h1nk) - [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93) - [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017) - [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga) - [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h) - [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface) - [joshuaboniface](https://github.com/joshuaboniface)
@ -68,6 +70,7 @@
- [marius-luca-87](https://github.com/marius-luca-87) - [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro) - [mark-monteiro](https://github.com/mark-monteiro)
- [Matt07211](https://github.com/Matt07211) - [Matt07211](https://github.com/Matt07211)
- [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00) - [mcarlton00](https://github.com/mcarlton00)
- [mitchfizz05](https://github.com/mitchfizz05) - [mitchfizz05](https://github.com/mitchfizz05)
- [MrTimscampi](https://github.com/MrTimscampi) - [MrTimscampi](https://github.com/MrTimscampi)
@ -80,6 +83,7 @@
- [nvllsvm](https://github.com/nvllsvm) - [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka) - [nyanmisaka](https://github.com/nyanmisaka)
- [OancaAndrei](https://github.com/OancaAndrei) - [OancaAndrei](https://github.com/OancaAndrei)
- [obradovichv](https://github.com/obradovichv)
- [oddstr13](https://github.com/oddstr13) - [oddstr13](https://github.com/oddstr13)
- [orryverducci](https://github.com/orryverducci) - [orryverducci](https://github.com/orryverducci)
- [petermcneil](https://github.com/petermcneil) - [petermcneil](https://github.com/petermcneil)
@ -103,10 +107,11 @@
- [shemanaev](https://github.com/shemanaev) - [shemanaev](https://github.com/shemanaev)
- [skaro13](https://github.com/skaro13) - [skaro13](https://github.com/skaro13)
- [sl1288](https://github.com/sl1288) - [sl1288](https://github.com/sl1288)
- [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004) - [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251) - [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits) - [spookbits](https://github.com/spookbits)
- [ssenart] (https://github.com/ssenart) - [ssenart](https://github.com/ssenart)
- [stanionascu](https://github.com/stanionascu) - [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles) - [stevehayles](https://github.com/stevehayles)
- [SuperSandro2000](https://github.com/SuperSandro2000) - [SuperSandro2000](https://github.com/SuperSandro2000)
@ -141,6 +146,8 @@
- [Pusta](https://github.com/pusta) - [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen) - [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk) - [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
- [peterspenler](https://github.com/peterspenler)
# Emby Contributors # Emby Contributors

View File

@ -1,14 +1,14 @@
ARG DOTNET_VERSION=5.0 ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& yarn install \ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist && mv dist /dist
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

View File

@ -5,12 +5,12 @@
ARG DOTNET_VERSION=5.0 ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& yarn install \ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist && mv dist /dist

View File

@ -5,12 +5,12 @@
ARG DOTNET_VERSION=5.0 ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& yarn install \ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist && mv dist /dist

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
namespace Emby.Dlna.Configuration namespace Emby.Dlna.Configuration

View File

@ -1,4 +1,3 @@
#nullable enable
#pragma warning disable CS1591 #pragma warning disable CS1591
using Emby.Dlna.Configuration; using Emby.Dlna.Configuration;

View File

@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
{ {
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase)) if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
{ {

View File

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

View File

@ -1,5 +1,6 @@
#nullable disable
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -7,7 +8,6 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Xml; using System.Xml;
using Emby.Dlna.Configuration;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
@ -121,7 +121,7 @@ namespace Emby.Dlna.ContentDirectory
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
{ {
if (xmlWriter == null) if (xmlWriter == null)
{ {
@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory
/// <summary> /// <summary>
/// Adds a "XSetBookmark" element to the xml document. /// Adds a "XSetBookmark" element to the xml document.
/// </summary> /// </summary>
/// <param name="sparams">The <see cref="IDictionary"/>.</param> /// <param name="sparams">The method parameters.</param>
private void HandleXSetBookmark(IDictionary<string, string> sparams) private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams)
{ {
var id = sparams["ObjectID"]; var id = sparams["ObjectID"];
@ -305,35 +305,18 @@ namespace Emby.Dlna.ContentDirectory
return builder.ToString(); return builder.ToString();
} }
/// <summary>
/// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist.
/// </summary>
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
/// <param name="key">The key.</param>
/// <param name="defaultValue">The defaultValue.</param>
/// <returns>The <see cref="string"/>.</returns>
public static string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue)
{
if (sparams != null && sparams.TryGetValue(key, out string val))
{
return val;
}
return defaultValue;
}
/// <summary> /// <summary>
/// Builds the "Browse" xml response. /// Builds the "Browse" xml response.
/// </summary> /// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
/// <param name="sparams">The <see cref="IDictionary"/>.</param> /// <param name="sparams">The method parameters.</param>
/// <param name="deviceId">The device Id to use.</param> /// <param name="deviceId">The device Id to use.</param>
private void HandleBrowse(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
{ {
var id = sparams["ObjectID"]; var id = sparams["ObjectID"];
var flag = sparams["BrowseFlag"]; var flag = sparams["BrowseFlag"];
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty)); var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
var provided = 0; var provided = 0;
@ -435,9 +418,9 @@ namespace Emby.Dlna.ContentDirectory
/// Builds the response to the "X_BrowseByLetter request. /// Builds the response to the "X_BrowseByLetter request.
/// </summary> /// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
/// <param name="sparams">The <see cref="IDictionary"/>.</param> /// <param name="sparams">The method parameters.</param>
/// <param name="deviceId">The device id.</param> /// <param name="deviceId">The device id.</param>
private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
{ {
// TODO: Implement this method // TODO: Implement this method
HandleSearch(xmlWriter, sparams, deviceId); HandleSearch(xmlWriter, sparams, deviceId);
@ -447,13 +430,13 @@ namespace Emby.Dlna.ContentDirectory
/// Builds a response to the "Search" request. /// Builds a response to the "Search" request.
/// </summary> /// </summary>
/// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param> /// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param>
/// <param name="sparams">The sparams<see cref="IDictionary"/>.</param> /// <param name="sparams">The method parameters.</param>
/// <param name="deviceId">The deviceId<see cref="string"/>.</param> /// <param name="deviceId">The deviceId<see cref="string"/>.</param>
private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
{ {
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty)); var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty)); var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
// sort example: dc:title, dc:date // sort example: dc:title, dc:date

View File

@ -1,5 +1,4 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1602
namespace Emby.Dlna.ContentDirectory namespace Emby.Dlna.ContentDirectory
{ {

View File

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

View File

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

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -96,6 +98,7 @@ namespace Emby.Dlna.Didl
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
{ {
// If this using are changed to single lines, then write.Flush needs to be appended before the return.
using (var writer = XmlWriter.Create(builder, settings)) using (var writer = XmlWriter.Create(builder, settings))
{ {
// writer.WriteStartDocument(); // writer.WriteStartDocument();
@ -207,7 +210,8 @@ namespace Emby.Dlna.Didl
var targetWidth = streamInfo.TargetWidth; var targetWidth = streamInfo.TargetWidth;
var targetHeight = streamInfo.TargetHeight; var targetHeight = streamInfo.TargetHeight;
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader( var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
_profile,
streamInfo.Container, streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
@ -598,7 +602,8 @@ namespace Emby.Dlna.Didl
? MimeTypes.GetMimeType(filename) ? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType; : mediaProfile.MimeType;
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader( var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
_profile,
streamInfo.Container, streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
targetAudioBitrate, targetAudioBitrate,
@ -973,15 +978,28 @@ namespace Emby.Dlna.Didl
return; return;
} }
var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg"); // TODO: Remove these default values
var albumArtUrlInfo = GetImageUrl(
imageInfo,
_profile.MaxAlbumArtWidth ?? 10000,
_profile.MaxAlbumArtHeight ?? 10000,
"jpg");
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp); writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn); if (!string.IsNullOrEmpty(_profile.AlbumArtPn))
writer.WriteString(albumartUrlInfo.url); {
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
}
writer.WriteString(albumArtUrlInfo.url);
writer.WriteFullEndElement(); writer.WriteFullEndElement();
// TOOD: Remove these default values // TODO: Remove these default values
var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg"); var iconUrlInfo = GetImageUrl(
imageInfo,
_profile.MaxIconWidth ?? 48,
_profile.MaxIconHeight ?? 48,
"jpg");
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url); writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
if (!_profile.EnableAlbumArtInDidl) if (!_profile.EnableAlbumArtInDidl)
@ -1032,8 +1050,7 @@ namespace Emby.Dlna.Didl
var width = albumartUrlInfo.width ?? maxWidth; var width = albumartUrlInfo.width ?? maxWidth;
var height = albumartUrlInfo.height ?? maxHeight; var height = albumartUrlInfo.height ?? maxHeight;
var contentFeatures = new ContentFeatureBuilder(_profile) var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
.BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
writer.WriteAttributeString( writer.WriteAttributeString(
"protocolInfo", "protocolInfo",
@ -1205,8 +1222,7 @@ namespace Emby.Dlna.Didl
if (width.HasValue && height.HasValue) if (width.HasValue && height.HasValue)
{ {
var newSize = DrawingUtils.Resize( var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
width = newSize.Width; width = newSize.Width;
height = newSize.Height; height = newSize.Height;

View File

@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
{ {
public class StringWriterWithEncoding : StringWriter public class StringWriterWithEncoding : StringWriter
{ {
private readonly Encoding _encoding; private readonly Encoding? _encoding;
public StringWriterWithEncoding() public StringWriterWithEncoding()
{ {

View File

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

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -7,12 +9,14 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Profiles; using Emby.Dlna.Profiles;
using Emby.Dlna.Server; using Emby.Dlna.Server;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
@ -32,9 +36,9 @@ namespace Emby.Dlna
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILogger<DlnaManager> _logger; private readonly ILogger<DlnaManager> _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal); private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
@ -43,14 +47,12 @@ namespace Emby.Dlna
IFileSystem fileSystem, IFileSystem fileSystem,
IApplicationPaths appPaths, IApplicationPaths appPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IJsonSerializer jsonSerializer,
IServerApplicationHost appHost) IServerApplicationHost appHost)
{ {
_xmlSerializer = xmlSerializer; _xmlSerializer = xmlSerializer;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_appPaths = appPaths; _appPaths = appPaths;
_logger = loggerFactory.CreateLogger<DlnaManager>(); _logger = loggerFactory.CreateLogger<DlnaManager>();
_jsonSerializer = jsonSerializer;
_appHost = appHost; _appHost = appHost;
} }
@ -111,7 +113,7 @@ namespace Emby.Dlna
if (profile != null) if (profile != null)
{ {
_logger.LogDebug("Found matching device profile: {0}", profile.Name); _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
} }
else else
{ {
@ -126,92 +128,57 @@ namespace Emby.Dlna
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used."); builder.AppendLine("No matching device profile found. The default will need to be used.");
builder.Append("FriendlyName:").AppendLine(profile.FriendlyName); builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
builder.Append("Manufacturer:").AppendLine(profile.Manufacturer); builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl); builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
builder.Append("ModelDescription:").AppendLine(profile.ModelDescription); builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
builder.Append("ModelName:").AppendLine(profile.ModelName); builder.Append("ModelName: ").AppendLine(profile.ModelName);
builder.Append("ModelNumber:").AppendLine(profile.ModelNumber); builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
builder.Append("ModelUrl:").AppendLine(profile.ModelUrl); builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
builder.Append("SerialNumber:").AppendLine(profile.SerialNumber); builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString()); _logger.LogInformation(builder.ToString());
} }
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo) /// <summary>
/// Attempts to match a device with a profile.
/// Rules:
/// - If the profile field has no value, the field matches irregardless of its contents.
/// - the profile field can be an exact match, or a reg exp.
/// </summary>
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
/// <returns><b>True</b> if they match.</returns>
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
{ {
if (!string.IsNullOrEmpty(profileInfo.FriendlyName)) return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
{ && IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)) && IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
{ && IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
return false; && IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
} && IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
} && IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
{
if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
{
if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
{
if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelName))
{
if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
{
if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
{
if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
{
if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
{
return false;
}
}
return true;
} }
private bool IsRegexOrSubstringMatch(string input, string pattern) private bool IsRegexOrSubstringMatch(string input, string pattern)
{ {
if (string.IsNullOrEmpty(pattern))
{
// In profile identification: An empty pattern matches anything.
return true;
}
if (string.IsNullOrEmpty(input))
{
// The profile contains a value, and the device doesn't.
return false;
}
try try
{ {
return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
@ -333,7 +300,12 @@ namespace Emby.Dlna
throw new ArgumentNullException(nameof(id)); throw new ArgumentNullException(nameof(id));
} }
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
if (info == null)
{
return null;
}
return ParseProfileFile(info.Path, info.Info.Type); return ParseProfileFile(info.Path, info.Info.Type);
} }
@ -395,7 +367,8 @@ namespace Emby.Dlna
{ {
Directory.CreateDirectory(systemProfilesPath); Directory.CreateDirectory(systemProfilesPath);
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{ {
await stream.CopyToAsync(fileStream).ConfigureAwait(false); await stream.CopyToAsync(fileStream).ConfigureAwait(false);
} }
@ -495,9 +468,9 @@ namespace Emby.Dlna
return profile; return profile;
} }
var json = _jsonSerializer.SerializeToString(profile); var json = JsonSerializer.Serialize(profile, _jsonOptions);
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json); return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
} }
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
@ -553,7 +526,7 @@ namespace Emby.Dlna
private void DumpProfiles() private void DumpProfiles()
{ {
DeviceProfile[] list = new [] DeviceProfile[] list = new[]
{ {
new SamsungSmartTvProfile(), new SamsungSmartTvProfile(),
new XboxOneProfile(), new XboxOneProfile(),

View File

@ -21,11 +21,11 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@ -78,9 +78,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -5,10 +7,10 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.PlayTo; using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager; using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@ -52,6 +54,8 @@ namespace Emby.Dlna.Main
private readonly ISocketFactory _socketFactory; private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object(); private readonly object _syncLock = new object();
private readonly NetworkConfiguration _netConfig;
private readonly bool _disabled;
private PlayToManager _manager; private PlayToManager _manager;
private SsdpDevicePublisher _publisher; private SsdpDevicePublisher _publisher;
@ -122,10 +126,23 @@ namespace Emby.Dlna.Main
httpClientFactory, httpClientFactory,
config); config);
Current = this; Current = this;
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
} }
public static DlnaEntryPoint Current { get; private set; } public static DlnaEntryPoint Current { get; private set; }
/// <summary>
/// Gets a value indicating whether the dlna server is enabled.
/// </summary>
public static bool Enabled { get; private set; }
public IContentDirectory ContentDirectory { get; private set; } public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; } public IConnectionManager ConnectionManager { get; private set; }
@ -136,6 +153,12 @@ namespace Emby.Dlna.Main
{ {
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
if (_disabled)
{
// No use starting as dlna won't work, as we're running purely on HTTPS.
return;
}
ReloadComponents(); ReloadComponents();
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
@ -152,6 +175,7 @@ namespace Emby.Dlna.Main
private void ReloadComponents() private void ReloadComponents()
{ {
var options = _config.GetDlnaConfiguration(); var options = _config.GetDlnaConfiguration();
Enabled = options.EnableServer;
StartSsdpHandler(); StartSsdpHandler();
@ -206,7 +230,10 @@ namespace Emby.Dlna.Main
{ {
try try
{ {
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer); if (communicationsServer != null)
{
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -290,12 +317,18 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri); var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
{
// DLNA will only work over http, so we must reset to http:// : {port}.
uri.Scheme = "http";
uri.Port = _netConfig.HttpServerPortNumber;
}
var device = new SsdpRootDevice var device = new SsdpRootDevice
{ {
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
Location = uri, // Must point to the URL that serves your devices UPnP description document. Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
Address = address.Address, Address = address.Address,
PrefixLength = address.PrefixLength, PrefixLength = address.PrefixLength,
FriendlyName = "Jellyfin", FriendlyName = "Jellyfin",

View File

@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
{ {
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
{ {

View File

@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
{ {

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -219,7 +221,7 @@ namespace Emby.Dlna.PlayTo
{ {
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
if (command == null) if (command == null)
{ {
return false; return false;
@ -235,7 +237,13 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("Setting mute"); _logger.LogDebug("Setting mute");
var value = mute ? 1 : 0; var value = mute ? 1 : 0;
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) await new SsdpHttpClient(_httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
rendererCommands.BuildPost(command, service.ServiceType, value),
cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
IsMuted = mute; IsMuted = mute;
@ -253,7 +261,7 @@ namespace Emby.Dlna.PlayTo
{ {
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
if (command == null) if (command == null)
{ {
return; return;
@ -270,7 +278,13 @@ namespace Emby.Dlna.PlayTo
// Remote control will perform better // Remote control will perform better
Volume = value; Volume = value;
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value)) await new SsdpHttpClient(_httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
rendererCommands.BuildPost(command, service.ServiceType, value),
cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@ -278,7 +292,7 @@ namespace Emby.Dlna.PlayTo
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
if (command == null) if (command == null)
{ {
return; return;
@ -291,7 +305,13 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service"); throw new InvalidOperationException("Unable to find service");
} }
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) await new SsdpHttpClient(_httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
RestartTimer(true); RestartTimer(true);
@ -305,7 +325,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header); _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
if (command == null) if (command == null)
{ {
return; return;
@ -325,14 +345,21 @@ namespace Emby.Dlna.PlayTo
} }
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header) await new SsdpHttpClient(_httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
post,
header: header,
cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
await Task.Delay(50).ConfigureAwait(false); await Task.Delay(50, cancellationToken).ConfigureAwait(false);
try try
{ {
await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false); await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
} }
catch catch
{ {
@ -343,6 +370,42 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true); RestartTimer(true);
} }
/*
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
* Without that information, the next track command on the device does not work.
*/
public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
url = url.Replace("&", "&amp;", StringComparison.Ordinal);
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
if (command == null)
{
return;
}
var dictionary = new Dictionary<string, string>
{
{ "NextURI", url },
{ "NextURIMetaData", CreateDidlMeta(metaData) }
};
var service = GetAvTransportService();
if (service == null)
{
throw new InvalidOperationException("Unable to find service");
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
.ConfigureAwait(false);
}
private static string CreateDidlMeta(string value) private static string CreateDidlMeta(string value)
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
@ -378,6 +441,10 @@ namespace Emby.Dlna.PlayTo
public async Task SetPlay(CancellationToken cancellationToken) public async Task SetPlay(CancellationToken cancellationToken)
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
if (avCommands == null)
{
return;
}
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
@ -388,7 +455,7 @@ namespace Emby.Dlna.PlayTo
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
if (command == null) if (command == null)
{ {
return; return;
@ -396,7 +463,13 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService(); var service = GetAvTransportService();
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) await new SsdpHttpClient(_httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
avCommands.BuildPost(command, service.ServiceType, 1),
cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
RestartTimer(true); RestartTimer(true);
@ -406,7 +479,7 @@ namespace Emby.Dlna.PlayTo
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
if (command == null) if (command == null)
{ {
return; return;
@ -414,7 +487,13 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService(); var service = GetAvTransportService();
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) await new SsdpHttpClient(_httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
avCommands.BuildPost(command, service.ServiceType, 1),
cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
TransportState = TransportState.Paused; TransportState = TransportState.Paused;
@ -528,7 +607,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
if (command == null) if (command == null)
{ {
return; return;
@ -578,7 +657,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
if (command == null) if (command == null)
{ {
return; return;
@ -665,6 +744,10 @@ namespace Emby.Dlna.PlayTo
} }
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
if (rendererCommands == null)
{
return null;
}
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
@ -733,6 +816,11 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
if (rendererCommands == null)
{
return (false, null);
}
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
@ -914,6 +1002,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory); var httpClient = new SsdpHttpClient(_httpClientFactory);
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
AvCommands = TransportCommands.Create(document); AvCommands = TransportCommands.Create(document);
return AvCommands; return AvCommands;
@ -942,6 +1034,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory); var httpClient = new SsdpHttpClient(_httpClientFactory);
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
RendererCommands = TransportCommands.Create(document); RendererCommands = TransportCommands.Create(document);
return RendererCommands; return RendererCommands;
@ -973,6 +1069,10 @@ namespace Emby.Dlna.PlayTo
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory); var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
var friendlyNames = new List<string>(); var friendlyNames = new List<string>();
@ -990,7 +1090,7 @@ namespace Emby.Dlna.PlayTo
var deviceProperties = new DeviceInfo() var deviceProperties = new DeviceInfo()
{ {
Name = string.Join(" ", friendlyNames), Name = string.Join(' ', friendlyNames),
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port) BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
}; };

View File

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

View File

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

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -102,6 +104,22 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft; _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
} }
/*
* Send a message to the DLNA device to notify what is the next track in the playlist.
*/
private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
{
if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
{
// The current playing item is indeed in the play list and we are not yet at the end of the playlist.
var nextItemIndex = currentPlayListItemIndex + 1;
var nextItem = _playlist[nextItemIndex];
// Send the SetNextAvTransport message.
await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
}
}
private void OnDeviceUnavailable() private void OnDeviceUnavailable()
{ {
try try
@ -132,7 +150,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e) private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{ {
if (_disposed) if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{ {
return; return;
} }
@ -156,6 +174,15 @@ namespace Emby.Dlna.PlayTo
var newItemProgress = GetProgressInfo(streamInfo); var newItemProgress = GetProgressInfo(streamInfo);
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false); await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
// Send a message to the DLNA device to notify what is the next track in the playlist.
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
if (currentItemIndex >= 0)
{
_currentPlaylistIndex = currentItemIndex;
}
await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -425,6 +452,11 @@ namespace Emby.Dlna.PlayTo
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex); var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
// Send a message to the DLNA device to notify what is the next track in the play list.
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
return; return;
} }
@ -499,8 +531,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Audio) if (streamInfo.MediaType == DlnaProfileType.Audio)
{ {
return new ContentFeatureBuilder(profile) return ContentFeatureBuilder.BuildAudioHeader(
.BuildAudioHeader( profile,
streamInfo.Container, streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate, streamInfo.TargetAudioBitrate,
@ -514,8 +546,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Video) if (streamInfo.MediaType == DlnaProfileType.Video)
{ {
var list = new ContentFeatureBuilder(profile) var list = ContentFeatureBuilder.BuildVideoHeader(
.BuildVideoHeader( profile,
streamInfo.Container, streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
@ -623,6 +655,9 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false); await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
// Send a message to the DLNA device to notify what is the next track in the play list.
await SendNextTrackMessage(index, cancellationToken);
var streamInfo = currentitem.StreamInfo; var streamInfo = currentitem.StreamInfo;
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo)) if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
{ {
@ -736,6 +771,10 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
// Send a message to the DLNA device to notify what is the next track in the play list.
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
if (EnableClientSideSeek(newItem.StreamInfo)) if (EnableClientSideSeek(newItem.StreamInfo))
{ {
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
@ -761,6 +800,10 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
// Send a message to the DLNA device to notify what is the next track in the play list.
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0) if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
{ {
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
@ -777,7 +820,7 @@ namespace Emby.Dlna.PlayTo
var currentWait = 0; var currentWait = 0;
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait) while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
{ {
await Task.Delay(Interval).ConfigureAwait(false); await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
currentWait += Interval; currentWait += Interval;
} }
@ -826,7 +869,7 @@ namespace Emby.Dlna.PlayTo
return SendPlayCommand(data as PlayRequest, cancellationToken); return SendPlayCommand(data as PlayRequest, cancellationToken);
} }
if (name == SessionMessageType.PlayState) if (name == SessionMessageType.Playstate)
{ {
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
} }
@ -896,16 +939,16 @@ namespace Emby.Dlna.PlayTo
var parts = url.Split('/'); var parts = url.Split('/');
for (var i = 0; i < parts.Length; i++) for (var i = 0; i < parts.Length - 1; i++)
{ {
var part = parts[i]; var part = parts[i];
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) || if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase)) string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
{ {
if (parts.Length > i + 1) if (Guid.TryParse(parts[i + 1], out var result))
{ {
return Guid.Parse(parts[i + 1]); return result;
} }
} }
} }
@ -943,11 +986,7 @@ namespace Emby.Dlna.PlayTo
request.DeviceId = values.GetValueOrDefault("DeviceId"); request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId"); request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId"); request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
// Be careful, IsDirectStream==true by default (Static != false or not in query).
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks"); request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -178,12 +180,17 @@ namespace Emby.Dlna.PlayTo
if (controller == null) if (controller == null)
{ {
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false); var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
if (device == null)
{
_logger.LogError("Ignoring device as xml response is invalid.");
return;
}
string deviceName = device.Properties.Name; string deviceName = device.Properties.Name;
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress); string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
controller = new PlayToController( controller = new PlayToController(
sessionInfo, sessionInfo,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
@ -45,10 +46,10 @@ namespace Emby.Dlna.PlayTo
cancellationToken) cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8); return await XDocument.LoadAsync(
return XDocument.Parse( stream,
await reader.ReadToEndAsync().ConfigureAwait(false), LoadOptions.PreserveWhitespace,
LoadOptions.PreserveWhitespace); cancellationToken).ConfigureAwait(false);
} }
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@ -94,10 +95,17 @@ namespace Emby.Dlna.PlayTo
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8); try
return XDocument.Parse( {
await reader.ReadToEndAsync().ConfigureAwait(false), return await XDocument.LoadAsync(
LoadOptions.PreserveWhitespace); stream,
LoadOptions.PreserveWhitespace,
cancellationToken).ConfigureAwait(false);
}
catch
{
return null;
}
} }
private async Task<HttpResponseMessage> PostSoapDataAsync( private async Task<HttpResponseMessage> PostSoapDataAsync(

View File

@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
public class TransportCommands public class TransportCommands
{ {
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>"; private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
private List<StateVariable> _stateVariables = new List<StateVariable>();
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
public List<StateVariable> StateVariables => _stateVariables; public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
public List<ServiceAction> ServiceActions => _serviceActions; public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
public static TransportCommands Create(XDocument document) public static TransportCommands Create(XDocument document)
{ {
@ -48,7 +46,7 @@ namespace Emby.Dlna.PlayTo
{ {
var serviceAction = new ServiceAction var serviceAction = new ServiceAction
{ {
Name = container.GetValue(UPnpNamespaces.Svc + "name"), Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
}; };
var argumentList = serviceAction.ArgumentList; var argumentList = serviceAction.ArgumentList;
@ -70,9 +68,9 @@ namespace Emby.Dlna.PlayTo
return new Argument return new Argument
{ {
Name = container.GetValue(UPnpNamespaces.Svc + "name"), Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"), Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
}; };
} }
@ -91,8 +89,8 @@ namespace Emby.Dlna.PlayTo
return new StateVariable return new StateVariable
{ {
Name = container.GetValue(UPnpNamespaces.Svc + "name"), Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"), DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
AllowedValues = allowedValues AllowedValues = allowedValues
}; };
} }
@ -168,7 +166,7 @@ namespace Emby.Dlna.PlayTo
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString); return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
} }
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "") private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
{ {
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase)); var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));

View File

@ -1,5 +1,4 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1602
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
{ {

View File

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

View File

@ -1,5 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using System.Globalization;
using System.Linq; using System.Linq;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
{ {
public DefaultProfile() public DefaultProfile()
{ {
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
Name = "Generic Device"; Name = "Generic Device";
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*"; ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";

View File

@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
Identification = new DeviceIdentification Identification = new DeviceIdentification
{ {
FriendlyName = @"KDL-\d{2}[EHLNPB]X\d[01]\d.*", FriendlyName = @"KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*",
Manufacturer = "Sony", Manufacturer = "Sony",
Headers = new[] Headers = new[]
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
new HttpHeaderInfo new HttpHeaderInfo
{ {
Name = "X-AV-Client-Info", Name = "X-AV-Client-Info",
Value = @".*KDL-\d{2}[EHLNPB]X\d[01]\d.*", Value = @".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*",
Match = HeaderMatchType.Regex Match = HeaderMatchType.Regex
} }
} }

View File

@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
Identification = new DeviceIdentification Identification = new DeviceIdentification
{ {
FriendlyName = @"KDL-\d{2}([A-Z]X\d2\d|CX400).*", FriendlyName = @"KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*",
Manufacturer = "Sony", Manufacturer = "Sony",
Headers = new[] Headers = new[]
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
new HttpHeaderInfo new HttpHeaderInfo
{ {
Name = "X-AV-Client-Info", Name = "X-AV-Client-Info",
Value = @".*KDL-\d{2}([A-Z]X\d2\d|CX400).*", Value = @".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*",
Match = HeaderMatchType.Regex Match = HeaderMatchType.Regex
} }
} }

View File

@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
Identification = new DeviceIdentification Identification = new DeviceIdentification
{ {
FriendlyName = @"KDL-\d{2}[A-Z]X\d5(\d|G).*", FriendlyName = @"KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*",
Manufacturer = "Sony", Manufacturer = "Sony",
Headers = new[] Headers = new[]
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
new HttpHeaderInfo new HttpHeaderInfo
{ {
Name = "X-AV-Client-Info", Name = "X-AV-Client-Info",
Value = @".*KDL-\d{2}[A-Z]X\d5(\d|G).*", Value = @".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*",
Match = HeaderMatchType.Regex Match = HeaderMatchType.Regex
} }
} }

View File

@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
Identification = new DeviceIdentification Identification = new DeviceIdentification
{ {
FriendlyName = @"KDL-\d{2}[WR][5689]\d{2}A.*", FriendlyName = @"KDL-[0-9]{2}[WR][5689][0-9]{2}A.*",
Manufacturer = "Sony", Manufacturer = "Sony",
Headers = new[] Headers = new[]
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
new HttpHeaderInfo new HttpHeaderInfo
{ {
Name = "X-AV-Client-Info", Name = "X-AV-Client-Info",
Value = @".*KDL-\d{2}[WR][5689]\d{2}A.*", Value = @".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*",
Match = HeaderMatchType.Regex Match = HeaderMatchType.Regex
} }
} }

View File

@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
Identification = new DeviceIdentification Identification = new DeviceIdentification
{ {
FriendlyName = @"(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*", FriendlyName = @"(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*",
Manufacturer = "Sony", Manufacturer = "Sony",
Headers = new[] Headers = new[]
@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
new HttpHeaderInfo new HttpHeaderInfo
{ {
Name = "X-AV-Client-Info", Name = "X-AV-Client-Info",
Value = @".*(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*", Value = @".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*",
Match = HeaderMatchType.Regex Match = HeaderMatchType.Regex
} }
} }

View File

@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts", Container = "ts,mpegts",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264", VideoCodec = "mpeg1video,mpeg2video,h264",
AudioCodec = "ac3,mp2,mp3,aac" AudioCodec = "aac,ac3,mp2"
}, },
new DirectPlayProfile new DirectPlayProfile
{ {
@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles
{ {
Container = "ts", Container = "ts",
VideoCodec = "h264", VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3", AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new TranscodingProfile new TranscodingProfile

View File

@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts", Container = "ts,mpegts",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264", VideoCodec = "mpeg1video,mpeg2video,h264",
AudioCodec = "ac3,mp2,mp3,aac" AudioCodec = "aac,ac3,mp2"
}, },
new DirectPlayProfile new DirectPlayProfile
{ {
@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles
{ {
Container = "ts", Container = "ts",
VideoCodec = "h264", VideoCodec = "h264",
AudioCodec = "mp3", AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new TranscodingProfile new TranscodingProfile

View File

@ -3,10 +3,10 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema"> xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Sony Bravia (2010)</Name> <Name>Sony Bravia (2010)</Name>
<Identification> <Identification>
<FriendlyName>KDL-\d{2}[EHLNPB]X\d[01]\d.*</FriendlyName> <FriendlyName>KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*</FriendlyName>
<Manufacturer>Sony</Manufacturer> <Manufacturer>Sony</Manufacturer>
<Headers> <Headers>
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[EHLNPB]X\d[01]\d.*" match="Regex" /> <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*" match="Regex" />
</Headers> </Headers>
</Identification> </Identification>
<Manufacturer>Microsoft Corporation</Manufacturer> <Manufacturer>Microsoft Corporation</Manufacturer>

View File

@ -3,10 +3,10 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema"> xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Sony Bravia (2011)</Name> <Name>Sony Bravia (2011)</Name>
<Identification> <Identification>
<FriendlyName>KDL-\d{2}([A-Z]X\d2\d|CX400).*</FriendlyName> <FriendlyName>KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*</FriendlyName>
<Manufacturer>Sony</Manufacturer> <Manufacturer>Sony</Manufacturer>
<Headers> <Headers>
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}([A-Z]X\d2\d|CX400).*" match="Regex" /> <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*" match="Regex" />
</Headers> </Headers>
</Identification> </Identification>
<Manufacturer>Microsoft Corporation</Manufacturer> <Manufacturer>Microsoft Corporation</Manufacturer>

View File

@ -3,10 +3,10 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema"> xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Sony Bravia (2012)</Name> <Name>Sony Bravia (2012)</Name>
<Identification> <Identification>
<FriendlyName>KDL-\d{2}[A-Z]X\d5(\d|G).*</FriendlyName> <FriendlyName>KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*</FriendlyName>
<Manufacturer>Sony</Manufacturer> <Manufacturer>Sony</Manufacturer>
<Headers> <Headers>
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[A-Z]X\d5(\d|G).*" match="Regex" /> <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*" match="Regex" />
</Headers> </Headers>
</Identification> </Identification>
<Manufacturer>Microsoft Corporation</Manufacturer> <Manufacturer>Microsoft Corporation</Manufacturer>

View File

@ -3,10 +3,10 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema"> xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Sony Bravia (2013)</Name> <Name>Sony Bravia (2013)</Name>
<Identification> <Identification>
<FriendlyName>KDL-\d{2}[WR][5689]\d{2}A.*</FriendlyName> <FriendlyName>KDL-[0-9]{2}[WR][5689][0-9]{2}A.*</FriendlyName>
<Manufacturer>Sony</Manufacturer> <Manufacturer>Sony</Manufacturer>
<Headers> <Headers>
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[WR][5689]\d{2}A.*" match="Regex" /> <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*" match="Regex" />
</Headers> </Headers>
</Identification> </Identification>
<Manufacturer>Microsoft Corporation</Manufacturer> <Manufacturer>Microsoft Corporation</Manufacturer>

View File

@ -3,10 +3,10 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema"> xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Sony Bravia (2014)</Name> <Name>Sony Bravia (2014)</Name>
<Identification> <Identification>
<FriendlyName>(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*</FriendlyName> <FriendlyName>(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*</FriendlyName>
<Manufacturer>Sony</Manufacturer> <Manufacturer>Sony</Manufacturer>
<Headers> <Headers>
<HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*" match="Regex" /> <HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*" match="Regex" />
</Headers> </Headers>
</Identification> </Identification>
<Manufacturer>Microsoft Corporation</Manufacturer> <Manufacturer>Microsoft Corporation</Manufacturer>

View File

@ -38,7 +38,7 @@
<XmlRootAttributes /> <XmlRootAttributes />
<DirectPlayProfiles> <DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
<DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> <DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
<DirectPlayProfile container="aac,mp3,wav" type="Audio" /> <DirectPlayProfile container="aac,mp3,wav" type="Audio" />
@ -46,7 +46,7 @@
</DirectPlayProfiles> </DirectPlayProfiles>
<TranscodingProfiles> <TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles> </TranscodingProfiles>
<ContainerProfiles> <ContainerProfiles>

View File

@ -38,7 +38,7 @@
<XmlRootAttributes /> <XmlRootAttributes />
<DirectPlayProfiles> <DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
<DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> <DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
<DirectPlayProfile container="aac,mp3,wav" type="Audio" /> <DirectPlayProfile container="aac,mp3,wav" type="Audio" />
@ -46,7 +46,7 @@
</DirectPlayProfiles> </DirectPlayProfiles>
<TranscodingProfiles> <TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles> </TranscodingProfiles>
<ContainerProfiles> <ContainerProfiles>

View File

@ -250,7 +250,8 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/'); url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
return SecurityElement.Escape(url); // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
return SecurityElement.Escape(url) ?? string.Empty;
} }
private IEnumerable<DeviceIcon> GetIcons() private IEnumerable<DeviceIcon> GetIcons()

View File

@ -47,9 +47,9 @@ namespace Emby.Dlna.Service
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request) private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
{ {
ControlRequestInfo requestInfo = null; ControlRequestInfo? requestInfo = null;
using (var streamReader = new StreamReader(request.InputXml)) using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
{ {
var readerSettings = new XmlReaderSettings() var readerSettings = new XmlReaderSettings()
{ {
@ -151,7 +151,7 @@ namespace Emby.Dlna.Service
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader) private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
{ {
string namespaceURI = null, localName = null; string? namespaceURI = null, localName = null;
await reader.MoveToContentAsync().ConfigureAwait(false); await reader.MoveToContentAsync().ConfigureAwait(false);
await reader.ReadAsync().ConfigureAwait(false); await reader.ReadAsync().ConfigureAwait(false);
@ -210,7 +210,7 @@ namespace Emby.Dlna.Service
} }
} }
protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter); protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter);
private void LogRequest(ControlRequest request) private void LogRequest(ControlRequest request)
{ {

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -69,7 +71,7 @@ namespace Emby.Dlna.Ssdp
{ {
lock (_syncLock) lock (_syncLock)
{ {
if (_listenerCount > 0 && _deviceLocator == null) if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
{ {
_deviceLocator = new SsdpDeviceLocator(_commsServer); _deviceLocator = new SsdpDeviceLocator(_commsServer);
@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp
{ {
Location = e.DiscoveredDevice.DescriptionLocation, Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers, Headers = headers,
LocalIpAddress = e.LocalIpAddress RemoteIpAddress = e.RemoteIpAddress
}); });
DeviceDiscoveredInternal?.Invoke(this, args); DeviceDiscoveredInternal?.Invoke(this, args);

View File

@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp
{ {
public static class SsdpExtensions public static class SsdpExtensions
{ {
public static string GetValue(this XElement container, XName name) public static string? GetValue(this XElement container, XName name)
{ {
var node = container.Element(name); var node = container.Element(name);
return node?.Value; return node?.Value;
} }
public static string GetAttributeValue(this XElement container, XName name) public static string? GetAttributeValue(this XElement container, XName name)
{ {
var node = container.Attribute(name); var node = container.Attribute(name);
return node?.Value; return node?.Value;
} }
public static string GetDescendantValue(this XElement container, XName name) public static string? GetDescendantValue(this XElement container, XName name)
=> container.Descendants(name).FirstOrDefault()?.Value; => container.Descendants(name).FirstOrDefault()?.Value;
} }
} }

View File

@ -25,7 +25,6 @@
<!-- Code analysers--> <!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@ -171,21 +172,31 @@ namespace Emby.Drawing
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
} }
ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
int quality = options.Quality; int quality = options.Quality;
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency); ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer); string cacheFilePath = GetCacheFilePath(
originalImagePath,
options.Width,
options.Height,
options.MaxWidth,
options.MaxHeight,
options.FillWidth,
options.FillHeight,
quality,
dateModified,
outputFormat,
options.AddPlayedIndicator,
options.PercentPlayed,
options.UnplayedCount,
options.Blur,
options.BackgroundColor,
options.ForegroundLayer);
try try
{ {
if (!File.Exists(cacheFilePath)) if (!File.Exists(cacheFilePath))
{ {
if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
{
options.CropWhiteSpace = false;
}
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat); string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase)) if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
@ -246,48 +257,111 @@ namespace Emby.Drawing
/// <summary> /// <summary>
/// Gets the cache file path based on a set of parameters. /// Gets the cache file path based on a set of parameters.
/// </summary> /// </summary>
private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer) private string GetCacheFilePath(
string originalPath,
int? width,
int? height,
int? maxWidth,
int? maxHeight,
int? fillWidth,
int? fillHeight,
int quality,
DateTime dateModified,
ImageFormat format,
bool addPlayedIndicator,
double percentPlayed,
int? unwatchedCount,
int? blur,
string backgroundColor,
string foregroundLayer)
{ {
var filename = originalPath var filename = new StringBuilder(256);
+ "width=" + outputSize.Width filename.Append(originalPath);
+ "height=" + outputSize.Height
+ "quality=" + quality filename.Append(",quality=");
+ "datemodified=" + dateModified.Ticks filename.Append(quality);
+ "f=" + format;
filename.Append(",datemodified=");
filename.Append(dateModified.Ticks);
filename.Append(",f=");
filename.Append(format);
if (width.HasValue)
{
filename.Append(",width=");
filename.Append(width.Value);
}
if (height.HasValue)
{
filename.Append(",height=");
filename.Append(height.Value);
}
if (maxWidth.HasValue)
{
filename.Append(",maxwidth=");
filename.Append(maxWidth.Value);
}
if (maxHeight.HasValue)
{
filename.Append(",maxheight=");
filename.Append(maxHeight.Value);
}
if (fillWidth.HasValue)
{
filename.Append(",fillwidth=");
filename.Append(fillWidth.Value);
}
if (fillHeight.HasValue)
{
filename.Append(",fillheight=");
filename.Append(fillHeight.Value);
}
if (addPlayedIndicator) if (addPlayedIndicator)
{ {
filename += "pl=true"; filename.Append(",pl=true");
} }
if (percentPlayed > 0) if (percentPlayed > 0)
{ {
filename += "p=" + percentPlayed; filename.Append(",p=");
filename.Append(percentPlayed);
} }
if (unwatchedCount.HasValue) if (unwatchedCount.HasValue)
{ {
filename += "p=" + unwatchedCount.Value; filename.Append(",p=");
filename.Append(unwatchedCount.Value);
} }
if (blur.HasValue) if (blur.HasValue)
{ {
filename += "blur=" + blur.Value; filename.Append(",blur=");
filename.Append(blur.Value);
} }
if (!string.IsNullOrEmpty(backgroundColor)) if (!string.IsNullOrEmpty(backgroundColor))
{ {
filename += "b=" + backgroundColor; filename.Append(",b=");
filename.Append(backgroundColor);
} }
if (!string.IsNullOrEmpty(foregroundLayer)) if (!string.IsNullOrEmpty(foregroundLayer))
{ {
filename += "fl=" + foregroundLayer; filename.Append(",fl=");
filename.Append(foregroundLayer);
} }
filename += "v=" + Version; filename.Append(",v=");
filename.Append(Version);
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant()); return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
} }
/// <inheritdoc /> /// <inheritdoc />
@ -352,8 +426,13 @@ namespace Emby.Drawing
} }
/// <inheritdoc /> /// <inheritdoc />
public string GetImageCacheTag(User user) public string? GetImageCacheTag(User user)
{ {
if (user.ProfileImage == null)
{
return null;
}
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5() return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
.ToString("N", CultureInfo.InvariantCulture); .ToString("N", CultureInfo.InvariantCulture);
} }

View File

@ -32,7 +32,7 @@ namespace Emby.Drawing
=> throw new NotImplementedException(); => throw new NotImplementedException();
/// <inheritdoc /> /// <inheritdoc />
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -1,7 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using Emby.Naming.Common; using Emby.Naming.Common;
using MediaBrowser.Common.Extensions;
namespace Emby.Naming.Audio namespace Emby.Naming.Audio
{ {
@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
/// <returns>True if file at path is audio file.</returns> /// <returns>True if file at path is audio file.</returns>
public static bool IsAudioFile(string path, NamingOptions options) public static bool IsAudioFile(string path, NamingOptions options)
{ {
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path.AsSpan());
return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
} }
} }
} }

View File

@ -15,13 +15,13 @@ namespace Emby.Naming.AudioBook
/// <param name="files">List of files composing the actual audiobook.</param> /// <param name="files">List of files composing the actual audiobook.</param>
/// <param name="extras">List of extra files.</param> /// <param name="extras">List of extra files.</param>
/// <param name="alternateVersions">Alternative version of files.</param> /// <param name="alternateVersions">Alternative version of files.</param>
public AudioBookInfo(string name, int? year, List<AudioBookFileInfo>? files, List<AudioBookFileInfo>? extras, List<AudioBookFileInfo>? alternateVersions) public AudioBookInfo(string name, int? year, List<AudioBookFileInfo> files, List<AudioBookFileInfo> extras, List<AudioBookFileInfo> alternateVersions)
{ {
Name = name; Name = name;
Year = year; Year = year;
Files = files ?? new List<AudioBookFileInfo>(); Files = files;
Extras = extras ?? new List<AudioBookFileInfo>(); Extras = extras;
AlternateVersions = alternateVersions ?? new List<AudioBookFileInfo>(); AlternateVersions = alternateVersions;
} }
/// <summary> /// <summary>

View File

@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null); var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber }); var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
var nameWithReplacedDots = nameParserResult.Name.Replace(" ", "."); var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.');
foreach (var group in groupedBy) foreach (var group in groupedBy)
{ {

View File

@ -282,7 +282,13 @@ namespace Emby.Naming.Common
SupportsAbsoluteEpisodeNumbers = true SupportsAbsoluteEpisodeNumbers = true
}, },
// Case Closed (1996-2007)/Case Closed - 317.mkv // Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
// [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
{
IsNamed = true
},
// /server/anything_102.mp4 // /server/anything_102.mp4
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv // /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
// /server/anything_1996.11.14.mp4 // /server/anything_1996.11.14.mp4
@ -299,11 +305,6 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming // *** End Kodi Standard Naming
// [bar] Foo - 1 [baz]
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
{
IsNamed = true
},
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
@ -587,7 +588,7 @@ namespace Emby.Naming.Common
AudioBookNamesExpressions = new[] AudioBookNamesExpressions = new[]
{ {
// Detect year usually in brackets after name Batman (2020) // Detect year usually in brackets after name Batman (2020)
@"^(?<name>.+?)\s*\(\s*(?<year>\d{4})\s*\)\s*$", @"^(?<name>.+?)\s*\(\s*(?<year>[0-9]{4})\s*\)\s*$",
@"^\s*(?<name>[^ ].*?)\s*$" @"^\s*(?<name>[^ ].*?)\s*$"
}; };

View File

@ -23,17 +23,18 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\SharedVersion.cs" /> <Compile Include="../SharedVersion.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.7.0</VersionPrefix> <VersionPrefix>10.8.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@ -44,7 +45,6 @@
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

View File

@ -68,6 +68,11 @@ namespace Emby.Naming.TV
var parsingResult = new EpisodePathParser(_options) var parsingResult = new EpisodePathParser(_options)
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo); .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
if (!parsingResult.Success && !isStub)
{
return null;
}
return new EpisodeInfo(path) return new EpisodeInfo(path)
{ {
Container = container, Container = container,

View File

@ -60,7 +60,7 @@ namespace Emby.Naming.TV
bool supportSpecialAliases, bool supportSpecialAliases,
bool supportNumericSeasonFolders) bool supportNumericSeasonFolders)
{ {
var filename = Path.GetFileName(path) ?? string.Empty; string filename = Path.GetFileName(path);
if (supportSpecialAliases) if (supportSpecialAliases)
{ {

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Emby.Naming.Video namespace Emby.Naming.Video
@ -16,8 +17,14 @@ namespace Emby.Naming.Video
/// <param name="expressions">List of regex to parse name and year from.</param> /// <param name="expressions">List of regex to parse name and year from.</param>
/// <param name="newName">Parsing result string.</param> /// <param name="newName">Parsing result string.</param>
/// <returns>True if parsing was successful.</returns> /// <returns>True if parsing was successful.</returns>
public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName) public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
{ {
if (string.IsNullOrEmpty(name))
{
newName = ReadOnlySpan<char>.Empty;
return false;
}
var len = expressions.Count; var len = expressions.Count;
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
{ {
@ -41,7 +48,7 @@ namespace Emby.Naming.Video
return true; return true;
} }
newName = string.Empty; newName = ReadOnlySpan<char>.Empty;
return false; return false;
} }
} }

View File

@ -29,72 +29,75 @@ namespace Emby.Naming.Video
/// <param name="path">Path to file.</param> /// <param name="path">Path to file.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns> /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public ExtraResult GetExtraInfo(string path) public ExtraResult GetExtraInfo(string path)
{
return _options.VideoExtraRules
.Select(i => GetExtraInfo(path, i))
.FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
}
private ExtraResult GetExtraInfo(string path, ExtraRule rule)
{ {
var result = new ExtraResult(); var result = new ExtraResult();
if (rule.MediaType == MediaType.Audio) for (var i = 0; i < _options.VideoExtraRules.Length; i++)
{ {
if (!AudioFileParser.IsAudioFile(path, _options)) var rule = _options.VideoExtraRules[i];
if (rule.MediaType == MediaType.Audio)
{
if (!AudioFileParser.IsAudioFile(path, _options))
{
continue;
}
}
else if (rule.MediaType == MediaType.Video)
{
if (!new VideoResolver(_options).IsVideoFile(path))
{
continue;
}
}
var pathSpan = path.AsSpan();
if (rule.RuleType == ExtraRuleType.Filename)
{
var filename = Path.GetFileNameWithoutExtension(pathSpan);
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
var filename = Path.GetFileNameWithoutExtension(pathSpan);
if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
var filename = Path.GetFileName(path);
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
if (regex.IsMatch(filename))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
if (result.ExtraType != null)
{ {
return result; return result;
} }
} }
else if (rule.MediaType == MediaType.Video)
{
if (!new VideoResolver(_options).IsVideoFile(path))
{
return result;
}
}
if (rule.RuleType == ExtraRuleType.Filename)
{
var filename = Path.GetFileNameWithoutExtension(path);
if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
var filename = Path.GetFileNameWithoutExtension(path);
if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
var filename = Path.GetFileName(path);
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
if (regex.IsMatch(filename))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
return result; return result;
} }

View File

@ -185,8 +185,8 @@ namespace Emby.Naming.Video
if (!string.IsNullOrEmpty(folderName) if (!string.IsNullOrEmpty(folderName)
&& folderName.Length > 1 && folderName.Length > 1
&& videos.All(i => i.Files.Count == 1 && videos.All(i => i.Files.Count == 1
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path)) && IsEligibleForMultiVersion(folderName, i.Files[0].Path))
&& HaveSameYear(videos)) && HaveSameYear(videos))
{ {
var ordered = videos.OrderBy(i => i.Name).ToList(); var ordered = videos.OrderBy(i => i.Name).ToList();
@ -216,26 +216,26 @@ namespace Emby.Naming.Video
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2; return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
} }
private bool IsEligibleForMultiVersion(string folderName, string? testFilename) private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
{ {
testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty; string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{ {
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName)) // Remove the folder name before cleaning as we don't care about cleaning that part
{
testFilename = cleanName.ToString();
}
if (folderName.Length <= testFilename.Length) if (folderName.Length <= testFilename.Length)
{ {
testFilename = testFilename.Substring(folderName.Length).Trim(); testFilename = testFilename.Substring(folderName.Length).Trim();
} }
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.Trim().ToString();
}
// The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(testFilename) return string.IsNullOrEmpty(testFilename)
|| testFilename[0].Equals('-') || testFilename[0] == '-'
|| testFilename[0].Equals('_') || Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
} }
return false; return false;

View File

@ -1,7 +1,8 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq;
using Emby.Naming.Common; using Emby.Naming.Common;
using MediaBrowser.Common.Extensions;
namespace Emby.Naming.Video namespace Emby.Naming.Video
{ {
@ -58,15 +59,15 @@ namespace Emby.Naming.Video
} }
bool isStub = false; bool isStub = false;
string? container = null; ReadOnlySpan<char> container = ReadOnlySpan<char>.Empty;
string? stubType = null; string? stubType = null;
if (!isDirectory) if (!isDirectory)
{ {
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path.AsSpan());
// Check supported extensions // Check supported extensions
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{ {
// It's not supported. Check stub extensions // It's not supported. Check stub extensions
if (!StubResolver.TryResolveFile(path, _options, out stubType)) if (!StubResolver.TryResolveFile(path, _options, out stubType))
@ -85,9 +86,7 @@ namespace Emby.Naming.Video
var extraResult = new ExtraResolver(_options).GetExtraInfo(path); var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
var name = isDirectory var name = Path.GetFileNameWithoutExtension(path);
? Path.GetFileName(path)
: Path.GetFileNameWithoutExtension(path);
int? year = null; int? year = null;
@ -106,7 +105,7 @@ namespace Emby.Naming.Video
return new VideoFileInfo( return new VideoFileInfo(
path: path, path: path,
container: container, container: container.IsEmpty ? null : container.ToString(),
isStub: isStub, isStub: isStub,
name: name, name: name,
year: year, year: year,
@ -125,8 +124,8 @@ namespace Emby.Naming.Video
/// <returns>True if is video file.</returns> /// <returns>True if is video file.</returns>
public bool IsVideoFile(string path) public bool IsVideoFile(string path)
{ {
var extension = Path.GetExtension(path) ?? string.Empty; var extension = Path.GetExtension(path.AsSpan());
return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); return _options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
} }
/// <summary> /// <summary>
@ -136,8 +135,8 @@ namespace Emby.Naming.Video
/// <returns>True if is video file stub.</returns> /// <returns>True if is video file stub.</returns>
public bool IsStubFile(string path) public bool IsStubFile(string path)
{ {
var extension = Path.GetExtension(path) ?? string.Empty; var extension = Path.GetExtension(path.AsSpan());
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); return _options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
} }
/// <summary> /// <summary>
@ -146,7 +145,7 @@ namespace Emby.Naming.Video
/// <param name="name">Raw name.</param> /// <param name="name">Raw name.</param>
/// <param name="newName">Clean name.</param> /// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns> /// <returns>True if cleaning of name was successful.</returns>
public bool TryCleanString(string name, out ReadOnlySpan<char> newName) public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
{ {
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName); return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
} }

View File

@ -75,10 +75,6 @@ namespace Emby.Notifications
Type = NotificationType.VideoPlaybackStopped.ToString() Type = NotificationType.VideoPlaybackStopped.ToString()
}, },
new NotificationTypeInfo new NotificationTypeInfo
{
Type = NotificationType.CameraImageUploaded.ToString()
},
new NotificationTypeInfo
{ {
Type = NotificationType.UserLockedOut.ToString() Type = NotificationType.UserLockedOut.ToString()
}, },
@ -114,10 +110,6 @@ namespace Emby.Notifications
{ {
note.Category = _localization.GetLocalizedString("Plugin"); note.Category = _localization.GetLocalizedString("Plugin");
} }
else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("Sync");
}
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1) else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
{ {
note.Category = _localization.GetLocalizedString("User"); note.Category = _localization.GetLocalizedString("User");

View File

@ -11,6 +11,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -25,14 +27,9 @@
<!-- Code analyzers--> <!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project> </Project>

View File

@ -24,18 +24,15 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project> </Project>

View File

@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath; CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath; WebPath = webDirectoryPath;
DataPath = Path.Combine(ProgramDataPath, "data"); _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
} }
/// <summary> /// <summary>
@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory. /// Gets the folder path to the data directory.
/// </summary> /// </summary>
/// <value>The data directory.</value> /// <value>The data directory.</value>
public string DataPath public string DataPath => _dataPath;
{
get => _dataPath;
private set => _dataPath = Directory.CreateDirectory(value).FullName;
}
/// <inheritdoc /> /// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%"; public string VirtualDataPath => "%AppDataPath%";

View File

@ -1,3 +1,5 @@
#nullable disable
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>(); private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
/// <summary>
/// The _configuration sync lock.
/// </summary>
private readonly object _configurationSyncLock = new object();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase
/// </summary> /// </summary>
private bool _configurationLoaded; private bool _configurationLoaded;
/// <summary>
/// The _configuration sync lock.
/// </summary>
private readonly object _configurationSyncLock = new object();
/// <summary> /// <summary>
/// The _configuration. /// The _configuration.
/// </summary> /// </summary>

View File

@ -1,9 +1,6 @@
#nullable enable
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase namespace Emby.Server.Implementations.AppBase
@ -36,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
} }
catch (Exception) catch (Exception)
{ {
configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type)); // Note: CreateInstance returns null for Nullable<T>, e.g. CreateInstance(typeof(int?)) returns null.
configuration = Activator.CreateInstance(type)!;
} }
using var stream = new MemoryStream(buffer?.Length ?? 0); using var stream = new MemoryStream(buffer?.Length ?? 0);
@ -53,7 +51,8 @@ namespace Emby.Server.Implementations.AppBase
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
// Save it after load in case we got new items // Save it after load in case we got new items
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{ {
fs.Write(newBytes, 0, newBytesLen); fs.Write(newBytes, 0, newBytesLen);
} }

View File

@ -1,16 +1,17 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna; using Emby.Dlna;
@ -42,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session; using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV; using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Configuration;
@ -97,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime; using Prometheus.DotNetRuntime;
@ -116,10 +119,12 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager; private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions; private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder; private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
private string[] _urlPrefixes; private string[] _urlPrefixes;
@ -181,16 +186,6 @@ namespace Emby.Server.Implementations
protected IServiceCollection ServiceCollection { get; } protected IServiceCollection ServiceCollection { get; }
private IPlugin[] _plugins;
private IReadOnlyList<LocalPlugin> _pluginsManifests;
/// <summary>
/// Gets the plugins.
/// </summary>
/// <value>The plugins.</value>
public IReadOnlyList<IPlugin> Plugins => _plugins;
/// <summary> /// <summary>
/// Gets the logger factory. /// Gets the logger factory.
/// </summary> /// </summary>
@ -217,7 +212,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the configuration manager. /// Gets or sets the configuration manager.
/// </summary> /// </summary>
/// <value>The configuration manager.</value> /// <value>The configuration manager.</value>
protected IConfigurationManager ConfigurationManager { get; set; } public ServerConfigurationManager ConfigurationManager { get; set; }
/// <summary> /// <summary>
/// Gets or sets the service provider. /// Gets or sets the service provider.
@ -235,10 +230,9 @@ namespace Emby.Server.Implementations
public int HttpsPort { get; private set; } public int HttpsPort { get; private set; }
/// <summary> /// <summary>
/// Gets the server configuration manager. /// Gets the value of the PublishedServerUrl setting.
/// </summary> /// </summary>
/// <value>The server configuration manager.</value> public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class. /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@ -246,54 +240,39 @@ namespace Emby.Server.Implementations
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param> /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param> /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost( public ApplicationHost(
IServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStartupOptions options, IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem, IFileSystem fileSystem,
IServiceCollection serviceCollection) IServiceCollection serviceCollection)
{ {
_xmlSerializer = new MyXmlSerializer();
_jsonSerializer = new JsonSerializer();
ServiceCollection = serviceCollection;
ApplicationPaths = applicationPaths; ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
_fileSystemManager = fileSystem; _fileSystemManager = fileSystem;
ServiceCollection = serviceCollection;
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
// Have to migrate settings here as migration subsystem not yet initialised.
MigrateNetworkConfiguration();
// Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
Logger = LoggerFactory.CreateLogger<ApplicationHost>(); Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_startupOptions = options;
// Initialize runtime stat collection
if (ServerConfigurationManager.Configuration.EnableMetrics)
{
DotNetRuntimeStatsBuilder.Default().StartCollecting();
}
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
CertificateInfo = new CertificateInfo
{
Path = ServerConfigurationManager.Configuration.CertificatePath,
Password = ServerConfigurationManager.Configuration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3); ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer();
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
} }
/// <summary> /// <summary>
@ -306,9 +285,9 @@ namespace Emby.Server.Implementations
if (!File.Exists(path)) if (!File.Exists(path))
{ {
var networkSettings = new NetworkConfiguration(); var networkSettings = new NetworkConfiguration();
ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings); ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
_xmlSerializer.SerializeToFile(networkSettings, path); _xmlSerializer.SerializeToFile(networkSettings, path);
Logger?.LogDebug("Successfully migrated network settings."); Logger.LogDebug("Successfully migrated network settings.");
} }
} }
@ -358,10 +337,7 @@ namespace Emby.Server.Implementations
{ {
get get
{ {
if (_deviceId == null) _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
{
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
}
return _deviceId.Value; return _deviceId.Value;
} }
@ -381,7 +357,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Creates an instance of type and resolves all constructor dependencies. /// Creates an instance of type and resolves all constructor dependencies.
/// </summary> /// </summary>
/// /// <typeparam name="T">The type.</typeparam> /// <typeparam name="T">The type.</typeparam>
/// <returns>T.</returns> /// <returns>T.</returns>
public T CreateInstance<T>() public T CreateInstance<T>()
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider); => ActivatorUtilities.CreateInstance<T>(ServiceProvider);
@ -393,16 +369,38 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns> /// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type) protected object CreateInstanceSafe(Type type)
{ {
_creatingInstances ??= new List<Type>();
if (_creatingInstances.IndexOf(type) != -1)
{
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
foreach (var entry in _creatingInstances)
{
Logger.LogError("Called from: {TypeName}", entry.FullName);
}
_pluginManager.FailPlugin(type.Assembly);
throw new ExternalException("DI Loop detected.");
}
try try
{ {
_creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type); Logger.LogDebug("Creating instance of {Type}", type);
return ActivatorUtilities.CreateInstance(ServiceProvider, type); return ActivatorUtilities.CreateInstance(ServiceProvider, type);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error creating {Type}", type); Logger.LogError(ex, "Error creating {Type}", type);
// If this is a plugin fail it.
_pluginManager.FailPlugin(type.Assembly);
return null; return null;
} }
finally
{
_creatingInstances.Remove(type);
}
} }
/// <summary> /// <summary>
@ -412,11 +410,7 @@ namespace Emby.Server.Implementations
/// <returns>``0.</returns> /// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>(); public T Resolve<T>() => ServiceProvider.GetService<T>();
/// <summary> /// <inheritdoc/>
/// Gets the export types.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>IEnumerable{Type}.</returns>
public IEnumerable<Type> GetExportTypes<T>() public IEnumerable<Type> GetExportTypes<T>()
{ {
var currentType = typeof(T); var currentType = typeof(T);
@ -445,17 +439,40 @@ namespace Emby.Server.Implementations
return parts; return parts;
} }
/// <inheritdoc />
public IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
{
// Convert to list so this isn't executed for each iteration
var parts = GetExportTypes<T>()
.Select(i => defaultFunc(i))
.Where(i => i != null)
.Cast<T>()
.ToList();
if (manageLifetime)
{
lock (_disposableParts)
{
_disposableParts.AddRange(parts.OfType<IDisposable>());
}
}
return parts;
}
/// <summary> /// <summary>
/// Runs the startup tasks. /// Runs the startup tasks.
/// </summary> /// </summary>
/// <returns><see cref="Task" />.</returns> /// <returns><see cref="Task" />.</returns>
public async Task RunStartupTasksAsync() public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
{ {
cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks"); Logger.LogInformation("Running startup tasks");
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
_mediaEncoder.SetFFmpegPath(); _mediaEncoder.SetFFmpegPath();
@ -463,14 +480,21 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports<IServerEntryPoint>(); var entryPoints = GetExports<IServerEntryPoint>();
cancellationToken.ThrowIfCancellationRequested();
var stopWatch = new Stopwatch(); var stopWatch = new Stopwatch();
stopWatch.Start(); stopWatch.Start();
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false); await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete"); Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true; CoreStartupHasCompleted = true;
cancellationToken.ThrowIfCancellationRequested();
stopWatch.Restart(); stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
stopWatch.Stop(); stopWatch.Stop();
@ -494,7 +518,21 @@ namespace Emby.Server.Implementations
/// <inheritdoc/> /// <inheritdoc/>
public void Init() public void Init()
{ {
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); DiscoverTypes();
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
// Have to migrate settings here as migration subsystem not yet initialised.
MigrateNetworkConfiguration();
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
{
DotNetRuntimeStatsBuilder.Default().StartCollecting();
}
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
HttpPort = networkConfiguration.HttpServerPortNumber; HttpPort = networkConfiguration.HttpServerPortNumber;
HttpsPort = networkConfiguration.HttpsPortNumber; HttpsPort = networkConfiguration.HttpsPortNumber;
@ -505,11 +543,16 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort; HttpsPort = NetworkConfiguration.DefaultHttpsPort;
} }
DiscoverTypes(); CertificateInfo = new CertificateInfo
{
Path = networkConfiguration.CertificatePath,
Password = networkConfiguration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
RegisterServices(); RegisterServices();
RegisterPluginServices(); _pluginManager.RegisterServices(ServiceCollection);
} }
/// <summary> /// <summary>
@ -521,13 +564,12 @@ namespace Emby.Server.Implementations
ServiceCollection.AddMemoryCache(); ServiceCollection.AddMemoryCache();
ServiceCollection.AddSingleton(ConfigurationManager); ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this); ServiceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
ServiceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TmdbClientManager>(); ServiceCollection.AddSingleton<TmdbClientManager>();
@ -550,8 +592,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IServerApplicationHost>(this); ServiceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton(ServerConfigurationManager);
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
@ -563,12 +603,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>(); ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@ -633,14 +669,14 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
ServiceCollection.AddSingleton<TranscodingJobHelper>(); ServiceCollection.AddSingleton<TranscodingJobHelper>();
ServiceCollection.AddScoped<MediaInfoHelper>(); ServiceCollection.AddScoped<MediaInfoHelper>();
ServiceCollection.AddScoped<AudioHelper>(); ServiceCollection.AddScoped<AudioHelper>();
ServiceCollection.AddScoped<DynamicHlsHelper>(); ServiceCollection.AddScoped<DynamicHlsHelper>();
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
} }
/// <summary> /// <summary>
@ -714,7 +750,7 @@ namespace Emby.Server.Implementations
// Don't use an empty string password // Don't use an empty string password
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password; var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
var localCert = new X509Certificate2(certificateLocation, password); var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; // localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
if (!localCert.HasPrivateKey) if (!localCert.HasPrivateKey)
{ {
@ -738,7 +774,7 @@ namespace Emby.Server.Implementations
{ {
// For now there's no real way to inject these properly // For now there's no real way to inject these properly
BaseItem.Logger = Resolve<ILogger<BaseItem>>(); BaseItem.Logger = Resolve<ILogger<BaseItem>>();
BaseItem.ConfigurationManager = ServerConfigurationManager; BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.LibraryManager = Resolve<ILibraryManager>(); BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.ProviderManager = Resolve<IProviderManager>(); BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
@ -752,7 +788,6 @@ namespace Emby.Server.Implementations
UserView.CollectionManager = Resolve<ICollectionManager>(); UserView.CollectionManager = Resolve<ICollectionManager>();
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>(); BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
CollectionFolder.XmlSerializer = _xmlSerializer; CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this; CollectionFolder.ApplicationHost = this;
} }
@ -761,41 +796,13 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
private void FindParts() private void FindParts()
{ {
if (!ServerConfigurationManager.Configuration.IsPortAuthorized) if (!ConfigurationManager.Configuration.IsPortAuthorized)
{ {
ServerConfigurationManager.Configuration.IsPortAuthorized = true; ConfigurationManager.Configuration.IsPortAuthorized = true;
ConfigurationManager.SaveConfiguration(); ConfigurationManager.SaveConfiguration();
} }
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); _pluginManager.CreatePlugins();
_plugins = GetExports<IPlugin>()
.Where(i => i != null)
.ToArray();
if (Plugins != null)
{
foreach (var plugin in Plugins)
{
if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin)
{
// Ensure the version number matches the Plugin Manifest information.
foreach (var item in _pluginsManifests)
{
if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase))
{
// Update version number to that of the manifest.
assemblyPlugin.SetAttributes(
plugin.AssemblyFilePath,
Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)),
item.Version);
break;
}
}
}
Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
}
}
_urlPrefixes = GetUrlPrefixes().ToArray(); _urlPrefixes = GetUrlPrefixes().ToArray();
@ -834,22 +841,6 @@ namespace Emby.Server.Implementations
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray(); _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
} }
private void RegisterPluginServices()
{
foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
{
try
{
var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
instance.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
}
}
}
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies) private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{ {
foreach (var ass in assemblies) foreach (var ass in assemblies)
@ -862,11 +853,13 @@ namespace Emby.Server.Implementations
catch (FileNotFoundException ex) catch (FileNotFoundException ex)
{ {
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
_pluginManager.FailPlugin(ass);
continue; continue;
} }
catch (TypeLoadException ex) catch (TypeLoadException ex)
{ {
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName); Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
_pluginManager.FailPlugin(ass);
continue; continue;
} }
@ -912,19 +905,19 @@ namespace Emby.Server.Implementations
protected void OnConfigurationUpdated(object sender, EventArgs e) protected void OnConfigurationUpdated(object sender, EventArgs e)
{ {
var requiresRestart = false; var requiresRestart = false;
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
// Don't do anything if these haven't been set yet // Don't do anything if these haven't been set yet
if (HttpPort != 0 && HttpsPort != 0) if (HttpPort != 0 && HttpsPort != 0)
{ {
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
// Need to restart if ports have changed // Need to restart if ports have changed
if (networkConfiguration.HttpServerPortNumber != HttpPort || if (networkConfiguration.HttpServerPortNumber != HttpPort ||
networkConfiguration.HttpsPortNumber != HttpsPort) networkConfiguration.HttpsPortNumber != HttpsPort)
{ {
if (ServerConfigurationManager.Configuration.IsPortAuthorized) if (ConfigurationManager.Configuration.IsPortAuthorized)
{ {
ServerConfigurationManager.Configuration.IsPortAuthorized = false; ConfigurationManager.Configuration.IsPortAuthorized = false;
ServerConfigurationManager.SaveConfiguration(); ConfigurationManager.SaveConfiguration();
requiresRestart = true; requiresRestart = true;
} }
@ -936,10 +929,7 @@ namespace Emby.Server.Implementations
requiresRestart = true; requiresRestart = true;
} }
var currentCertPath = CertificateInfo?.Path; if (ValidateSslCertificate(networkConfiguration))
var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
{ {
requiresRestart = true; requiresRestart = true;
} }
@ -952,6 +942,33 @@ namespace Emby.Server.Implementations
} }
} }
/// <summary>
/// Validates the SSL certificate.
/// </summary>
/// <param name="networkConfig">The new configuration.</param>
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
private bool ValidateSslCertificate(NetworkConfiguration networkConfig)
{
var newPath = networkConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
{
if (File.Exists(newPath))
{
return true;
}
throw new FileNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"Certificate file '{0}' does not exist.",
newPath));
}
return false;
}
/// <summary> /// <summary>
/// Notifies that the kernel that a change has been made that requires a restart. /// Notifies that the kernel that a change has been made that requires a restart.
/// </summary> /// </summary>
@ -1005,129 +1022,15 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal(); protected abstract void RestartInternal();
/// <inheritdoc/>
public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
{
var minimumVersion = new Version(0, 0, 0, 1);
var versions = new List<LocalPlugin>();
if (!Directory.Exists(path))
{
// Plugin path doesn't exist, don't try to enumerate subfolders.
return Enumerable.Empty<LocalPlugin>();
}
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
try
{
var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
targetAbi = minimumVersion;
}
if (!Version.TryParse(manifest.Version, out var version))
{
version = minimumVersion;
}
if (ApplicationVersion >= targetAbi)
{
// Only load Plugins if the plugin is built for this version or below.
versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
}
}
else
{
// No metafile, so lets see if the folder is versioned.
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion))
{
// Versioned folder.
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
}
else
{
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
}
}
}
catch
{
continue;
}
}
string lastName = string.Empty;
versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list.
// The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--)
{
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
{
versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
lastName = versions[x].Name;
continue;
}
if (!string.IsNullOrEmpty(lastName) && cleanup)
{
// Attempt a cleanup of old folders.
try
{
Logger.LogDebug("Deleting {Path}", versions[x].Path);
Directory.Delete(versions[x].Path, true);
}
catch (Exception e)
{
Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
}
versions.RemoveAt(x);
}
}
return versions;
}
/// <summary> /// <summary>
/// Gets the composable part assemblies. /// Gets the composable part assemblies.
/// </summary> /// </summary>
/// <returns>IEnumerable{Assembly}.</returns> /// <returns>IEnumerable{Assembly}.</returns>
protected IEnumerable<Assembly> GetComposablePartAssemblies() protected IEnumerable<Assembly> GetComposablePartAssemblies()
{ {
if (Directory.Exists(ApplicationPaths.PluginsPath)) foreach (var p in _pluginManager.LoadAssemblies())
{ {
_pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList(); yield return p;
foreach (var plugin in _pluginsManifests)
{
foreach (var file in plugin.DllFiles)
{
Assembly plugAss;
try
{
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss;
}
}
} }
// Include composable parts in the Model assembly // Include composable parts in the Model assembly
@ -1230,16 +1133,16 @@ namespace Emby.Server.Implementations
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps; public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null) public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
{ {
// Published server ends with a / // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null) if (!string.IsNullOrEmpty(PublishedServerUrl))
{ {
// Published server ends with a '/', so we need to remove it. // Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(ipAddress, out port); string smart = NetManager.GetBindInterface(ipAddress, out port);
@ -1256,10 +1159,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(HttpRequest request, int? port = null) public string GetSmartApiUrl(HttpRequest request, int? port = null)
{ {
// Published server ends with a / // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null) if (!string.IsNullOrEmpty(PublishedServerUrl))
{ {
// Published server ends with a '/', so we need to remove it. // Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(request, out port); string smart = NetManager.GetBindInterface(request, out port);
@ -1276,10 +1179,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(string hostname, int? port = null) public string GetSmartApiUrl(string hostname, int? port = null)
{ {
// Published server ends with a / // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null) if (!string.IsNullOrEmpty(PublishedServerUrl))
{ {
// Published server ends with a '/', so we need to remove it. // Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(hostname, out port); string smart = NetManager.GetBindInterface(hostname, out port);
@ -1314,14 +1217,14 @@ namespace Emby.Server.Implementations
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
Host = host, Host = host,
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
}.ToString().TrimEnd('/'); }.ToString().TrimEnd('/');
} }
public string FriendlyName => public string FriendlyName =>
string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName) string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName ? Environment.MachineName
: ServerConfigurationManager.Configuration.ServerName; : ConfigurationManager.Configuration.ServerName;
/// <summary> /// <summary>
/// Shuts down. /// Shuts down.
@ -1369,17 +1272,6 @@ namespace Emby.Server.Implementations
} }
} }
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
public void RemovePlugin(IPlugin plugin)
{
var list = _plugins.ToList();
list.Remove(plugin);
_plugins = list.ToArray();
}
public IEnumerable<Assembly> GetApiPluginAssemblies() public IEnumerable<Assembly> GetApiPluginAssemblies()
{ {
var assemblies = _allConcreteTypes var assemblies = _allConcreteTypes

View File

@ -1,13 +1,17 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Progress; using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -21,7 +25,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@ -44,10 +47,10 @@ namespace Emby.Server.Implementations.Channels
private readonly ILogger<ChannelManager> _logger; private readonly ILogger<ChannelManager> _logger;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager; private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ChannelManager"/> class. /// Initializes a new instance of the <see cref="ChannelManager"/> class.
@ -59,7 +62,6 @@ namespace Emby.Server.Implementations.Channels
/// <param name="config">The server configuration manager.</param> /// <param name="config">The server configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param> /// <param name="fileSystem">The filesystem.</param>
/// <param name="userDataManager">The user data manager.</param> /// <param name="userDataManager">The user data manager.</param>
/// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="providerManager">The provider manager.</param> /// <param name="providerManager">The provider manager.</param>
/// <param name="memoryCache">The memory cache.</param> /// <param name="memoryCache">The memory cache.</param>
public ChannelManager( public ChannelManager(
@ -70,7 +72,6 @@ namespace Emby.Server.Implementations.Channels
IServerConfigurationManager config, IServerConfigurationManager config,
IFileSystem fileSystem, IFileSystem fileSystem,
IUserDataManager userDataManager, IUserDataManager userDataManager,
IJsonSerializer jsonSerializer,
IProviderManager providerManager, IProviderManager providerManager,
IMemoryCache memoryCache) IMemoryCache memoryCache)
{ {
@ -81,7 +82,6 @@ namespace Emby.Server.Implementations.Channels
_config = config; _config = config;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_userDataManager = userDataManager; _userDataManager = userDataManager;
_jsonSerializer = jsonSerializer;
_providerManager = providerManager; _providerManager = providerManager;
_memoryCache = memoryCache; _memoryCache = memoryCache;
} }
@ -337,21 +337,23 @@ namespace Emby.Server.Implementations.Channels
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result; return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
} }
private List<MediaSourceInfo> GetSavedMediaSources(BaseItem item) private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
{ {
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json"); var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
try try
{ {
return _jsonSerializer.DeserializeFromFile<List<MediaSourceInfo>>(path) ?? new List<MediaSourceInfo>(); var bytes = File.ReadAllBytes(path);
return JsonSerializer.Deserialize<MediaSourceInfo[]>(bytes, _jsonOptions)
?? Array.Empty<MediaSourceInfo>();
} }
catch catch
{ {
return new List<MediaSourceInfo>(); return Array.Empty<MediaSourceInfo>();
} }
} }
private void SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources) private async Task SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
{ {
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json"); var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
@ -370,7 +372,8 @@ namespace Emby.Server.Implementations.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path));
_jsonSerializer.SerializeToFile(mediaSources, path); await using FileStream createStream = File.Create(path);
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -812,7 +815,8 @@ namespace Emby.Server.Implementations.Channels
{ {
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{ {
var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); await using FileStream jsonStream = File.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null) if (cachedResult != null)
{ {
return null; return null;
@ -834,7 +838,8 @@ namespace Emby.Server.Implementations.Channels
{ {
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{ {
var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); await using FileStream jsonStream = File.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null) if (cachedResult != null)
{ {
return null; return null;
@ -865,7 +870,7 @@ namespace Emby.Server.Implementations.Channels
throw new InvalidOperationException("Channel returned a null result from GetChannelItems"); throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
} }
CacheResponse(result, cachePath); await CacheResponse(result, cachePath);
return result; return result;
} }
@ -875,13 +880,14 @@ namespace Emby.Server.Implementations.Channels
} }
} }
private void CacheResponse(object result, string path) private async Task CacheResponse(object result, string path)
{ {
try try
{ {
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path));
_jsonSerializer.SerializeToFile(result, path); await using FileStream createStream = File.Create(path);
await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -1176,11 +1182,11 @@ namespace Emby.Server.Implementations.Channels
{ {
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol) if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
{ {
SaveMediaSources(item, new List<MediaSourceInfo>()); await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
} }
else else
{ {
SaveMediaSources(item, info.MediaSources); await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
} }
} }

View File

@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections
return null; return null;
}) })
.Where(i => i != null) .Where(i => i != null)
.GroupBy(x => x.Id) .GroupBy(x => x!.Id) // We removed the null values
.Select(x => x.First()) .Select(x => x.First())
.ToList(); .ToList()!; // Again... the list doesn't contain any null values
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -1,6 +1,7 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -8,11 +9,9 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -107,7 +106,7 @@ namespace Emby.Server.Implementations.Collections
var name = _localizationManager.GetLocalizedString("Collections"); var name = _localizationManager.GetLocalizedString("Collections");
await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false); await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
return FindFolders(path).First(); return FindFolders(path).First();
} }
@ -124,7 +123,7 @@ namespace Emby.Server.Implementations.Collections
private IEnumerable<BoxSet> GetCollections(User user) private IEnumerable<BoxSet> GetCollections(User user)
{ {
var folder = GetCollectionsFolder(false).Result; var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
return folder == null return folder == null
? Enumerable.Empty<BoxSet>() ? Enumerable.Empty<BoxSet>()
@ -167,7 +166,7 @@ namespace Emby.Server.Implementations.Collections
parentFolder.AddChild(collection, CancellationToken.None); parentFolder.AddChild(collection, CancellationToken.None);
if (options.ItemIdList.Length > 0) if (options.ItemIdList.Count > 0)
{ {
await AddToCollectionAsync( await AddToCollectionAsync(
collection.Id, collection.Id,
@ -251,11 +250,7 @@ namespace Emby.Server.Implementations.Collections
if (fireEvent) if (fireEvent)
{ {
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
{
Collection = collection,
ItemsChanged = itemList
});
} }
} }
} }
@ -307,11 +302,7 @@ namespace Emby.Server.Implementations.Collections
}, },
RefreshPriority.High); RefreshPriority.High);
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
{
Collection = collection,
ItemsChanged = itemList
});
} }
/// <inheritdoc /> /// <inheritdoc />
@ -319,11 +310,11 @@ namespace Emby.Server.Implementations.Collections
{ {
var results = new Dictionary<Guid, BaseItem>(); var results = new Dictionary<Guid, BaseItem>();
var allBoxsets = GetCollections(user).ToList(); var allBoxSets = GetCollections(user).ToList();
foreach (var item in items) foreach (var item in items)
{ {
if (!(item is ISupportsBoxSetGrouping)) if (item is not ISupportsBoxSetGrouping)
{ {
results[item.Id] = item; results[item.Id] = item;
} }
@ -331,20 +322,44 @@ namespace Emby.Server.Implementations.Collections
{ {
var itemId = item.Id; var itemId = item.Id;
var currentBoxSets = allBoxsets var itemIsInBoxSet = false;
.Where(i => i.ContainsLinkedChildByItemId(itemId)) foreach (var boxSet in allBoxSets)
.ToList();
if (currentBoxSets.Count > 0)
{ {
foreach (var boxset in currentBoxSets) if (!boxSet.ContainsLinkedChildByItemId(itemId))
{ {
results[boxset.Id] = boxset; continue;
}
itemIsInBoxSet = true;
results.TryAdd(boxSet.Id, boxSet);
}
// skip any item that is in a box set
if (itemIsInBoxSet)
{
continue;
}
var alreadyInResults = false;
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
if (item is Video video)
{
foreach (var childId in video.GetLocalAlternateVersionIds())
{
if (!results.ContainsKey(childId))
{
continue;
}
alreadyInResults = true;
break;
} }
} }
else
if (!alreadyInResults)
{ {
results[item.Id] = item; results[itemId] = item;
} }
} }
} }

View File

@ -1,3 +1,5 @@
#nullable disable
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -88,38 +90,12 @@ namespace Emby.Server.Implementations.Configuration
var newConfig = (ServerConfiguration)newConfiguration; var newConfig = (ServerConfiguration)newConfiguration;
ValidateMetadataPath(newConfig); ValidateMetadataPath(newConfig);
ValidateSslCertificate(newConfig);
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig)); ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
base.ReplaceConfiguration(newConfiguration); base.ReplaceConfiguration(newConfiguration);
} }
/// <summary>
/// Validates the SSL certificate.
/// </summary>
/// <param name="newConfig">The new configuration.</param>
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
private void ValidateSslCertificate(BaseApplicationConfiguration newConfig)
{
var serverConfig = (ServerConfiguration)newConfig;
var newPath = serverConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
{
if (!File.Exists(newPath))
{
throw new FileNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"Certificate file '{0}' does not exist.",
newPath));
}
}
}
/// <summary> /// <summary>
/// Validates the metadata path. /// Validates the metadata path.
/// </summary> /// </summary>

View File

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations namespace Emby.Server.Implementations

View File

@ -1,5 +1,3 @@
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Cryptography; using System.Security.Cryptography;

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
{ {
if (row[1].SQLiteType != SQLiteType.Null) if (row.TryGetString(1, out var columnName))
{ {
var name = row[1].ToString(); columnNames.Add(columnName);
columnNames.Add(name);
} }
} }

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