Merge branch 'master' into Plugins

This commit is contained in:
BaronGreenback 2020-09-05 20:49:04 +01:00 committed by GitHub
commit 8439860b2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
1097 changed files with 42305 additions and 43436 deletions

View file

@ -12,10 +12,12 @@ parameters:
jobs: jobs:
- job: CompatibilityCheck - job: CompatibilityCheck
displayName: Compatibility Check displayName: Compatibility Check
dependsOn: Build
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
pool: pool:
vmImage: "${{ parameters.LinuxImage }}" vmImage: "${{ parameters.LinuxImage }}"
# only execute for pull requests
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
strategy: strategy:
matrix: matrix:
${{ each Package in parameters.Packages }}: ${{ each Package in parameters.Packages }}:
@ -23,7 +25,7 @@ jobs:
NugetPackageName: ${{ Package.value.NugetPackageName }} NugetPackageName: ${{ Package.value.NugetPackageName }}
AssemblyFileName: ${{ Package.value.AssemblyFileName }} AssemblyFileName: ${{ Package.value.AssemblyFileName }}
maxParallel: 2 maxParallel: 2
dependsOn: Build
steps: steps:
- checkout: none - checkout: none
@ -34,32 +36,32 @@ jobs:
version: ${{ parameters.DotNetSdkVersion }} version: ${{ parameters.DotNetSdkVersion }}
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Install ABI CompatibilityChecker tool' displayName: 'Install ABI CompatibilityChecker Tool'
inputs: inputs:
command: custom command: custom
custom: tool custom: tool
arguments: 'update compatibilitychecker -g' arguments: 'update compatibilitychecker -g'
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: "Download New Assembly Build Artifact" displayName: 'Download New Assembly Build Artifact'
inputs: inputs:
source: "current" source: 'current'
artifact: "$(NugetPackageName)" artifact: "$(NugetPackageName)"
path: "$(System.ArtifactsDirectory)/new-artifacts" path: "$(System.ArtifactsDirectory)/new-artifacts"
runVersion: "latest" runVersion: "latest"
- task: CopyFiles@2 - task: CopyFiles@2
displayName: "Copy New Assembly Build Artifact" displayName: 'Copy New Assembly Build Artifact'
inputs: inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
contents: "**/*.dll" contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/new-release targetFolder: $(System.ArtifactsDirectory)/new-release
cleanTargetFolder: true cleanTargetFolder: true
overWrite: true overWrite: true
flattenFolders: true flattenFolders: true
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: "Download Reference Assembly Build Artifact" displayName: 'Download Reference Assembly Build Artifact'
inputs: inputs:
source: "specific" source: "specific"
artifact: "$(NugetPackageName)" artifact: "$(NugetPackageName)"
@ -70,16 +72,15 @@ jobs:
runBranch: "refs/heads/$(System.PullRequest.TargetBranch)" runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
- task: CopyFiles@2 - task: CopyFiles@2
displayName: "Copy Reference Assembly Build Artifact" displayName: 'Copy Reference Assembly Build Artifact'
inputs: inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: "**/*.dll" contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/current-release targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true cleanTargetFolder: true
overWrite: true overWrite: true
flattenFolders: true flattenFolders: true
# The `--warnings-only` switch will swallow the return code and not emit any errors.
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Execute ABI Compatibility Check Tool' displayName: 'Execute ABI Compatibility Check Tool'
inputs: inputs:

View file

@ -64,28 +64,28 @@ jobs:
arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)' arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: false zipAfterPublish: false
- task: PublishPipelineArtifact@0 - task: PublishPipelineArtifact@1
displayName: 'Publish Artifact Naming' displayName: 'Publish Artifact Naming'
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs: inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll' targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
artifactName: 'Jellyfin.Naming' artifactName: 'Jellyfin.Naming'
- task: PublishPipelineArtifact@0 - task: PublishPipelineArtifact@1
displayName: 'Publish Artifact Controller' displayName: 'Publish Artifact Controller'
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs: inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll' targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
artifactName: 'Jellyfin.Controller' artifactName: 'Jellyfin.Controller'
- task: PublishPipelineArtifact@0 - task: PublishPipelineArtifact@1
displayName: 'Publish Artifact Model' displayName: 'Publish Artifact Model'
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs: inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll' targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
artifactName: 'Jellyfin.Model' artifactName: 'Jellyfin.Model'
- task: PublishPipelineArtifact@0 - task: PublishPipelineArtifact@1
displayName: 'Publish Artifact Common' displayName: 'Publish Artifact Common'
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs: inputs:

View file

@ -0,0 +1,214 @@
jobs:
- job: BuildPackage
displayName: 'Build Packages'
strategy:
matrix:
CentOS.amd64:
BuildConfiguration: centos.amd64
Fedora.amd64:
BuildConfiguration: fedora.amd64
Debian.amd64:
BuildConfiguration: debian.amd64
Debian.arm64:
BuildConfiguration: debian.arm64
Debian.armhf:
BuildConfiguration: debian.armhf
Ubuntu.amd64:
BuildConfiguration: ubuntu.amd64
Ubuntu.arm64:
BuildConfiguration: ubuntu.arm64
Ubuntu.armhf:
BuildConfiguration: ubuntu.armhf
Linux.amd64:
BuildConfiguration: linux.amd64
Windows.amd64:
BuildConfiguration: windows.amd64
MacOS:
BuildConfiguration: macos
Portable:
BuildConfiguration: portable
pool:
vmImage: 'ubuntu-latest'
steps:
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
displayName: 'Build Dockerfile'
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
displayName: 'Run Dockerfile (unstable)'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
displayName: 'Run Dockerfile (stable)'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- task: PublishPipelineArtifact@1
displayName: 'Publish Release'
inputs:
targetPath: '$(Build.SourcesDirectory)/deployment/dist'
artifactName: 'jellyfin-server-$(BuildConfiguration)'
- task: SSH@0
displayName: 'Create target directory on repository server'
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- task: CopyFilesOverSSH@0
displayName: 'Upload artifacts to repository server'
inputs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: BuildDocker
displayName: 'Build Docker'
strategy:
matrix:
amd64:
BuildConfiguration: amd64
arm64:
BuildConfiguration: arm64
armhf:
BuildConfiguration: armhf
pool:
vmImage: 'ubuntu-latest'
variables:
- name: JellyfinVersion
value: 0.0.0
steps:
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- task: Docker@2
displayName: 'Push Unstable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
repository: 'jellyfin/jellyfin-server'
command: buildAndPush
buildContext: '.'
Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
containerRegistry: Docker Hub
tags: |
unstable-$(Build.BuildNumber)-$(BuildConfiguration)
unstable-$(BuildConfiguration)
- task: Docker@2
displayName: 'Push Stable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
repository: 'jellyfin/jellyfin-server'
command: buildAndPush
buildContext: '.'
Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
containerRegistry: Docker Hub
tags: |
stable-$(Build.BuildNumber)-$(BuildConfiguration)
$(JellyfinVersion)-$(BuildConfiguration)
- job: CollectArtifacts
timeoutInMinutes: 20
displayName: 'Collect Artifacts'
continueOnError: true
dependsOn:
- BuildPackage
- BuildDocker
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
steps:
- task: SSH@0
displayName: 'Update Unstable Repository'
continueOnError: true
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
sshEndpoint: repository
runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
- task: SSH@0
displayName: 'Update Stable Repository'
continueOnError: true
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
sshEndpoint: repository
runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
- job: PublishNuget
displayName: 'Publish NuGet packages'
dependsOn:
- BuildPackage
condition: succeeded('BuildPackage')
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: 'pack'
packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
versioningScheme: 'off'
- task: DotNetCoreCLI@2
displayName: 'Build Unstable Nuget packages'
inputs:
command: 'custom'
projects: |
Jellyfin.Data/Jellyfin.Data.csproj
MediaBrowser.Common/MediaBrowser.Common.csproj
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
custom: 'pack'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
- task: PublishBuildArtifacts@1
displayName: 'Publish Nuget packages'
inputs:
pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: Jellyfin Nuget Packages
- task: NuGetAuthenticate@0
displayName: 'Authenticate to stable Nuget feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
nuGetServiceConnections: 'NugetOrg'
- task: NuGetCommand@2
displayName: 'Push Nuget packages to stable feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
nuGetFeedType: 'external'
publishFeedCredentials: 'NugetOrg'
allowPackageConflicts: true # This ignores an error if the version already exists
- task: NuGetAuthenticate@0
displayName: 'Authenticate to unstable Nuget feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- task: NuGetCommand@2
displayName: 'Push Nuget packages to unstable feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it
nuGetFeedType: 'internal'
publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327'
allowPackageConflicts: true # This ignores an error if the version already exists

View file

@ -74,7 +74,6 @@ jobs:
- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
displayName: 'Run ReportGenerator' displayName: 'Run ReportGenerator'
enabled: false
inputs: inputs:
reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml" reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
targetdir: "$(Agent.TempDirectory)/merged/" targetdir: "$(Agent.TempDirectory)/merged/"
@ -84,10 +83,16 @@ jobs:
- task: PublishCodeCoverageResults@1 - task: PublishCodeCoverageResults@1
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
displayName: 'Publish Code Coverage' displayName: 'Publish Code Coverage'
enabled: false
inputs: inputs:
codeCoverageTool: "cobertura" codeCoverageTool: "cobertura"
#summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2 #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml" summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
pathToSources: $(Build.SourcesDirectory) pathToSources: $(Build.SourcesDirectory)
failIfCoverageEmpty: true failIfCoverageEmpty: true
- task: PublishPipelineArtifact@1
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json"
artifactName: 'OpenAPI Spec'

View file

@ -13,13 +13,21 @@ pr:
trigger: trigger:
batch: true batch: true
branches:
include:
- '*'
tags:
include:
- 'v*'
jobs: jobs:
- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}:
- template: azure-pipelines-main.yml - template: azure-pipelines-main.yml
parameters: parameters:
LinuxImage: 'ubuntu-latest' LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: $(RestoreBuildProjects) RestoreBuildProjects: $(RestoreBuildProjects)
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-test.yml - template: azure-pipelines-test.yml
parameters: parameters:
ImageNames: ImageNames:
@ -27,6 +35,7 @@ jobs:
Windows: 'windows-latest' Windows: 'windows-latest'
macOS: 'macos-latest' macOS: 'macos-latest'
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-abi.yml - template: azure-pipelines-abi.yml
parameters: parameters:
Packages: Packages:
@ -43,3 +52,6 @@ jobs:
NugetPackageName: Jellyfin.Common NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll AssemblyFileName: MediaBrowser.Common.dll
LinuxImage: 'ubuntu-latest' LinuxImage: 'ubuntu-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml

3
.gitignore vendored
View file

@ -39,7 +39,6 @@ ProgramData*/
CorePlugins*/ CorePlugins*/
ProgramData-Server*/ ProgramData-Server*/
ProgramData-UI*/ ProgramData-UI*/
MediaBrowser.WebDashboard/jellyfin-web/**
################# #################
## Visual Studio ## Visual Studio
@ -276,4 +275,4 @@ BenchmarkDotNet.Artifacts
# Ignore web artifacts from native builds # Ignore web artifacts from native builds
web/ web/
web-src.* web-src.*
MediaBrowser.WebDashboard/jellyfin-web/ MediaBrowser.WebDashboard/jellyfin-web

26
.vscode/launch.json vendored
View file

@ -1,19 +1,26 @@
{ {
// Use IntelliSense to find out which attributes exist for C# debugging "version": "0.2.0",
// Use hover for the description of the existing attributes "configurations": [
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{ {
"name": ".NET Core Launch (console)", "name": ".NET Core Launch (console)",
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll", "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
"args": [], "args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window "console": "internalConsole",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": ".NET Core Launch (nowebclient)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",
"stopAtEntry": false, "stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart" "internalConsoleOptions": "openOnSessionStart"
@ -24,5 +31,8 @@
"request": "attach", "request": "attach",
"processId": "${command:pickProcess}" "processId": "${command:pickProcess}"
} }
,] ],
"env": {
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
}
} }

9
.vscode/tasks.json vendored
View file

@ -17,9 +17,14 @@
"type": "process", "type": "process",
"args": [ "args": [
"test", "test",
"${workspaceFolder}/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj" "${workspaceFolder}/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj"
], ],
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
} }
] ],
"options": {
"env": {
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
}
}
} }

View file

@ -16,6 +16,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)
- [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)
- [cromefire](https://github.com/cromefire) - [cromefire](https://github.com/cromefire)
@ -56,6 +57,7 @@
- [Larvitar](https://github.com/Larvitar) - [Larvitar](https://github.com/Larvitar)
- [LeoVerto](https://github.com/LeoVerto) - [LeoVerto](https://github.com/LeoVerto)
- [Liggy](https://github.com/Liggy) - [Liggy](https://github.com/Liggy)
- [lmaonator](https://github.com/lmaonator)
- [LogicalPhallacy](https://github.com/LogicalPhallacy) - [LogicalPhallacy](https://github.com/LogicalPhallacy)
- [loli10K](https://github.com/loli10K) - [loli10K](https://github.com/loli10K)
- [lostmypillow](https://github.com/lostmypillow) - [lostmypillow](https://github.com/lostmypillow)
@ -77,6 +79,7 @@
- [nvllsvm](https://github.com/nvllsvm) - [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka) - [nyanmisaka](https://github.com/nyanmisaka)
- [oddstr13](https://github.com/oddstr13) - [oddstr13](https://github.com/oddstr13)
- [orryverducci](https://github.com/orryverducci)
- [petermcneil](https://github.com/petermcneil) - [petermcneil](https://github.com/petermcneil)
- [Phlogi](https://github.com/Phlogi) - [Phlogi](https://github.com/Phlogi)
- [pjeanjean](https://github.com/pjeanjean) - [pjeanjean](https://github.com/pjeanjean)

View file

@ -14,7 +14,7 @@ COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment # because of changes in docker and systemd we need to not build in parallel at the moment
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
FROM debian:buster-slim FROM debian:buster-slim

View file

@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists # Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r RUN find . -type d -name obj | xargs -r rm -r
# Build # Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-arm as qemu FROM multiarch/qemu-user-static:x86_64-arm as qemu

View file

@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists # Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r RUN find . -type d -name obj | xargs -r rm -r
# Build # Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM arm64v8/debian:buster-slim FROM arm64v8/debian:buster-slim

View file

@ -1,383 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Emby.Dlna.Main;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services;
namespace Emby.Dlna.Api
{
[Route("/Dlna/{UuId}/description.xml", "GET", Summary = "Gets dlna server info")]
[Route("/Dlna/{UuId}/description", "GET", Summary = "Gets dlna server info")]
public class GetDescriptionXml
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")]
[Route("/Dlna/{UuId}/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")]
public class GetContentDirectory
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/connectionmanager/connectionmanager.xml", "GET", Summary = "Gets dlna connection manager xml")]
[Route("/Dlna/{UuId}/connectionmanager/connectionmanager", "GET", Summary = "Gets dlna connection manager xml")]
public class GetConnnectionManager
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar.xml", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
[Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
public class GetMediaReceiverRegistrar
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/contentdirectory/control", "POST", Summary = "Processes a control request")]
public class ProcessContentDirectoryControlRequest : IRequiresRequestStream
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
public Stream RequestStream { get; set; }
}
[Route("/Dlna/{UuId}/connectionmanager/control", "POST", Summary = "Processes a control request")]
public class ProcessConnectionManagerControlRequest : IRequiresRequestStream
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
public Stream RequestStream { get; set; }
}
[Route("/Dlna/{UuId}/mediareceiverregistrar/control", "POST", Summary = "Processes a control request")]
public class ProcessMediaReceiverRegistrarControlRequest : IRequiresRequestStream
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
public Stream RequestStream { get; set; }
}
[Route("/Dlna/{UuId}/mediareceiverregistrar/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
[Route("/Dlna/{UuId}/mediareceiverregistrar/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
public class ProcessMediaReceiverRegistrarEventRequest
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/contentdirectory/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
[Route("/Dlna/{UuId}/contentdirectory/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
public class ProcessContentDirectoryEventRequest
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/connectionmanager/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
[Route("/Dlna/{UuId}/connectionmanager/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
public class ProcessConnectionManagerEventRequest
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/icons/{Filename}", "GET", Summary = "Gets a server icon")]
[Route("/Dlna/icons/{Filename}", "GET", Summary = "Gets a server icon")]
public class GetIcon
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string UuId { get; set; }
[ApiMember(Name = "Filename", Description = "The icon filename", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Filename { get; set; }
}
public class DlnaServerService : IService, IRequiresRequest
{
private const string XMLContentType = "text/xml; charset=UTF-8";
private readonly IDlnaManager _dlnaManager;
private readonly IHttpResultFactory _resultFactory;
private readonly IServerConfigurationManager _configurationManager;
public IRequest Request { get; set; }
private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory;
private IConnectionManager ConnectionManager => DlnaEntryPoint.Current.ConnectionManager;
private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar;
public DlnaServerService(
IDlnaManager dlnaManager,
IHttpResultFactory httpResultFactory,
IServerConfigurationManager configurationManager)
{
_dlnaManager = dlnaManager;
_resultFactory = httpResultFactory;
_configurationManager = configurationManager;
}
private string GetHeader(string name)
{
return Request.Headers[name];
}
public object Get(GetDescriptionXml request)
{
var url = Request.AbsoluteUri;
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress);
var cacheLength = TimeSpan.FromDays(1);
var cacheKey = Request.RawUrl.GetMD5();
var bytes = Encoding.UTF8.GetBytes(xml);
return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, XMLContentType, () => Task.FromResult<Stream>(new MemoryStream(bytes)));
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetContentDirectory request)
{
var xml = ContentDirectory.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetMediaReceiverRegistrar request)
{
var xml = MediaReceiverRegistrar.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetConnnectionManager request)
{
var xml = ConnectionManager.GetServiceXml();
return _resultFactory.GetResult(Request, xml, XMLContentType);
}
public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
{
var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
}
public async Task<object> Post(ProcessContentDirectoryControlRequest request)
{
var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
}
public async Task<object> Post(ProcessConnectionManagerControlRequest request)
{
var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false);
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
}
private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
{
var id = GetPathValue(2).ToString();
return service.ProcessControlRequestAsync(new ControlRequest
{
Headers = Request.Headers,
InputXml = requestStream,
TargetServerUuId = id,
RequestedUrl = Request.AbsoluteUri
});
}
// Copied from MediaBrowser.Api/BaseApiService.cs
// TODO: Remove code duplication
/// <summary>
/// Gets the path segment at the specified index.
/// </summary>
/// <param name="index">The index of the path segment.</param>
/// <returns>The path segment at the specified index.</returns>
/// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
/// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
protected internal ReadOnlySpan<char> GetPathValue(int index)
{
static void ThrowIndexOutOfRangeException()
=> throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
static void ThrowInvalidDataException()
=> throw new InvalidDataException("Path doesn't start with the base url.");
ReadOnlySpan<char> path = Request.PathInfo;
// Remove the protocol part from the url
int pos = path.LastIndexOf("://");
if (pos != -1)
{
path = path.Slice(pos + 3);
}
// Remove the query string
pos = path.LastIndexOf('?');
if (pos != -1)
{
path = path.Slice(0, pos);
}
// Remove the domain
pos = path.IndexOf('/');
if (pos != -1)
{
path = path.Slice(pos);
}
// Remove base url
string baseUrl = _configurationManager.Configuration.BaseUrl;
int baseUrlLen = baseUrl.Length;
if (baseUrlLen != 0)
{
if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
{
path = path.Slice(baseUrlLen);
}
else
{
// The path doesn't start with the base url,
// how did we get here?
ThrowInvalidDataException();
}
}
// Remove leading /
path = path.Slice(1);
// Backwards compatibility
const string Emby = "emby/";
if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
{
path = path.Slice(Emby.Length);
}
const string MediaBrowser = "mediabrowser/";
if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
{
path = path.Slice(MediaBrowser.Length);
}
// Skip segments until we are at the right index
for (int i = 0; i < index; i++)
{
pos = path.IndexOf('/');
if (pos == -1)
{
ThrowIndexOutOfRangeException();
}
path = path.Slice(pos + 1);
}
// Remove the rest
pos = path.IndexOf('/');
if (pos != -1)
{
path = path.Slice(0, pos);
}
return path;
}
public object Get(GetIcon request)
{
var contentType = "image/" + Path.GetExtension(request.Filename)
.TrimStart('.')
.ToLowerInvariant();
var cacheLength = TimeSpan.FromDays(365);
var cacheKey = Request.RawUrl.GetMD5();
return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, contentType, () => Task.FromResult(_dlnaManager.GetIcon(request.Filename).Stream));
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Subscribe(ProcessContentDirectoryEventRequest request)
{
return ProcessEventRequest(ContentDirectory);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Subscribe(ProcessConnectionManagerEventRequest request)
{
return ProcessEventRequest(ConnectionManager);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request)
{
return ProcessEventRequest(MediaReceiverRegistrar);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Unsubscribe(ProcessContentDirectoryEventRequest request)
{
return ProcessEventRequest(ContentDirectory);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Unsubscribe(ProcessConnectionManagerEventRequest request)
{
return ProcessEventRequest(ConnectionManager);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Unsubscribe(ProcessMediaReceiverRegistrarEventRequest request)
{
return ProcessEventRequest(MediaReceiverRegistrar);
}
private object ProcessEventRequest(IEventManager eventManager)
{
var subscriptionId = GetHeader("SID");
if (string.Equals(Request.Verb, "SUBSCRIBE", StringComparison.OrdinalIgnoreCase))
{
var notificationType = GetHeader("NT");
var callback = GetHeader("CALLBACK");
var timeoutString = GetHeader("TIMEOUT");
if (string.IsNullOrEmpty(notificationType))
{
return GetSubscriptionResponse(eventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callback));
}
return GetSubscriptionResponse(eventManager.CreateEventSubscription(notificationType, timeoutString, callback));
}
return GetSubscriptionResponse(eventManager.CancelEventSubscription(subscriptionId));
}
private object GetSubscriptionResponse(EventSubscriptionResponse response)
{
return _resultFactory.GetResult(Request, response.Content, response.ContentType, response.Headers);
}
}
}

View file

@ -1,88 +0,0 @@
#pragma warning disable CS1591
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Services;
namespace Emby.Dlna.Api
{
[Route("/Dlna/ProfileInfos", "GET", Summary = "Gets a list of profiles")]
public class GetProfileInfos : IReturn<DeviceProfileInfo[]>
{
}
[Route("/Dlna/Profiles/{Id}", "DELETE", Summary = "Deletes a profile")]
public class DeleteProfile : IReturnVoid
{
[ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
public string Id { get; set; }
}
[Route("/Dlna/Profiles/Default", "GET", Summary = "Gets the default profile")]
public class GetDefaultProfile : IReturn<DeviceProfile>
{
}
[Route("/Dlna/Profiles/{Id}", "GET", Summary = "Gets a single profile")]
public class GetProfile : IReturn<DeviceProfile>
{
[ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Id { get; set; }
}
[Route("/Dlna/Profiles/{Id}", "POST", Summary = "Updates a profile")]
public class UpdateProfile : DeviceProfile, IReturnVoid
{
}
[Route("/Dlna/Profiles", "POST", Summary = "Creates a profile")]
public class CreateProfile : DeviceProfile, IReturnVoid
{
}
[Authenticated(Roles = "Admin")]
public class DlnaService : IService
{
private readonly IDlnaManager _dlnaManager;
public DlnaService(IDlnaManager dlnaManager)
{
_dlnaManager = dlnaManager;
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetProfileInfos request)
{
return _dlnaManager.GetProfileInfos().ToArray();
}
public object Get(GetProfile request)
{
return _dlnaManager.GetProfile(request.Id);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetDefaultProfile request)
{
return _dlnaManager.GetDefaultProfile();
}
public void Delete(DeleteProfile request)
{
_dlnaManager.DeleteProfile(request.Id);
}
public void Post(UpdateProfile request)
{
_dlnaManager.UpdateProfile(request);
}
public void Post(CreateProfile request)
{
_dlnaManager.CreateProfile(request);
}
}
}

View file

@ -13,7 +13,7 @@ namespace Emby.Dlna.Common
public string Name { get; set; } public string Name { get; set; }
public List<Argument> ArgumentList { get; set; } public List<Argument> ArgumentList { get; }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()

View file

@ -1,6 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic;
namespace Emby.Dlna.Common namespace Emby.Dlna.Common
{ {
@ -17,7 +18,7 @@ namespace Emby.Dlna.Common
public bool SendsEvents { get; set; } public bool SendsEvents { get; set; }
public string[] AllowedValues { get; set; } public IReadOnlyList<string> AllowedValues { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()

View file

@ -1,7 +1,6 @@
#nullable enable #nullable enable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic;
using Emby.Dlna.Configuration; using Emby.Dlna.Configuration;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@ -14,19 +13,4 @@ namespace Emby.Dlna
return manager.GetConfiguration<DlnaOptions>("dlna"); return manager.GetConfiguration<DlnaOptions>("dlna");
} }
} }
public class DlnaConfigurationFactory : IConfigurationFactory
{
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new ConfigurationStore[]
{
new ConfigurationStore
{
Key = "dlna",
ConfigurationType = typeof (DlnaOptions)
}
};
}
}
} }

View file

@ -1,30 +1,28 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager namespace Emby.Dlna.ConnectionManager
{ {
public class ConnectionManager : BaseService, IConnectionManager public class ConnectionManagerService : BaseService, IConnectionManager
{ {
private readonly IDlnaManager _dlna; private readonly IDlnaManager _dlna;
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
public ConnectionManager( public ConnectionManagerService(
IDlnaManager dlna, IDlnaManager dlna,
IServerConfigurationManager config, IServerConfigurationManager config,
ILogger<ConnectionManager> logger, ILogger<ConnectionManagerService> logger,
IHttpClient httpClient) IHttpClientFactory httpClientFactory)
: base(logger, httpClient) : base(logger, httpClientFactory)
{ {
_dlna = dlna; _dlna = dlna;
_config = config; _config = config;
_logger = logger;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -39,7 +37,7 @@ namespace Emby.Dlna.ConnectionManager
var profile = _dlna.GetProfile(request.Headers) ?? var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile(); _dlna.GetDefaultProfile();
return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request); return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
} }
} }
} }

View file

@ -44,7 +44,7 @@ namespace Emby.Dlna.ConnectionManager
DataType = "string", DataType = "string",
SendsEvents = false, SendsEvents = false,
AllowedValues = new string[] AllowedValues = new[]
{ {
"OK", "OK",
"ContentFormatMismatch", "ContentFormatMismatch",
@ -67,7 +67,7 @@ namespace Emby.Dlna.ConnectionManager
DataType = "string", DataType = "string",
SendsEvents = false, SendsEvents = false,
AllowedValues = new string[] AllowedValues = new[]
{ {
"Output", "Output",
"Input" "Input"

View file

@ -2,11 +2,11 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
@ -19,7 +19,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ContentDirectory namespace Emby.Dlna.ContentDirectory
{ {
public class ContentDirectory : BaseService, IContentDirectory public class ContentDirectoryService : BaseService, IContentDirectory
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
@ -33,15 +33,15 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ITVSeriesManager _tvSeriesManager; private readonly ITVSeriesManager _tvSeriesManager;
public ContentDirectory( public ContentDirectoryService(
IDlnaManager dlna, IDlnaManager dlna,
IUserDataManager userDataManager, IUserDataManager userDataManager,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IServerConfigurationManager config, IServerConfigurationManager config,
IUserManager userManager, IUserManager userManager,
ILogger<ContentDirectory> logger, ILogger<ContentDirectoryService> logger,
IHttpClient httpClient, IHttpClientFactory httpClient,
ILocalizationManager localization, ILocalizationManager localization,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IUserViewManager userViewManager, IUserViewManager userViewManager,

View file

@ -10,7 +10,8 @@ namespace Emby.Dlna.ContentDirectory
{ {
public string GetXml() public string GetXml()
{ {
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), return new ServiceXmlBuilder().GetXml(
new ServiceActionListBuilder().GetActions(),
GetStateVariables()); GetStateVariables());
} }
@ -101,7 +102,7 @@ namespace Emby.Dlna.ContentDirectory
DataType = "string", DataType = "string",
SendsEvents = false, SendsEvents = false,
AllowedValues = new string[] AllowedValues = new[]
{ {
"BrowseMetadata", "BrowseMetadata",
"BrowseDirectChildren" "BrowseDirectChildren"

View file

@ -11,6 +11,7 @@ using System.Xml;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
@ -39,6 +40,11 @@ namespace Emby.Dlna.ContentDirectory
{ {
public class ControlHandler : BaseControlHandler public class ControlHandler : BaseControlHandler
{ {
private const string NsDc = "http://purl.org/dc/elements/1.1/";
private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataManager; private readonly IUserDataManager _userDataManager;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
@ -46,11 +52,6 @@ namespace Emby.Dlna.ContentDirectory
private readonly IUserViewManager _userViewManager; private readonly IUserViewManager _userViewManager;
private readonly ITVSeriesManager _tvSeriesManager; private readonly ITVSeriesManager _tvSeriesManager;
private const string NS_DC = "http://purl.org/dc/elements/1.1/";
private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private readonly int _systemUpdateId; private readonly int _systemUpdateId;
private readonly DidlBuilder _didlBuilder; private readonly DidlBuilder _didlBuilder;
@ -180,7 +181,11 @@ namespace Emby.Dlna.ContentDirectory
userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks; userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks;
_userDataManager.SaveUserData(_user, item, userdata, UserDataSaveReason.TogglePlayed, _userDataManager.SaveUserData(
_user,
item,
userdata,
UserDataSaveReason.TogglePlayed,
CancellationToken.None); CancellationToken.None);
} }
@ -252,7 +257,7 @@ namespace Emby.Dlna.ContentDirectory
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(GetValueOrDefault(sparams, "Filter", "*"));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
var provided = 0; var provided = 0;
@ -285,18 +290,17 @@ namespace Emby.Dlna.ContentDirectory
using (var writer = XmlWriter.Create(builder, settings)) using (var writer = XmlWriter.Create(builder, settings))
{ {
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
writer.WriteAttributeString("xmlns", "dc", null, NS_DC); writer.WriteAttributeString("xmlns", "dc", null, NsDc);
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
DidlBuilder.WriteXmlRootAttributes(_profile, writer); DidlBuilder.WriteXmlRootAttributes(_profile, writer);
var serverItem = GetItemFromObjectId(id); var serverItem = GetItemFromObjectId(id);
var item = serverItem.Item; var item = serverItem.Item;
if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal)) if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
{ {
totalCount = 1; totalCount = 1;
@ -361,8 +365,8 @@ namespace Emby.Dlna.ContentDirectory
private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
{ {
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", "")); var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
// sort example: dc:title, dc:date // sort example: dc:title, dc:date
@ -396,11 +400,11 @@ namespace Emby.Dlna.ContentDirectory
using (var writer = XmlWriter.Create(builder, settings)) using (var writer = XmlWriter.Create(builder, settings))
{ {
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
writer.WriteAttributeString("xmlns", "dc", null, NS_DC); writer.WriteAttributeString("xmlns", "dc", null, NsDc);
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
DidlBuilder.WriteXmlRootAttributes(_profile, writer); DidlBuilder.WriteXmlRootAttributes(_profile, writer);
@ -782,11 +786,14 @@ namespace Emby.Dlna.ContentDirectory
}) })
.ToArray(); .ToArray();
return ApplyPaging(new QueryResult<ServerItem> return ApplyPaging(
{ new QueryResult<ServerItem>
Items = folders, {
TotalRecordCount = folders.Length Items = folders,
}, startIndex, limit); TotalRecordCount = folders.Length
},
startIndex,
limit);
} }
private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
@ -1134,14 +1141,16 @@ namespace Emby.Dlna.ContentDirectory
{ {
query.OrderBy = Array.Empty<(string, SortOrder)>(); query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(new LatestItemsQuery var items = _userViewManager.GetLatestItems(
{ new LatestItemsQuery
UserId = user.Id, {
Limit = 50, UserId = user.Id,
IncludeItemTypes = new[] { nameof(Audio) }, Limit = 50,
ParentId = parent?.Id ?? Guid.Empty, IncludeItemTypes = new[] { nameof(Audio) },
GroupItems = true ParentId = parent?.Id ?? Guid.Empty,
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); GroupItems = true
},
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items); return ToResult(items);
} }
@ -1150,12 +1159,15 @@ namespace Emby.Dlna.ContentDirectory
{ {
query.OrderBy = Array.Empty<(string, SortOrder)>(); query.OrderBy = Array.Empty<(string, SortOrder)>();
var result = _tvSeriesManager.GetNextUp(new NextUpQuery var result = _tvSeriesManager.GetNextUp(
{ new NextUpQuery
Limit = query.Limit, {
StartIndex = query.StartIndex, Limit = query.Limit,
UserId = query.User.Id StartIndex = query.StartIndex,
}, new[] { parent }, query.DtoOptions); UserId = query.User.Id
},
new[] { parent },
query.DtoOptions);
return ToResult(result); return ToResult(result);
} }
@ -1164,14 +1176,16 @@ namespace Emby.Dlna.ContentDirectory
{ {
query.OrderBy = Array.Empty<(string, SortOrder)>(); query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(new LatestItemsQuery var items = _userViewManager.GetLatestItems(
{ new LatestItemsQuery
UserId = user.Id, {
Limit = 50, UserId = user.Id,
IncludeItemTypes = new[] { typeof(Episode).Name }, Limit = 50,
ParentId = parent == null ? Guid.Empty : parent.Id, IncludeItemTypes = new[] { typeof(Episode).Name },
GroupItems = false ParentId = parent == null ? Guid.Empty : parent.Id,
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); GroupItems = false
},
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items); return ToResult(items);
} }
@ -1182,13 +1196,14 @@ namespace Emby.Dlna.ContentDirectory
var items = _userViewManager.GetLatestItems( var items = _userViewManager.GetLatestItems(
new LatestItemsQuery new LatestItemsQuery
{ {
UserId = user.Id, UserId = user.Id,
Limit = 50, Limit = 50,
IncludeItemTypes = new[] { nameof(Movie) }, IncludeItemTypes = new[] { nameof(Movie) },
ParentId = parent?.Id ?? Guid.Empty, ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true GroupItems = true
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); },
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items); return ToResult(items);
} }
@ -1348,49 +1363,9 @@ namespace Emby.Dlna.ContentDirectory
}; };
} }
Logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id); Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id);
return new ServerItem(_libraryManager.GetUserRootFolder()); return new ServerItem(_libraryManager.GetUserRootFolder());
} }
} }
internal class ServerItem
{
public BaseItem Item { get; set; }
public StubType? StubType { get; set; }
public ServerItem(BaseItem item)
{
Item = item;
if (item is IItemByName && !(item is Folder))
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}
}
}
public enum StubType
{
Folder = 0,
Latest = 2,
Playlists = 3,
Albums = 4,
AlbumArtists = 5,
Artists = 6,
Songs = 7,
Genres = 8,
FavoriteSongs = 9,
FavoriteArtists = 10,
FavoriteAlbums = 11,
ContinueWatching = 12,
Movies = 13,
Collections = 14,
Favorites = 15,
NextUp = 16,
Series = 17,
FavoriteSeries = 18,
FavoriteEpisodes = 19
}
} }

View file

@ -0,0 +1,23 @@
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory
{
internal class ServerItem
{
public ServerItem(BaseItem item)
{
Item = item;
if (item is IItemByName && !(item is Folder))
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}
}
public BaseItem Item { get; set; }
public StubType? StubType { get; set; }
}
}

View file

@ -0,0 +1,28 @@
#pragma warning disable CS1591
#pragma warning disable SA1602
namespace Emby.Dlna.ContentDirectory
{
public enum StubType
{
Folder = 0,
Latest = 2,
Playlists = 3,
Albums = 4,
AlbumArtists = 5,
Artists = 6,
Songs = 7,
Genres = 8,
FavoriteSongs = 9,
FavoriteArtists = 10,
FavoriteAlbums = 11,
ContinueWatching = 12,
Movies = 13,
Collections = 14,
Favorites = 15,
NextUp = 16,
Series = 17,
FavoriteSeries = 18,
FavoriteEpisodes = 19
}
}

View file

@ -7,17 +7,17 @@ namespace Emby.Dlna
{ {
public class ControlRequest public class ControlRequest
{ {
public IHeaderDictionary Headers { get; set; } public ControlRequest(IHeaderDictionary headers)
{
Headers = headers;
}
public IHeaderDictionary Headers { get; }
public Stream InputXml { get; set; } public Stream InputXml { get; set; }
public string TargetServerUuId { get; set; } public string TargetServerUuId { get; set; }
public string RequestedUrl { get; set; } public string RequestedUrl { get; set; }
public ControlRequest()
{
Headers = new HeaderDictionary();
}
} }
} }

View file

@ -11,10 +11,16 @@ namespace Emby.Dlna
Headers = new Dictionary<string, string>(); Headers = new Dictionary<string, string>();
} }
public IDictionary<string, string> Headers { get; set; } public IDictionary<string, string> Headers { get; }
public string Xml { get; set; } public string Xml { get; set; }
public bool IsSuccessful { get; set; } public bool IsSuccessful { get; set; }
/// <inheritdoc />
public override string ToString()
{
return Xml;
}
} }
} }

View file

@ -34,12 +34,12 @@ namespace Emby.Dlna.Didl
{ {
public class DidlBuilder public class DidlBuilder
{ {
private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private const string NsDc = "http://purl.org/dc/elements/1.1/";
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private const string NS_DC = "http://purl.org/dc/elements/1.1/";
private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
private readonly DeviceProfile _profile; private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
@ -100,11 +100,11 @@ namespace Emby.Dlna.Didl
{ {
// writer.WriteStartDocument(); // writer.WriteStartDocument();
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
writer.WriteAttributeString("xmlns", "dc", null, NS_DC); writer.WriteAttributeString("xmlns", "dc", null, NsDc);
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
// didl.SetAttribute("xmlns:sec", NS_SEC); // didl.SetAttribute("xmlns:sec", NS_SEC);
WriteXmlRootAttributes(_profile, writer); WriteXmlRootAttributes(_profile, writer);
@ -147,7 +147,7 @@ namespace Emby.Dlna.Didl
{ {
var clientId = GetClientId(item, null); var clientId = GetClientId(item, null);
writer.WriteStartElement(string.Empty, "item", NS_DIDL); writer.WriteStartElement(string.Empty, "item", NsDidl);
writer.WriteAttributeString("restricted", "1"); writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("id", clientId); writer.WriteAttributeString("id", clientId);
@ -207,7 +207,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(streamInfo.Container, var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
targetWidth, targetWidth,
@ -279,7 +280,7 @@ namespace Emby.Dlna.Didl
} }
else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase))
{ {
writer.WriteStartElement(string.Empty, "res", NS_DIDL); writer.WriteStartElement(string.Empty, "res", NsDidl);
writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*"); writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*");
@ -288,7 +289,7 @@ namespace Emby.Dlna.Didl
} }
else else
{ {
writer.WriteStartElement(string.Empty, "res", NS_DIDL); writer.WriteStartElement(string.Empty, "res", NsDidl);
var protocolInfo = string.Format( var protocolInfo = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"http-get:*:text/{0}:*", "http-get:*:text/{0}:*",
@ -304,7 +305,7 @@ namespace Emby.Dlna.Didl
private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo) private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo)
{ {
writer.WriteStartElement(string.Empty, "res", NS_DIDL); writer.WriteStartElement(string.Empty, "res", NsDidl);
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken)); var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
@ -364,7 +365,8 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture)); writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
} }
var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container, var mediaProfile = _profile.GetVideoMediaProfile(
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate, streamInfo.TargetAudioBitrate,
@ -525,7 +527,7 @@ namespace Emby.Dlna.Didl
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
{ {
writer.WriteStartElement(string.Empty, "res", NS_DIDL); writer.WriteStartElement(string.Empty, "res", NsDidl);
if (streamInfo == null) if (streamInfo == null)
{ {
@ -582,7 +584,8 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture)); writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
} }
var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container, var mediaProfile = _profile.GetAudioMediaProfile(
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
targetChannels, targetChannels,
targetAudioBitrate, targetAudioBitrate,
@ -595,7 +598,8 @@ namespace Emby.Dlna.Didl
? MimeTypes.GetMimeType(filename) ? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType; : mediaProfile.MimeType;
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container, var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
targetAudioBitrate, targetAudioBitrate,
targetSampleRate, targetSampleRate,
@ -626,7 +630,7 @@ namespace Emby.Dlna.Didl
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null) public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
{ {
writer.WriteStartElement(string.Empty, "container", NS_DIDL); writer.WriteStartElement(string.Empty, "container", NsDidl);
writer.WriteAttributeString("restricted", "1"); writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("searchable", "1"); writer.WriteAttributeString("searchable", "1");
@ -713,7 +717,7 @@ namespace Emby.Dlna.Didl
// MediaMonkey for example won't display content without a title // MediaMonkey for example won't display content without a title
// if (filter.Contains("dc:title")) // if (filter.Contains("dc:title"))
{ {
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC); AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NsDc);
} }
WriteObjectClass(writer, item, itemStubType); WriteObjectClass(writer, item, itemStubType);
@ -722,7 +726,7 @@ namespace Emby.Dlna.Didl
{ {
if (item.PremiereDate.HasValue) if (item.PremiereDate.HasValue)
{ {
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NS_DC); AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
} }
} }
@ -730,13 +734,13 @@ namespace Emby.Dlna.Didl
{ {
foreach (var genre in item.Genres) foreach (var genre in item.Genres)
{ {
AddValue(writer, "upnp", "genre", genre, NS_UPNP); AddValue(writer, "upnp", "genre", genre, NsUpnp);
} }
} }
foreach (var studio in item.Studios) foreach (var studio in item.Studios)
{ {
AddValue(writer, "upnp", "publisher", studio, NS_UPNP); AddValue(writer, "upnp", "publisher", studio, NsUpnp);
} }
if (!(item is Folder)) if (!(item is Folder))
@ -747,28 +751,29 @@ namespace Emby.Dlna.Didl
if (!string.IsNullOrWhiteSpace(desc)) if (!string.IsNullOrWhiteSpace(desc))
{ {
AddValue(writer, "dc", "description", desc, NS_DC); AddValue(writer, "dc", "description", desc, NsDc);
} }
} }
// if (filter.Contains("upnp:longDescription")) // if (filter.Contains("upnp:longDescription"))
//{ // {
// if (!string.IsNullOrWhiteSpace(item.Overview)) // if (!string.IsNullOrWhiteSpace(item.Overview))
// { // {
// AddValue(writer, "upnp", "longDescription", item.Overview, NS_UPNP); // AddValue(writer, "upnp", "longDescription", item.Overview, NsUpnp);
// } // }
//} // }
} }
if (!string.IsNullOrEmpty(item.OfficialRating)) if (!string.IsNullOrEmpty(item.OfficialRating))
{ {
if (filter.Contains("dc:rating")) if (filter.Contains("dc:rating"))
{ {
AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC); AddValue(writer, "dc", "rating", item.OfficialRating, NsDc);
} }
if (filter.Contains("upnp:rating")) if (filter.Contains("upnp:rating"))
{ {
AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP); AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp);
} }
} }
@ -780,7 +785,7 @@ namespace Emby.Dlna.Didl
// More types here // More types here
// http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs // http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs
writer.WriteStartElement("upnp", "class", NS_UPNP); writer.WriteStartElement("upnp", "class", NsUpnp);
if (item.IsDisplayedAsFolder || stubType.HasValue) if (item.IsDisplayedAsFolder || stubType.HasValue)
{ {
@ -881,7 +886,7 @@ namespace Emby.Dlna.Didl
var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
?? PersonType.Actor; ?? PersonType.Actor;
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP); AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
} }
} }
@ -895,8 +900,8 @@ namespace Emby.Dlna.Didl
{ {
foreach (var artist in hasArtists.Artists) foreach (var artist in hasArtists.Artists)
{ {
AddValue(writer, "upnp", "artist", artist, NS_UPNP); AddValue(writer, "upnp", "artist", artist, NsUpnp);
AddValue(writer, "dc", "creator", artist, NS_DC); AddValue(writer, "dc", "creator", artist, NsDc);
// If it doesn't support album artists (musicvideo), then tag as both // If it doesn't support album artists (musicvideo), then tag as both
if (hasAlbumArtists == null) if (hasAlbumArtists == null)
@ -916,16 +921,16 @@ namespace Emby.Dlna.Didl
if (!string.IsNullOrWhiteSpace(item.Album)) if (!string.IsNullOrWhiteSpace(item.Album))
{ {
AddValue(writer, "upnp", "album", item.Album, NS_UPNP); AddValue(writer, "upnp", "album", item.Album, NsUpnp);
} }
if (item.IndexNumber.HasValue) if (item.IndexNumber.HasValue)
{ {
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
if (item is Episode) if (item is Episode)
{ {
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
} }
} }
} }
@ -934,7 +939,7 @@ namespace Emby.Dlna.Didl
{ {
try try
{ {
writer.WriteStartElement("upnp", "artist", NS_UPNP); writer.WriteStartElement("upnp", "artist", NsUpnp);
writer.WriteAttributeString("role", "AlbumArtist"); writer.WriteAttributeString("role", "AlbumArtist");
writer.WriteString(name); writer.WriteString(name);
@ -943,7 +948,7 @@ namespace Emby.Dlna.Didl
} }
catch (XmlException ex) catch (XmlException ex)
{ {
_logger.LogError(ex, "Error adding xml value: {value}", name); _logger.LogError(ex, "Error adding xml value: {Value}", name);
} }
} }
@ -955,7 +960,7 @@ namespace Emby.Dlna.Didl
} }
catch (XmlException ex) catch (XmlException ex)
{ {
_logger.LogError(ex, "Error adding xml value: {value}", value); _logger.LogError(ex, "Error adding xml value: {Value}", value);
} }
} }
@ -970,14 +975,14 @@ namespace Emby.Dlna.Didl
var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg"); var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP); writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn); writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
writer.WriteString(albumartUrlInfo.Url); writer.WriteString(albumartUrlInfo.url);
writer.WriteFullEndElement(); writer.WriteFullEndElement();
// TOOD: Remove these default values // TOOD: 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", NS_UPNP, iconUrlInfo.Url); writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
if (!_profile.EnableAlbumArtInDidl) if (!_profile.EnableAlbumArtInDidl)
{ {
@ -1020,12 +1025,12 @@ namespace Emby.Dlna.Didl
var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format); var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format);
writer.WriteStartElement(string.Empty, "res", NS_DIDL); writer.WriteStartElement(string.Empty, "res", NsDidl);
// Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail // Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail
// rather than using a larger one when available // rather than using a larger one when available
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 = new ContentFeatureBuilder(_profile)
.BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn); .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
@ -1042,7 +1047,7 @@ namespace Emby.Dlna.Didl
"resolution", "resolution",
string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height)); string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
writer.WriteString(albumartUrlInfo.Url); writer.WriteString(albumartUrlInfo.url);
writer.WriteFullEndElement(); writer.WriteFullEndElement();
} }
@ -1138,7 +1143,6 @@ namespace Emby.Dlna.Didl
if (width == 0 || height == 0) if (width == 0 || height == 0)
{ {
// _imageProcessor.GetImageSize(item, imageInfo);
width = null; width = null;
height = null; height = null;
} }
@ -1148,18 +1152,6 @@ namespace Emby.Dlna.Didl
height = null; height = null;
} }
// try
//{
// var size = _imageProcessor.GetImageSize(imageInfo);
// width = size.Width;
// height = size.Height;
//}
// catch
//{
//}
var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty) var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty)
.TrimStart('.') .TrimStart('.')
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase); .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
@ -1176,30 +1168,6 @@ namespace Emby.Dlna.Didl
}; };
} }
private class ImageDownloadInfo
{
internal Guid ItemId;
internal string ImageTag;
internal ImageType Type;
internal int? Width;
internal int? Height;
internal bool IsDirectStream;
internal string Format;
internal ItemImageInfo ItemImageInfo;
}
private class ImageUrlInfo
{
internal string Url;
internal int? Width;
internal int? Height;
}
public static string GetClientId(BaseItem item, StubType? stubType) public static string GetClientId(BaseItem item, StubType? stubType)
{ {
return GetClientId(item.Id, stubType); return GetClientId(item.Id, stubType);
@ -1217,7 +1185,7 @@ namespace Emby.Dlna.Didl
return id; return id;
} }
private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format) private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
{ {
var url = string.Format( var url = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@ -1255,12 +1223,26 @@ namespace Emby.Dlna.Didl
// just lie // just lie
info.IsDirectStream = true; info.IsDirectStream = true;
return new ImageUrlInfo return (url, width, height);
{ }
Url = url,
Width = width, private class ImageDownloadInfo
Height = height {
}; internal Guid ItemId { get; set; }
internal string ImageTag { get; set; }
internal ImageType Type { get; set; }
internal int? Width { get; set; }
internal int? Height { get; set; }
internal bool IsDirectStream { get; set; }
internal string Format { get; set; }
internal ItemImageInfo ItemImageInfo { get; set; }
} }
} }
} }

View file

@ -23,9 +23,7 @@ namespace Emby.Dlna.Didl
public bool Contains(string field) public bool Contains(string field)
{ {
// Don't bother with this. Some clients (media monkey) use the filter and then don't display very well when very little data comes back. return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase));
return true;
// return _all || ListHelper.ContainsIgnoreCase(_fields, field);
} }
} }
} }

View file

@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable CA1305
using System; using System;
using System.IO; using System.IO;
@ -29,7 +30,6 @@ namespace Emby.Dlna.Didl
{ {
} }
public StringWriterWithEncoding(Encoding encoding) public StringWriterWithEncoding(Encoding encoding)
{ {
_encoding = encoding; _encoding = encoding;

View file

@ -0,0 +1,24 @@
#nullable enable
#pragma warning disable CS1591
using System.Collections.Generic;
using Emby.Dlna.Configuration;
using MediaBrowser.Common.Configuration;
namespace Emby.Dlna
{
public class DlnaConfigurationFactory : IConfigurationFactory
{
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new[]
{
new ConfigurationStore
{
Key = "dlna",
ConfigurationType = typeof(DlnaOptions)
}
};
}
}
}

View file

@ -54,11 +54,15 @@ namespace Emby.Dlna
_appHost = appHost; _appHost = appHost;
} }
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
public async Task InitProfilesAsync() public async Task InitProfilesAsync()
{ {
try try
{ {
await ExtractSystemProfilesAsync(); await ExtractSystemProfilesAsync().ConfigureAwait(false);
LoadProfiles(); LoadProfiles();
} }
catch (Exception ex) catch (Exception ex)
@ -122,15 +126,15 @@ 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.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty)); builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
_logger.LogInformation(builder.ToString()); _logger.LogInformation(builder.ToString());
} }
@ -240,7 +244,7 @@ namespace Emby.Dlna
} }
else else
{ {
var headerString = string.Join(", ", headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray()); var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
_logger.LogDebug("No matching device profile found. {0}", headerString); _logger.LogDebug("No matching device profile found. {0}", headerString);
} }
@ -280,10 +284,6 @@ namespace Emby.Dlna
return false; return false;
} }
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type) private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
{ {
try try
@ -387,7 +387,7 @@ namespace Emby.Dlna
foreach (var name in _assembly.GetManifestResourceNames()) foreach (var name in _assembly.GetManifestResourceNames())
{ {
if (!name.StartsWith(namespaceName)) if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
{ {
continue; continue;
} }
@ -406,7 +406,7 @@ namespace Emby.Dlna
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{ {
await stream.CopyToAsync(fileStream); await stream.CopyToAsync(fileStream).ConfigureAwait(false);
} }
} }
} }
@ -495,8 +495,8 @@ namespace Emby.Dlna
/// Recreates the object using serialization, to ensure it's not a subclass. /// Recreates the object using serialization, to ensure it's not a subclass.
/// If it's a subclass it may not serlialize properly to xml (different root element tag name). /// If it's a subclass it may not serlialize properly to xml (different root element tag name).
/// </summary> /// </summary>
/// <param name="profile"></param> /// <param name="profile">The device profile.</param>
/// <returns></returns> /// <returns>The reserialized device profile.</returns>
private DeviceProfile ReserializeProfile(DeviceProfile profile) private DeviceProfile ReserializeProfile(DeviceProfile profile)
{ {
if (profile.GetType() == typeof(DeviceProfile)) if (profile.GetType() == typeof(DeviceProfile))
@ -509,13 +509,6 @@ namespace Emby.Dlna
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json); return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
} }
class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
internal string Path { get; set; }
}
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{ {
var profile = GetProfile(headers) ?? var profile = GetProfile(headers) ??
@ -540,7 +533,15 @@ namespace Emby.Dlna
Stream = _assembly.GetManifestResourceStream(resource) Stream = _assembly.GetManifestResourceStream(resource)
}; };
} }
private class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
internal string Path { get; set; }
}
} }
/* /*
class DlnaProfileEntryPoint : IServerEntryPoint class DlnaProfileEntryPoint : IServerEntryPoint
{ {

View file

@ -20,7 +20,7 @@
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->
@ -80,6 +80,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.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

@ -15,6 +15,6 @@ namespace Emby.Dlna
public string ContentType { get; set; } public string ContentType { get; set; }
public Dictionary<string, string> Headers { get; set; } public Dictionary<string, string> Headers { get; }
} }
} }

View file

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@ -14,17 +15,19 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Eventing namespace Emby.Dlna.Eventing
{ {
public class EventManager : IEventManager public class DlnaEventManager : IDlnaEventManager
{ {
private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions = private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase); new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IHttpClient _httpClient; private readonly IHttpClientFactory _httpClientFactory;
public EventManager(ILogger logger, IHttpClient httpClient) private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
{ {
_httpClient = httpClient; _httpClientFactory = httpClientFactory;
_logger = logger; _logger = logger;
} }
@ -58,7 +61,8 @@ namespace Emby.Dlna.Eventing
var timeout = ParseTimeout(requestedTimeoutString) ?? 300; var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
_logger.LogDebug("Creating event subscription for {0} with timeout of {1} to {2}", _logger.LogDebug(
"Creating event subscription for {0} with timeout of {1} to {2}",
notificationType, notificationType,
timeout, timeout,
callbackUrl); callbackUrl);
@ -94,7 +98,7 @@ namespace Emby.Dlna.Eventing
{ {
_logger.LogDebug("Cancelling event subscription {0}", subscriptionId); _logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
_subscriptions.TryRemove(subscriptionId, out EventSubscription sub); _subscriptions.TryRemove(subscriptionId, out _);
return new EventSubscriptionResponse return new EventSubscriptionResponse
{ {
@ -103,7 +107,6 @@ namespace Emby.Dlna.Eventing
}; };
} }
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds) private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
{ {
var response = new EventSubscriptionResponse var response = new EventSubscriptionResponse
@ -152,33 +155,30 @@ namespace Emby.Dlna.Eventing
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">"); builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
foreach (var key in stateVariables.Keys) foreach (var key in stateVariables.Keys)
{ {
builder.Append("<e:property>"); builder.Append("<e:property>")
builder.Append("<" + key + ">"); .Append('<')
builder.Append(stateVariables[key]); .Append(key)
builder.Append("</" + key + ">"); .Append('>')
builder.Append("</e:property>"); .Append(stateVariables[key])
.Append("</")
.Append(key)
.Append('>')
.Append("</e:property>");
} }
builder.Append("</e:propertyset>"); builder.Append("</e:propertyset>");
var options = new HttpRequestOptions using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
{ options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
RequestContent = builder.ToString(), options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
RequestContentType = "text/xml", options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
Url = subscription.CallbackUrl, options.Headers.TryAddWithoutValidation("SID", subscription.Id);
BufferContent = false options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
};
options.RequestHeaders.Add("NT", subscription.NotificationType);
options.RequestHeaders.Add("NTS", "upnp:propchange");
options.RequestHeaders.Add("SID", subscription.Id);
options.RequestHeaders.Add("SEQ", subscription.TriggerCount.ToString(_usCulture));
try try
{ {
using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false)) using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
{ .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {

View file

@ -2,7 +2,7 @@
namespace Emby.Dlna namespace Emby.Dlna
{ {
public interface IConnectionManager : IEventManager, IUpnpService public interface IConnectionManager : IDlnaEventManager, IUpnpService
{ {
} }
} }

View file

@ -2,7 +2,7 @@
namespace Emby.Dlna namespace Emby.Dlna
{ {
public interface IContentDirectory : IEventManager, IUpnpService public interface IContentDirectory : IDlnaEventManager, IUpnpService
{ {
} }
} }

View file

@ -2,22 +2,32 @@
namespace Emby.Dlna namespace Emby.Dlna
{ {
public interface IEventManager public interface IDlnaEventManager
{ {
/// <summary> /// <summary>
/// Cancels the event subscription. /// Cancels the event subscription.
/// </summary> /// </summary>
/// <param name="subscriptionId">The subscription identifier.</param> /// <param name="subscriptionId">The subscription identifier.</param>
/// <returns>The response.</returns>
EventSubscriptionResponse CancelEventSubscription(string subscriptionId); EventSubscriptionResponse CancelEventSubscription(string subscriptionId);
/// <summary> /// <summary>
/// Renews the event subscription. /// Renews the event subscription.
/// </summary> /// </summary>
/// <param name="subscriptionId">The subscription identifier.</param>
/// <param name="notificationType">The notification type.</param>
/// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
/// <param name="callbackUrl">The callback url.</param>
/// <returns>The response.</returns>
EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl); EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
/// <summary> /// <summary>
/// Creates the event subscription. /// Creates the event subscription.
/// </summary> /// </summary>
/// <param name="notificationType">The notification type.</param>
/// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
/// <param name="callbackUrl">The callback url.</param>
/// <returns>The response.</returns>
EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl); EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
} }
} }

View file

@ -2,7 +2,7 @@
namespace Emby.Dlna namespace Emby.Dlna
{ {
public interface IMediaReceiverRegistrar : IEventManager, IUpnpService public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
{ {
} }
} }

View file

@ -2,6 +2,7 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -30,15 +31,13 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Dlna.Main namespace Emby.Dlna.Main
{ {
public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
{ {
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly ILogger<DlnaEntryPoint> _logger; private readonly ILogger<DlnaEntryPoint> _logger;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private PlayToManager _manager;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly IHttpClient _httpClient; private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager; private readonly IDlnaManager _dlnaManager;
@ -47,30 +46,23 @@ namespace Emby.Dlna.Main
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery; private readonly IDeviceDiscovery _deviceDiscovery;
private SsdpDevicePublisher _Publisher;
private readonly ISocketFactory _socketFactory; private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
private PlayToManager _manager;
private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer; private ISsdpCommunicationsServer _communicationsServer;
internal IContentDirectory ContentDirectory { get; private set; } private bool _disposed;
internal IConnectionManager ConnectionManager { get; private set; }
internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public static DlnaEntryPoint Current;
public DlnaEntryPoint( public DlnaEntryPoint(
IServerConfigurationManager config, IServerConfigurationManager config,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IServerApplicationHost appHost, IServerApplicationHost appHost,
ISessionManager sessionManager, ISessionManager sessionManager,
IHttpClient httpClient, IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IUserManager userManager, IUserManager userManager,
IDlnaManager dlnaManager, IDlnaManager dlnaManager,
@ -88,7 +80,7 @@ namespace Emby.Dlna.Main
_config = config; _config = config;
_appHost = appHost; _appHost = appHost;
_sessionManager = sessionManager; _sessionManager = sessionManager;
_httpClient = httpClient; _httpClientFactory = httpClientFactory;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userManager = userManager; _userManager = userManager;
_dlnaManager = dlnaManager; _dlnaManager = dlnaManager;
@ -102,34 +94,42 @@ namespace Emby.Dlna.Main
_networkManager = networkManager; _networkManager = networkManager;
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
ContentDirectory = new ContentDirectory.ContentDirectory( ContentDirectory = new ContentDirectory.ContentDirectoryService(
dlnaManager, dlnaManager,
userDataManager, userDataManager,
imageProcessor, imageProcessor,
libraryManager, libraryManager,
config, config,
userManager, userManager,
loggerFactory.CreateLogger<ContentDirectory.ContentDirectory>(), loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
httpClient, httpClientFactory,
localizationManager, localizationManager,
mediaSourceManager, mediaSourceManager,
userViewManager, userViewManager,
mediaEncoder, mediaEncoder,
tvSeriesManager); tvSeriesManager);
ConnectionManager = new ConnectionManager.ConnectionManager( ConnectionManager = new ConnectionManager.ConnectionManagerService(
dlnaManager, dlnaManager,
config, config,
loggerFactory.CreateLogger<ConnectionManager.ConnectionManager>(), loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
httpClient); httpClientFactory);
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar( MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrar>(), loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
httpClient, httpClientFactory,
config); config);
Current = this; Current = this;
} }
public static DlnaEntryPoint Current { get; private set; }
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public async Task RunAsync() public async Task RunAsync()
{ {
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
@ -181,7 +181,7 @@ namespace Emby.Dlna.Main
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows || var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
OperatingSystem.Id == OperatingSystemId.Linux; OperatingSystem.Id == OperatingSystemId.Linux;
_communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding) _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{ {
IsShared = true IsShared = true
}; };
@ -232,20 +232,22 @@ namespace Emby.Dlna.Main
return; return;
} }
if (_Publisher != null) if (_publisher != null)
{ {
return; return;
} }
try try
{ {
_Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost); _publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
_Publisher.LogFunction = LogMessage; {
_Publisher.SupportPnpRootDevice = false; LogFunction = LogMessage,
SupportPnpRootDevice = false
};
await RegisterServerEndpoints().ConfigureAwait(false); await RegisterServerEndpoints().ConfigureAwait(false);
_Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -267,6 +269,12 @@ namespace Emby.Dlna.Main
continue; continue;
} }
// Limit to LAN addresses only
if (!_networkManager.IsAddressInSubnets(address, true, true))
{
continue;
}
var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
@ -288,13 +296,13 @@ namespace Emby.Dlna.Main
}; };
SetProperies(device, fullService); SetProperies(device, fullService);
_Publisher.AddDevice(device); _publisher.AddDevice(device);
var embeddedDevices = new[] var embeddedDevices = new[]
{ {
"urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ContentDirectory:1",
"urn:schemas-upnp-org:service:ConnectionManager:1", "urn:schemas-upnp-org:service:ConnectionManager:1",
//"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
}; };
foreach (var subDevice in embeddedDevices) foreach (var subDevice in embeddedDevices)
@ -326,7 +334,7 @@ namespace Emby.Dlna.Main
private void SetProperies(SsdpDevice device, string fullDeviceType) private void SetProperies(SsdpDevice device, string fullDeviceType)
{ {
var service = fullDeviceType.Replace("urn:", string.Empty).Replace(":1", string.Empty); var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
var serviceParts = service.Split(':'); var serviceParts = service.Split(':');
@ -337,7 +345,6 @@ namespace Emby.Dlna.Main
device.DeviceType = serviceParts[2]; device.DeviceType = serviceParts[2];
} }
private readonly object _syncLock = new object();
private void StartPlayToManager() private void StartPlayToManager()
{ {
lock (_syncLock) lock (_syncLock)
@ -358,7 +365,7 @@ namespace Emby.Dlna.Main
_appHost, _appHost,
_imageProcessor, _imageProcessor,
_deviceDiscovery, _deviceDiscovery,
_httpClient, _httpClientFactory,
_config, _config,
_userDataManager, _userDataManager,
_localization, _localization,
@ -395,8 +402,24 @@ namespace Emby.Dlna.Main
} }
} }
public void DisposeDevicePublisher()
{
if (_publisher != null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_publisher.Dispose();
_publisher = null;
}
}
/// <inheritdoc />
public void Dispose() public void Dispose()
{ {
if (_disposed)
{
return;
}
DisposeDevicePublisher(); DisposeDevicePublisher();
DisposePlayToManager(); DisposePlayToManager();
DisposeDeviceDiscovery(); DisposeDeviceDiscovery();
@ -412,16 +435,8 @@ namespace Emby.Dlna.Main
ConnectionManager = null; ConnectionManager = null;
MediaReceiverRegistrar = null; MediaReceiverRegistrar = null;
Current = null; Current = null;
}
public void DisposeDevicePublisher() _disposed = true;
{
if (_Publisher != null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_Publisher.Dispose();
_Publisher = null;
}
} }
} }
} }

View file

@ -1,22 +1,22 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
{ {
public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
{ {
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
public MediaReceiverRegistrar( public MediaReceiverRegistrarService(
ILogger<MediaReceiverRegistrar> logger, ILogger<MediaReceiverRegistrarService> logger,
IHttpClient httpClient, IHttpClientFactory httpClientFactory,
IServerConfigurationManager config) IServerConfigurationManager config)
: base(logger, httpClient) : base(logger, httpClientFactory)
{ {
_config = config; _config = config;
} }

View file

@ -10,7 +10,8 @@ namespace Emby.Dlna.MediaReceiverRegistrar
{ {
public string GetXml() public string GetXml()
{ {
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), return new ServiceXmlBuilder().GetXml(
new ServiceActionListBuilder().GetActions(),
GetStateVariables()); GetStateVariables());
} }

View file

@ -4,12 +4,13 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Security;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Server;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -19,15 +20,40 @@ namespace Emby.Dlna.PlayTo
{ {
public class Device : IDisposable public class Device : IDisposable
{ {
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
private readonly object _timerLock = new object();
private Timer _timer; private Timer _timer;
private int _muteVol;
private int _volume;
private DateTime _lastVolumeRefresh;
private bool _volumeRefreshActive;
private int _connectFailureCount;
private bool _disposed;
public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger)
{
Properties = deviceProperties;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
public event EventHandler<MediaChangedEventArgs> MediaChanged;
public DeviceInfo Properties { get; set; } public DeviceInfo Properties { get; set; }
private int _muteVol;
public bool IsMuted { get; set; } public bool IsMuted { get; set; }
private int _volume;
public int Volume public int Volume
{ {
get get
@ -43,29 +69,21 @@ namespace Emby.Dlna.PlayTo
public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0); public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0);
public TRANSPORTSTATE TransportState { get; private set; } public TransportState TransportState { get; private set; }
public bool IsPlaying => TransportState == TRANSPORTSTATE.PLAYING; public bool IsPlaying => TransportState == TransportState.Playing;
public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK; public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback;
public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED; public bool IsStopped => TransportState == TransportState.Stopped;
private readonly IHttpClient _httpClient;
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
public Action OnDeviceUnavailable { get; set; } public Action OnDeviceUnavailable { get; set; }
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config) private TransportCommands AvCommands { get; set; }
{
Properties = deviceProperties; private TransportCommands RendererCommands { get; set; }
_httpClient = httpClient;
_logger = logger; public UBaseObject CurrentMediaInfo { get; private set; }
_config = config;
}
public void Start() public void Start()
{ {
@ -73,8 +91,6 @@ namespace Emby.Dlna.PlayTo
_timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite); _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
} }
private DateTime _lastVolumeRefresh;
private bool _volumeRefreshActive;
private Task RefreshVolumeIfNeeded() private Task RefreshVolumeIfNeeded()
{ {
if (_volumeRefreshActive if (_volumeRefreshActive
@ -105,7 +121,6 @@ namespace Emby.Dlna.PlayTo
} }
} }
private readonly object _timerLock = new object();
private void RestartTimer(bool immediate = false) private void RestartTimer(bool immediate = false)
{ {
lock (_timerLock) lock (_timerLock)
@ -222,7 +237,7 @@ 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(_httpClient).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))
.ConfigureAwait(false); .ConfigureAwait(false);
IsMuted = mute; IsMuted = mute;
@ -233,6 +248,9 @@ namespace Emby.Dlna.PlayTo
/// <summary> /// <summary>
/// Sets volume on a scale of 0-100. /// Sets volume on a scale of 0-100.
/// </summary> /// </summary>
/// <param name="value">The volume on a scale of 0-100.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task SetVolume(int value, CancellationToken cancellationToken) public async Task SetVolume(int value, CancellationToken cancellationToken)
{ {
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
@ -254,7 +272,7 @@ namespace Emby.Dlna.PlayTo
// Remote control will perform better // Remote control will perform better
Volume = value; Volume = value;
await new SsdpHttpClient(_httpClient).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))
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@ -275,7 +293,7 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service"); throw new InvalidOperationException("Unable to find service");
} }
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{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"))
.ConfigureAwait(false); .ConfigureAwait(false);
RestartTimer(true); RestartTimer(true);
@ -285,7 +303,7 @@ namespace Emby.Dlna.PlayTo
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
url = url.Replace("&", "&amp;"); url = url.Replace("&", "&amp;", StringComparison.Ordinal);
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header); _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
@ -297,8 +315,8 @@ namespace Emby.Dlna.PlayTo
var dictionary = new Dictionary<string, string> var dictionary = new Dictionary<string, string>
{ {
{"CurrentURI", url}, { "CurrentURI", url },
{"CurrentURIMetaData", CreateDidlMeta(metaData)} { "CurrentURIMetaData", CreateDidlMeta(metaData) }
}; };
var service = GetAvTransportService(); var service = GetAvTransportService();
@ -309,7 +327,7 @@ 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(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header) await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
.ConfigureAwait(false); .ConfigureAwait(false);
await Task.Delay(50).ConfigureAwait(false); await Task.Delay(50).ConfigureAwait(false);
@ -334,7 +352,7 @@ namespace Emby.Dlna.PlayTo
return string.Empty; return string.Empty;
} }
return DescriptionXmlBuilder.Escape(value); return SecurityElement.Escape(value);
} }
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken) private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
@ -351,7 +369,7 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service"); throw new InvalidOperationException("Unable to find service");
} }
return new SsdpHttpClient(_httpClient).SendCommandAsync( return new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -380,7 +398,7 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService(); var service = GetAvTransportService();
await new SsdpHttpClient(_httpClient).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))
.ConfigureAwait(false); .ConfigureAwait(false);
RestartTimer(true); RestartTimer(true);
@ -398,16 +416,14 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService(); var service = GetAvTransportService();
await new SsdpHttpClient(_httpClient).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))
.ConfigureAwait(false); .ConfigureAwait(false);
TransportState = TRANSPORTSTATE.PAUSED; TransportState = TransportState.Paused;
RestartTimer(true); RestartTimer(true);
} }
private int _connectFailureCount;
private async void TimerCallback(object sender) private async void TimerCallback(object sender)
{ {
if (_disposed) if (_disposed)
@ -436,7 +452,7 @@ namespace Emby.Dlna.PlayTo
if (transportState.HasValue) if (transportState.HasValue)
{ {
// If we're not playing anything no need to get additional data // If we're not playing anything no need to get additional data
if (transportState.Value == TRANSPORTSTATE.STOPPED) if (transportState.Value == TransportState.Stopped)
{ {
UpdateMediaInfo(null, transportState.Value); UpdateMediaInfo(null, transportState.Value);
} }
@ -465,7 +481,7 @@ namespace Emby.Dlna.PlayTo
} }
// If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
if (transportState.Value == TRANSPORTSTATE.STOPPED) if (transportState.Value == TransportState.Stopped)
{ {
RestartTimerInactive(); RestartTimerInactive();
} }
@ -527,7 +543,7 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -539,7 +555,7 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null); var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
var volumeValue = volume?.Value; var volumeValue = volume?.Value;
if (string.IsNullOrWhiteSpace(volumeValue)) if (string.IsNullOrWhiteSpace(volumeValue))
@ -577,7 +593,7 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -589,14 +605,14 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse") var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse")
.Select(i => i.Element("CurrentMute")) .Select(i => i.Element("CurrentMute"))
.FirstOrDefault(i => i != null); .FirstOrDefault(i => i != null);
IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase); IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase);
} }
private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken) private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{ {
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
if (command == null) if (command == null)
@ -610,7 +626,7 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -623,12 +639,12 @@ namespace Emby.Dlna.PlayTo
} }
var transportState = var transportState =
result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null); result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
var transportStateValue = transportState?.Value; var transportStateValue = transportState?.Value;
if (transportStateValue != null if (transportStateValue != null
&& Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state)) && Enum.TryParse(transportStateValue, true, out TransportState state))
{ {
return state; return state;
} }
@ -636,7 +652,7 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
private async Task<uBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{ {
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
if (command == null) if (command == null)
@ -652,7 +668,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -671,7 +687,7 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
var e = track.Element(uPnpNamespaces.items) ?? track; var e = track.Element(UPnpNamespaces.Items) ?? track;
var elementString = (string)e; var elementString = (string)e;
@ -687,13 +703,13 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
e = track.Element(uPnpNamespaces.items) ?? track; e = track.Element(UPnpNamespaces.Items) ?? track;
elementString = (string)e; elementString = (string)e;
if (!string.IsNullOrWhiteSpace(elementString)) if (!string.IsNullOrWhiteSpace(elementString))
{ {
return new uBaseObject return new UBaseObject
{ {
Url = elementString Url = elementString
}; };
@ -702,7 +718,7 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{ {
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
if (command == null) if (command == null)
@ -719,7 +735,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -731,11 +747,11 @@ namespace Emby.Dlna.PlayTo
return (false, null); return (false, null);
} }
var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null); var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
var trackUri = trackUriElem == null ? null : trackUriElem.Value; var trackUri = trackUriElem?.Value;
var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null); var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
var duration = durationElem == null ? null : durationElem.Value; var duration = durationElem?.Value;
if (!string.IsNullOrWhiteSpace(duration) if (!string.IsNullOrWhiteSpace(duration)
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
@ -747,8 +763,8 @@ namespace Emby.Dlna.PlayTo
Duration = null; Duration = null;
} }
var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null); var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
var position = positionElem == null ? null : positionElem.Value; var position = positionElem?.Value;
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{ {
@ -787,7 +803,7 @@ namespace Emby.Dlna.PlayTo
return (true, null); return (true, null);
} }
var e = uPnpResponse.Element(uPnpNamespaces.items); var e = uPnpResponse.Element(UPnpNamespaces.Items);
var uTrack = CreateUBaseObject(e, trackUri); var uTrack = CreateUBaseObject(e, trackUri);
@ -819,7 +835,7 @@ namespace Emby.Dlna.PlayTo
// some devices send back invalid xml // some devices send back invalid xml
try try
{ {
return XElement.Parse(xml.Replace("&", "&amp;")); return XElement.Parse(xml.Replace("&", "&amp;", StringComparison.Ordinal));
} }
catch (XmlException) catch (XmlException)
{ {
@ -828,27 +844,27 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
private static uBaseObject CreateUBaseObject(XElement container, string trackUri) private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
{ {
if (container == null) if (container == null)
{ {
throw new ArgumentNullException(nameof(container)); throw new ArgumentNullException(nameof(container));
} }
var url = container.GetValue(uPnpNamespaces.Res); var url = container.GetValue(UPnpNamespaces.Res);
if (string.IsNullOrWhiteSpace(url)) if (string.IsNullOrWhiteSpace(url))
{ {
url = trackUri; url = trackUri;
} }
return new uBaseObject return new UBaseObject
{ {
Id = container.GetAttributeValue(uPnpNamespaces.Id), Id = container.GetAttributeValue(UPnpNamespaces.Id),
ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId), ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
Title = container.GetValue(uPnpNamespaces.title), Title = container.GetValue(UPnpNamespaces.Title),
IconUrl = container.GetValue(uPnpNamespaces.Artwork), IconUrl = container.GetValue(UPnpNamespaces.Artwork),
SecondText = "", SecondText = string.Empty,
Url = url, Url = url,
ProtocolInfo = GetProtocolInfo(container), ProtocolInfo = GetProtocolInfo(container),
MetaData = container.ToString() MetaData = container.ToString()
@ -862,11 +878,11 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentNullException(nameof(container)); throw new ArgumentNullException(nameof(container));
} }
var resElement = container.Element(uPnpNamespaces.Res); var resElement = container.Element(UPnpNamespaces.Res);
if (resElement != null) if (resElement != null)
{ {
var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo); var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
if (info != null && !string.IsNullOrWhiteSpace(info.Value)) if (info != null && !string.IsNullOrWhiteSpace(info.Value))
{ {
@ -897,7 +913,7 @@ namespace Emby.Dlna.PlayTo
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
var httpClient = new SsdpHttpClient(_httpClient); var httpClient = new SsdpHttpClient(_httpClientFactory);
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
@ -925,7 +941,7 @@ namespace Emby.Dlna.PlayTo
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
var httpClient = new SsdpHttpClient(_httpClient); 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);
@ -941,12 +957,12 @@ namespace Emby.Dlna.PlayTo
return url; return url;
} }
if (!url.Contains("/")) if (!url.Contains('/', StringComparison.Ordinal))
{ {
url = "/dmr/" + url; url = "/dmr/" + url;
} }
if (!url.StartsWith("/")) if (!url.StartsWith("/", StringComparison.Ordinal))
{ {
url = "/" + url; url = "/" + url;
} }
@ -954,25 +970,21 @@ namespace Emby.Dlna.PlayTo
return baseUrl + url; return baseUrl + url;
} }
private TransportCommands AvCommands { get; set; } public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
private TransportCommands RendererCommands { get; set; }
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken)
{ {
var ssdpHttpClient = new SsdpHttpClient(httpClient); 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);
var friendlyNames = new List<string>(); var friendlyNames = new List<string>();
var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault(); var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault();
if (name != null && !string.IsNullOrWhiteSpace(name.Value)) if (name != null && !string.IsNullOrWhiteSpace(name.Value))
{ {
friendlyNames.Add(name.Value); friendlyNames.Add(name.Value);
} }
var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault(); var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault();
if (room != null && !string.IsNullOrWhiteSpace(room.Value)) if (room != null && !string.IsNullOrWhiteSpace(room.Value))
{ {
friendlyNames.Add(room.Value); friendlyNames.Add(room.Value);
@ -981,77 +993,77 @@ namespace Emby.Dlna.PlayTo
var deviceProperties = new DeviceInfo() var deviceProperties = new DeviceInfo()
{ {
Name = string.Join(" ", friendlyNames), Name = string.Join(" ", friendlyNames),
BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port) BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
}; };
var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault(); var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault();
if (model != null) if (model != null)
{ {
deviceProperties.ModelName = model.Value; deviceProperties.ModelName = model.Value;
} }
var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault(); var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault();
if (modelNumber != null) if (modelNumber != null)
{ {
deviceProperties.ModelNumber = modelNumber.Value; deviceProperties.ModelNumber = modelNumber.Value;
} }
var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault(); var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault();
if (uuid != null) if (uuid != null)
{ {
deviceProperties.UUID = uuid.Value; deviceProperties.UUID = uuid.Value;
} }
var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault(); var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault();
if (manufacturer != null) if (manufacturer != null)
{ {
deviceProperties.Manufacturer = manufacturer.Value; deviceProperties.Manufacturer = manufacturer.Value;
} }
var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault(); var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault();
if (manufacturerUrl != null) if (manufacturerUrl != null)
{ {
deviceProperties.ManufacturerUrl = manufacturerUrl.Value; deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
} }
var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault(); var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault();
if (presentationUrl != null) if (presentationUrl != null)
{ {
deviceProperties.PresentationUrl = presentationUrl.Value; deviceProperties.PresentationUrl = presentationUrl.Value;
} }
var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault(); var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault();
if (modelUrl != null) if (modelUrl != null)
{ {
deviceProperties.ModelUrl = modelUrl.Value; deviceProperties.ModelUrl = modelUrl.Value;
} }
var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault(); var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault();
if (serialNumber != null) if (serialNumber != null)
{ {
deviceProperties.SerialNumber = serialNumber.Value; deviceProperties.SerialNumber = serialNumber.Value;
} }
var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault(); var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault();
if (modelDescription != null) if (modelDescription != null)
{ {
deviceProperties.ModelDescription = modelDescription.Value; deviceProperties.ModelDescription = modelDescription.Value;
} }
var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault(); var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault();
if (icon != null) if (icon != null)
{ {
deviceProperties.Icon = CreateIcon(icon); deviceProperties.Icon = CreateIcon(icon);
} }
foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList"))) foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList")))
{ {
if (services == null) if (services == null)
{ {
continue; continue;
} }
var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service")); var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service"));
if (servicesList == null) if (servicesList == null)
{ {
continue; continue;
@ -1068,10 +1080,9 @@ namespace Emby.Dlna.PlayTo
} }
} }
return new Device(deviceProperties, httpClient, logger, config); return new Device(deviceProperties, httpClientFactory, logger);
} }
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private static DeviceIcon CreateIcon(XElement element) private static DeviceIcon CreateIcon(XElement element)
{ {
if (element == null) if (element == null)
@ -1079,11 +1090,11 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentNullException(nameof(element)); throw new ArgumentNullException(nameof(element));
} }
var mimeType = element.GetDescendantValue(uPnpNamespaces.ud.GetName("mimetype")); var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
var width = element.GetDescendantValue(uPnpNamespaces.ud.GetName("width")); var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
var height = element.GetDescendantValue(uPnpNamespaces.ud.GetName("height")); var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
var depth = element.GetDescendantValue(uPnpNamespaces.ud.GetName("depth")); var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
var url = element.GetDescendantValue(uPnpNamespaces.ud.GetName("url")); var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture); var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture); var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
@ -1100,11 +1111,11 @@ namespace Emby.Dlna.PlayTo
private static DeviceService Create(XElement element) private static DeviceService Create(XElement element)
{ {
var type = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceType")); var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
var id = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceId")); var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
var scpdUrl = element.GetDescendantValue(uPnpNamespaces.ud.GetName("SCPDURL")); var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL")); var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL")); var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
return new DeviceService return new DeviceService
{ {
@ -1116,14 +1127,7 @@ namespace Emby.Dlna.PlayTo
}; };
} }
public event EventHandler<PlaybackStartEventArgs> PlaybackStart; private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
public event EventHandler<MediaChangedEventArgs> MediaChanged;
public uBaseObject CurrentMediaInfo { get; private set; }
private void UpdateMediaInfo(uBaseObject mediaInfo, TRANSPORTSTATE state)
{ {
TransportState = state; TransportState = state;
@ -1132,7 +1136,7 @@ namespace Emby.Dlna.PlayTo
if (previousMediaInfo == null && mediaInfo != null) if (previousMediaInfo == null && mediaInfo != null)
{ {
if (state != TRANSPORTSTATE.STOPPED) if (state != TransportState.Stopped)
{ {
OnPlaybackStart(mediaInfo); OnPlaybackStart(mediaInfo);
} }
@ -1151,7 +1155,7 @@ namespace Emby.Dlna.PlayTo
} }
} }
private void OnPlaybackStart(uBaseObject mediaInfo) private void OnPlaybackStart(UBaseObject mediaInfo)
{ {
if (string.IsNullOrWhiteSpace(mediaInfo.Url)) if (string.IsNullOrWhiteSpace(mediaInfo.Url))
{ {
@ -1164,7 +1168,7 @@ namespace Emby.Dlna.PlayTo
}); });
} }
private void OnPlaybackProgress(uBaseObject mediaInfo) private void OnPlaybackProgress(UBaseObject mediaInfo)
{ {
if (string.IsNullOrWhiteSpace(mediaInfo.Url)) if (string.IsNullOrWhiteSpace(mediaInfo.Url))
{ {
@ -1177,7 +1181,7 @@ namespace Emby.Dlna.PlayTo
}); });
} }
private void OnPlaybackStop(uBaseObject mediaInfo) private void OnPlaybackStop(UBaseObject mediaInfo)
{ {
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
{ {
@ -1185,7 +1189,7 @@ namespace Emby.Dlna.PlayTo
}); });
} }
private void OnMediaChanged(uBaseObject old, uBaseObject newMedia) private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
{ {
MediaChanged?.Invoke(this, new MediaChangedEventArgs MediaChanged?.Invoke(this, new MediaChangedEventArgs
{ {
@ -1194,14 +1198,17 @@ namespace Emby.Dlna.PlayTo
}); });
} }
bool _disposed; /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (_disposed) if (_disposed)
@ -1220,9 +1227,10 @@ namespace Emby.Dlna.PlayTo
_disposed = true; _disposed = true;
} }
/// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl); return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
} }
} }
} }

View file

@ -8,6 +8,9 @@ namespace Emby.Dlna.PlayTo
{ {
public class DeviceInfo public class DeviceInfo
{ {
private readonly List<DeviceService> _services = new List<DeviceService>();
private string _baseUrl = string.Empty;
public DeviceInfo() public DeviceInfo()
{ {
Name = "Generic Device"; Name = "Generic Device";
@ -33,7 +36,6 @@ namespace Emby.Dlna.PlayTo
public string PresentationUrl { get; set; } public string PresentationUrl { get; set; }
private string _baseUrl = string.Empty;
public string BaseUrl public string BaseUrl
{ {
get => _baseUrl; get => _baseUrl;
@ -42,7 +44,6 @@ namespace Emby.Dlna.PlayTo
public DeviceIcon Icon { get; set; } public DeviceIcon Icon { get; set; }
private readonly List<DeviceService> _services = new List<DeviceService>();
public List<DeviceService> Services => _services; public List<DeviceService> Services => _services;
public DeviceIdentification ToDeviceIdentification() public DeviceIdentification ToDeviceIdentification()

View file

@ -0,0 +1,13 @@
#pragma warning disable CS1591
using System;
namespace Emby.Dlna.PlayTo
{
public class MediaChangedEventArgs : EventArgs
{
public UBaseObject OldMediaInfo { get; set; }
public UBaseObject NewMediaInfo { get; set; }
}
}

View file

@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
@ -18,7 +19,6 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.WebUtilities;
@ -31,7 +31,6 @@ namespace Emby.Dlna.PlayTo
{ {
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private Device _device;
private readonly SessionInfo _session; private readonly SessionInfo _session;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -50,6 +49,7 @@ namespace Emby.Dlna.PlayTo
private readonly string _accessToken; private readonly string _accessToken;
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
private Device _device;
private int _currentPlaylistIndex; private int _currentPlaylistIndex;
private bool _disposed; private bool _disposed;
@ -372,8 +372,13 @@ namespace Emby.Dlna.PlayTo
if (!command.ControllingUserId.Equals(Guid.Empty)) if (!command.ControllingUserId.Equals(Guid.Empty))
{ {
_sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId, _sessionManager.LogSessionActivity(
_session.DeviceName, _session.RemoteEndPoint, user); _session.Client,
_session.ApplicationVersion,
_session.DeviceId,
_session.DeviceName,
_session.RemoteEndPoint,
user);
} }
return PlayItems(playlist, cancellationToken); return PlayItems(playlist, cancellationToken);
@ -498,42 +503,44 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Audio) if (streamInfo.MediaType == DlnaProfileType.Audio)
{ {
return new ContentFeatureBuilder(profile) return new ContentFeatureBuilder(profile)
.BuildAudioHeader(streamInfo.Container, .BuildAudioHeader(
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.Container,
streamInfo.TargetAudioBitrate, streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetAudioSampleRate, streamInfo.TargetAudioBitrate,
streamInfo.TargetAudioChannels, streamInfo.TargetAudioSampleRate,
streamInfo.TargetAudioBitDepth, streamInfo.TargetAudioChannels,
streamInfo.IsDirectStream, streamInfo.TargetAudioBitDepth,
streamInfo.RunTimeTicks ?? 0, streamInfo.IsDirectStream,
streamInfo.TranscodeSeekInfo); streamInfo.RunTimeTicks ?? 0,
streamInfo.TranscodeSeekInfo);
} }
if (streamInfo.MediaType == DlnaProfileType.Video) if (streamInfo.MediaType == DlnaProfileType.Video)
{ {
var list = new ContentFeatureBuilder(profile) var list = new ContentFeatureBuilder(profile)
.BuildVideoHeader(streamInfo.Container, .BuildVideoHeader(
streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetWidth, streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetHeight, streamInfo.TargetWidth,
streamInfo.TargetVideoBitDepth, streamInfo.TargetHeight,
streamInfo.TargetVideoBitrate, streamInfo.TargetVideoBitDepth,
streamInfo.TargetTimestamp, streamInfo.TargetVideoBitrate,
streamInfo.IsDirectStream, streamInfo.TargetTimestamp,
streamInfo.RunTimeTicks ?? 0, streamInfo.IsDirectStream,
streamInfo.TargetVideoProfile, streamInfo.RunTimeTicks ?? 0,
streamInfo.TargetVideoLevel, streamInfo.TargetVideoProfile,
streamInfo.TargetFramerate ?? 0, streamInfo.TargetVideoLevel,
streamInfo.TargetPacketLength, streamInfo.TargetFramerate ?? 0,
streamInfo.TranscodeSeekInfo, streamInfo.TargetPacketLength,
streamInfo.IsTargetAnamorphic, streamInfo.TranscodeSeekInfo,
streamInfo.IsTargetInterlaced, streamInfo.IsTargetAnamorphic,
streamInfo.TargetRefFrames, streamInfo.IsTargetInterlaced,
streamInfo.TargetVideoStreamCount, streamInfo.TargetRefFrames,
streamInfo.TargetAudioStreamCount, streamInfo.TargetVideoStreamCount,
streamInfo.TargetVideoCodecTag, streamInfo.TargetAudioStreamCount,
streamInfo.IsTargetAVC); streamInfo.TargetVideoCodecTag,
streamInfo.IsTargetAVC);
return list.Count == 0 ? null : list[0]; return list.Count == 0 ? null : list[0];
} }
@ -633,6 +640,10 @@ namespace Emby.Dlna.PlayTo
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (_disposed) if (_disposed)
@ -673,48 +684,41 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.ToggleMute: case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken); return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex: case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
{ {
if (command.Arguments.TryGetValue("Index", out string arg)) if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{ {
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val)) return SetAudioStreamIndex(val);
{
return SetAudioStreamIndex(val);
}
throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
} }
throw new ArgumentException("SetAudioStreamIndex argument cannot be null"); throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
} }
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
case GeneralCommandType.SetSubtitleStreamIndex: case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index))
{ {
if (command.Arguments.TryGetValue("Index", out string arg)) if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{ {
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val)) return SetSubtitleStreamIndex(val);
{
return SetSubtitleStreamIndex(val);
}
throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
} }
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null"); throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
} }
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
case GeneralCommandType.SetVolume: case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
{ {
if (command.Arguments.TryGetValue("Volume", out string arg)) if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
{ {
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var volume)) return _device.SetVolume(volume, cancellationToken);
{
return _device.SetVolume(volume, cancellationToken);
}
throw new ArgumentException("Unsupported volume value supplied.");
} }
throw new ArgumentException("Volume argument cannot be null"); throw new ArgumentException("Unsupported volume value supplied.");
} }
throw new ArgumentException("Volume argument cannot be null");
default: default:
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -778,7 +782,7 @@ namespace Emby.Dlna.PlayTo
const int maxWait = 15000000; const int maxWait = 15000000;
const int interval = 500; const int interval = 500;
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).ConfigureAwait(false);
currentWait += interval; currentWait += interval;
@ -787,8 +791,67 @@ namespace Emby.Dlna.PlayTo
await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false); await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
} }
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.GetValueOrDefault(name);
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
return null;
}
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.GetValueOrDefault(name);
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
return 0;
}
/// <inheritdoc />
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
if (_device == null)
{
return Task.CompletedTask;
}
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}
// Not supported or needed right now
return Task.CompletedTask;
}
private class StreamParams private class StreamParams
{ {
private MediaSourceInfo mediaSource;
private IMediaSourceManager _mediaSourceManager;
public Guid ItemId { get; set; } public Guid ItemId { get; set; }
public bool IsDirectStream { get; set; } public bool IsDirectStream { get; set; }
@ -809,15 +872,11 @@ namespace Emby.Dlna.PlayTo
public BaseItem Item { get; set; } public BaseItem Item { get; set; }
private MediaSourceInfo MediaSource;
private IMediaSourceManager _mediaSourceManager;
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken) public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
{ {
if (MediaSource != null) if (mediaSource != null)
{ {
return MediaSource; return mediaSource;
} }
var hasMediaSources = Item as IHasMediaSources; var hasMediaSources = Item as IHasMediaSources;
@ -827,9 +886,9 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
MediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false); mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
return MediaSource; return mediaSource;
} }
private static Guid GetItemId(string url) private static Guid GetItemId(string url)
@ -901,61 +960,5 @@ namespace Emby.Dlna.PlayTo
return request; return request;
} }
} }
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.GetValueOrDefault(name);
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
return null;
}
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.GetValueOrDefault(name);
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
return 0;
}
/// <inheritdoc />
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
if (_device == null)
{
return Task.CompletedTask;
}
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}
// Not supported or needed right now
return Task.CompletedTask;
}
} }
} }

View file

@ -4,8 +4,10 @@ using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -16,7 +18,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -33,7 +34,7 @@ namespace Emby.Dlna.PlayTo
private readonly IDlnaManager _dlnaManager; private readonly IDlnaManager _dlnaManager;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly IHttpClient _httpClient; private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IUserDataManager _userDataManager; private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
@ -46,7 +47,7 @@ namespace Emby.Dlna.PlayTo
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{ {
_logger = logger; _logger = logger;
_sessionManager = sessionManager; _sessionManager = sessionManager;
@ -56,7 +57,7 @@ namespace Emby.Dlna.PlayTo
_appHost = appHost; _appHost = appHost;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_deviceDiscovery = deviceDiscovery; _deviceDiscovery = deviceDiscovery;
_httpClient = httpClient; _httpClientFactory = httpClientFactory;
_config = config; _config = config;
_userDataManager = userDataManager; _userDataManager = userDataManager;
_localization = localization; _localization = localization;
@ -92,7 +93,7 @@ namespace Emby.Dlna.PlayTo
// It has to report that it's a media renderer // It has to report that it's a media renderer
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 && if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1) nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
{ {
// _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location); // _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
return; return;
@ -174,7 +175,7 @@ namespace Emby.Dlna.PlayTo
if (controller == null) if (controller == null)
{ {
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false); var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
string deviceName = device.Properties.Name; string deviceName = device.Properties.Name;
@ -192,20 +193,20 @@ namespace Emby.Dlna.PlayTo
controller = new PlayToController( controller = new PlayToController(
sessionInfo, sessionInfo,
_sessionManager, _sessionManager,
_libraryManager, _libraryManager,
_logger, _logger,
_dlnaManager, _dlnaManager,
_userManager, _userManager,
_imageProcessor, _imageProcessor,
serverAddress, serverAddress,
null, null,
_deviceDiscovery, _deviceDiscovery,
_userDataManager, _userDataManager,
_localization, _localization,
_mediaSourceManager, _mediaSourceManager,
_config, _config,
_mediaEncoder); _mediaEncoder);
sessionInfo.AddController(controller); sessionInfo.AddController(controller);
@ -218,17 +219,17 @@ namespace Emby.Dlna.PlayTo
{ {
PlayableMediaTypes = profile.GetSupportedMediaTypes(), PlayableMediaTypes = profile.GetSupportedMediaTypes(),
SupportedCommands = new string[] SupportedCommands = new[]
{ {
GeneralCommandType.VolumeDown.ToString(), GeneralCommandType.VolumeDown.ToString(),
GeneralCommandType.VolumeUp.ToString(), GeneralCommandType.VolumeUp.ToString(),
GeneralCommandType.Mute.ToString(), GeneralCommandType.Mute.ToString(),
GeneralCommandType.Unmute.ToString(), GeneralCommandType.Unmute.ToString(),
GeneralCommandType.ToggleMute.ToString(), GeneralCommandType.ToggleMute.ToString(),
GeneralCommandType.SetVolume.ToString(), GeneralCommandType.SetVolume.ToString(),
GeneralCommandType.SetAudioStreamIndex.ToString(), GeneralCommandType.SetAudioStreamIndex.ToString(),
GeneralCommandType.SetSubtitleStreamIndex.ToString(), GeneralCommandType.SetSubtitleStreamIndex.ToString(),
GeneralCommandType.PlayMediaSource.ToString() GeneralCommandType.PlayMediaSource.ToString()
}, },
SupportsMediaControl = true SupportsMediaControl = true
@ -247,8 +248,9 @@ namespace Emby.Dlna.PlayTo
{ {
_disposeCancellationTokenSource.Cancel(); _disposeCancellationTokenSource.Cancel();
} }
catch catch (Exception ex)
{ {
_logger.LogDebug(ex, "Error while disposing PlayToManager");
} }
_sessionLock.Dispose(); _sessionLock.Dispose();

View file

@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class PlaybackProgressEventArgs : EventArgs public class PlaybackProgressEventArgs : EventArgs
{ {
public uBaseObject MediaInfo { get; set; } public UBaseObject MediaInfo { get; set; }
} }
} }

View file

@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class PlaybackStartEventArgs : EventArgs public class PlaybackStartEventArgs : EventArgs
{ {
public uBaseObject MediaInfo { get; set; } public UBaseObject MediaInfo { get; set; }
} }
} }

View file

@ -6,13 +6,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class PlaybackStoppedEventArgs : EventArgs public class PlaybackStoppedEventArgs : EventArgs
{ {
public uBaseObject MediaInfo { get; set; } public UBaseObject MediaInfo { get; set; }
}
public class MediaChangedEventArgs : EventArgs
{
public uBaseObject OldMediaInfo { get; set; }
public uBaseObject NewMediaInfo { get; set; }
} }
} }

View file

@ -4,6 +4,8 @@ using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -20,11 +22,11 @@ namespace Emby.Dlna.PlayTo
private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IHttpClient _httpClient; private readonly IHttpClientFactory _httpClientFactory;
public SsdpHttpClient(IHttpClient httpClient) public SsdpHttpClient(IHttpClientFactory httpClientFactory)
{ {
_httpClient = httpClient; _httpClientFactory = httpClientFactory;
} }
public async Task<XDocument> SendCommandAsync( public async Task<XDocument> SendCommandAsync(
@ -36,20 +38,18 @@ namespace Emby.Dlna.PlayTo
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var url = NormalizeServiceUrl(baseUrl, service.ControlUrl); var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
using (var response = await PostSoapDataAsync( using var response = await PostSoapDataAsync(
url, url,
$"\"{service.ServiceType}#{command}\"", $"\"{service.ServiceType}#{command}\"",
postData, postData,
header, header,
cancellationToken) cancellationToken)
.ConfigureAwait(false)) .ConfigureAwait(false);
using (var stream = response.Content) await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using (var reader = new StreamReader(stream, Encoding.UTF8)) using var reader = new StreamReader(stream, Encoding.UTF8);
{ return XDocument.Parse(
return XDocument.Parse( await reader.ReadToEndAsync().ConfigureAwait(false),
await reader.ReadToEndAsync().ConfigureAwait(false), LoadOptions.PreserveWhitespace);
LoadOptions.PreserveWhitespace);
}
} }
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@ -76,49 +76,32 @@ namespace Emby.Dlna.PlayTo
int eventport, int eventport,
int timeOut = 3600) int timeOut = 3600)
{ {
var options = new HttpRequestOptions using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
{ options.Headers.UserAgent.ParseAdd(USERAGENT);
Url = url, options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
UserAgent = USERAGENT, options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
LogErrorResponseBody = true, options.Headers.TryAddWithoutValidation("NT", "upnp:event");
BufferContent = false, options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
};
options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture); using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">"; .SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
options.RequestHeaders["NT"] = "upnp:event"; .ConfigureAwait(false);
options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture);
using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false))
{
}
} }
public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken) public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
{ {
var options = new HttpRequestOptions using var options = new HttpRequestMessage(HttpMethod.Get, url);
{ options.Headers.UserAgent.ParseAdd(USERAGENT);
Url = url, options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
UserAgent = USERAGENT, using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
LogErrorResponseBody = true, await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
BufferContent = false, using var reader = new StreamReader(stream, Encoding.UTF8);
return XDocument.Parse(
CancellationToken = cancellationToken await reader.ReadToEndAsync().ConfigureAwait(false),
}; LoadOptions.PreserveWhitespace);
options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
using (var stream = response.Content)
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),
LoadOptions.PreserveWhitespace);
}
} }
private Task<HttpResponseInfo> PostSoapDataAsync( private Task<HttpResponseMessage> PostSoapDataAsync(
string url, string url,
string soapAction, string soapAction,
string postData, string postData,
@ -130,29 +113,20 @@ namespace Emby.Dlna.PlayTo
soapAction = $"\"{soapAction}\""; soapAction = $"\"{soapAction}\"";
} }
var options = new HttpRequestOptions using var options = new HttpRequestMessage(HttpMethod.Post, url);
{ options.Headers.UserAgent.ParseAdd(USERAGENT);
Url = url, options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction);
UserAgent = USERAGENT, options.Headers.TryAddWithoutValidation("Pragma", "no-cache");
LogErrorResponseBody = true, options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
BufferContent = false,
CancellationToken = cancellationToken
};
options.RequestHeaders["SOAPAction"] = soapAction;
options.RequestHeaders["Pragma"] = "no-cache";
options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
if (!string.IsNullOrEmpty(header)) if (!string.IsNullOrEmpty(header))
{ {
options.RequestHeaders["contentFeatures.dlna.org"] = header; options.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
} }
options.RequestContentType = "text/xml"; options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml);
options.RequestContent = postData;
return _httpClient.Post(options); return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
} }
} }
} }

View file

@ -1,13 +0,0 @@
#pragma warning disable CS1591
namespace Emby.Dlna.PlayTo
{
public enum TRANSPORTSTATE
{
STOPPED,
PLAYING,
TRANSITIONING,
PAUSED_PLAYBACK,
PAUSED
}
}

View file

@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using Emby.Dlna.Common; using Emby.Dlna.Common;
@ -11,36 +12,30 @@ 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 List<StateVariable> _stateVariables = new List<StateVariable>(); private List<StateVariable> _stateVariables = new List<StateVariable>();
public List<StateVariable> StateVariables
{
get => _stateVariables;
set => _stateVariables = value;
}
private List<ServiceAction> _serviceActions = new List<ServiceAction>(); private List<ServiceAction> _serviceActions = new List<ServiceAction>();
public List<ServiceAction> ServiceActions
{ public List<StateVariable> StateVariables => _stateVariables;
get => _serviceActions;
set => _serviceActions = value; public List<ServiceAction> ServiceActions => _serviceActions;
}
public static TransportCommands Create(XDocument document) public static TransportCommands Create(XDocument document)
{ {
var command = new TransportCommands(); var command = new TransportCommands();
var actionList = document.Descendants(uPnpNamespaces.svc + "actionList"); var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList");
foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action")) foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action"))
{ {
command.ServiceActions.Add(ServiceActionFromXml(container)); command.ServiceActions.Add(ServiceActionFromXml(container));
} }
var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault(); var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault();
if (stateValues != null) if (stateValues != null)
{ {
foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable")) foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable"))
{ {
command.StateVariables.Add(FromXml(container)); command.StateVariables.Add(FromXml(container));
} }
@ -51,19 +46,19 @@ namespace Emby.Dlna.PlayTo
private static ServiceAction ServiceActionFromXml(XElement container) private static ServiceAction ServiceActionFromXml(XElement container)
{ {
var argumentList = new List<Argument>(); var serviceAction = new ServiceAction
{
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
};
foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument")) var argumentList = serviceAction.ArgumentList;
foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument"))
{ {
argumentList.Add(ArgumentFromXml(arg)); argumentList.Add(ArgumentFromXml(arg));
} }
return new ServiceAction return serviceAction;
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
ArgumentList = argumentList
};
} }
private static Argument ArgumentFromXml(XElement container) private static Argument ArgumentFromXml(XElement container)
@ -75,29 +70,29 @@ namespace Emby.Dlna.PlayTo
return new Argument return new Argument
{ {
Name = container.GetValue(uPnpNamespaces.svc + "name"), Name = container.GetValue(UPnpNamespaces.Svc + "name"),
Direction = container.GetValue(uPnpNamespaces.svc + "direction"), Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable") RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
}; };
} }
private static StateVariable FromXml(XElement container) private static StateVariable FromXml(XElement container)
{ {
var allowedValues = new List<string>(); var allowedValues = new List<string>();
var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList") var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
.FirstOrDefault(); .FirstOrDefault();
if (element != null) if (element != null)
{ {
var values = element.Descendants(uPnpNamespaces.svc + "allowedValue"); var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
allowedValues.AddRange(values.Select(child => child.Value)); allowedValues.AddRange(values.Select(child => child.Value));
} }
return new StateVariable return new StateVariable
{ {
Name = container.GetValue(uPnpNamespaces.svc + "name"), Name = container.GetValue(UPnpNamespaces.Svc + "name"),
DataType = container.GetValue(uPnpNamespaces.svc + "dataType"), DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
AllowedValues = allowedValues.ToArray() AllowedValues = allowedValues.ToArray()
}; };
} }
@ -123,7 +118,7 @@ namespace Emby.Dlna.PlayTo
} }
} }
return string.Format(CommandBase, action.Name, xmlNamespace, stateString); return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
} }
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "") public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
@ -147,7 +142,7 @@ namespace Emby.Dlna.PlayTo
} }
} }
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
} }
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary) public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
@ -170,7 +165,7 @@ namespace Emby.Dlna.PlayTo
} }
} }
return string.Format(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 = "")
@ -180,15 +175,12 @@ namespace Emby.Dlna.PlayTo
if (state != null) if (state != null)
{ {
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ?? var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
state.AllowedValues.FirstOrDefault() ?? (state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
value;
return string.Format("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue); return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
} }
return string.Format("<{0}>{1}</{0}>", argument.Name, value); return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
} }
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>";
} }
} }

View file

@ -0,0 +1,14 @@
#pragma warning disable CS1591
#pragma warning disable SA1602
namespace Emby.Dlna.PlayTo
{
public enum TransportState
{
Stopped,
Playing,
Transitioning,
PausedPlayback,
Paused
}
}

View file

@ -6,22 +6,22 @@ using Emby.Dlna.Ssdp;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
{ {
public class UpnpContainer : uBaseObject public class UpnpContainer : UBaseObject
{ {
public static uBaseObject Create(XElement container) public static UBaseObject Create(XElement container)
{ {
if (container == null) if (container == null)
{ {
throw new ArgumentNullException(nameof(container)); throw new ArgumentNullException(nameof(container));
} }
return new uBaseObject return new UBaseObject
{ {
Id = container.GetAttributeValue(uPnpNamespaces.Id), Id = container.GetAttributeValue(UPnpNamespaces.Id),
ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId), ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
Title = container.GetValue(uPnpNamespaces.title), Title = container.GetValue(UPnpNamespaces.Title),
IconUrl = container.GetValue(uPnpNamespaces.Artwork), IconUrl = container.GetValue(UPnpNamespaces.Artwork),
UpnpClass = container.GetValue(uPnpNamespaces.uClass) UpnpClass = container.GetValue(UPnpNamespaces.Class)
}; };
} }
} }

View file

@ -1,10 +1,11 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
{ {
public class uBaseObject public class UBaseObject
{ {
public string Id { get; set; } public string Id { get; set; }
@ -20,20 +21,10 @@ namespace Emby.Dlna.PlayTo
public string Url { get; set; } public string Url { get; set; }
public string[] ProtocolInfo { get; set; } public IReadOnlyList<string> ProtocolInfo { get; set; }
public string UpnpClass { get; set; } public string UpnpClass { get; set; }
public bool Equals(uBaseObject obj)
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
return string.Equals(Id, obj.Id);
}
public string MediaType public string MediaType
{ {
get get
@ -58,5 +49,15 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
} }
public bool Equals(UBaseObject obj)
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
return string.Equals(Id, obj.Id, StringComparison.Ordinal);
}
} }
} }

View file

@ -4,38 +4,64 @@ using System.Xml.Linq;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
{ {
public class uPnpNamespaces public static class UPnpNamespaces
{ {
public static XNamespace dc = "http://purl.org/dc/elements/1.1/"; public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/";
public static XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
public static XNamespace svc = "urn:schemas-upnp-org:service-1-0";
public static XNamespace ud = "urn:schemas-upnp-org:device-1-0";
public static XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
public static XNamespace RenderingControl = "urn:schemas-upnp-org:service:RenderingControl:1";
public static XNamespace AvTransport = "urn:schemas-upnp-org:service:AVTransport:1";
public static XNamespace ContentDirectory = "urn:schemas-upnp-org:service:ContentDirectory:1";
public static XName containers = ns + "container"; public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
public static XName items = ns + "item";
public static XName title = dc + "title";
public static XName creator = dc + "creator";
public static XName artist = upnp + "artist";
public static XName Id = "id";
public static XName ParentId = "parentID";
public static XName uClass = upnp + "class";
public static XName Artwork = upnp + "albumArtURI";
public static XName Description = dc + "description";
public static XName LongDescription = upnp + "longDescription";
public static XName Album = upnp + "album";
public static XName Author = upnp + "author";
public static XName Director = upnp + "director";
public static XName PlayCount = upnp + "playbackCount";
public static XName Tracknumber = upnp + "originalTrackNumber";
public static XName Res = ns + "res";
public static XName Duration = "duration";
public static XName ProtocolInfo = "protocolInfo";
public static XName ServiceStateTable = svc + "serviceStateTable"; public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0";
public static XName StateVariable = svc + "stateVariable";
public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0";
public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/";
public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1";
public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1";
public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1";
public static XName Containers { get; } = Ns + "container";
public static XName Items { get; } = Ns + "item";
public static XName Title { get; } = Dc + "title";
public static XName Creator { get; } = Dc + "creator";
public static XName Artist { get; } = UPnp + "artist";
public static XName Id { get; } = "id";
public static XName ParentId { get; } = "parentID";
public static XName Class { get; } = UPnp + "class";
public static XName Artwork { get; } = UPnp + "albumArtURI";
public static XName Description { get; } = Dc + "description";
public static XName LongDescription { get; } = UPnp + "longDescription";
public static XName Album { get; } = UPnp + "album";
public static XName Author { get; } = UPnp + "author";
public static XName Director { get; } = UPnp + "director";
public static XName PlayCount { get; } = UPnp + "playbackCount";
public static XName Tracknumber { get; } = UPnp + "originalTrackNumber";
public static XName Res { get; } = Ns + "res";
public static XName Duration { get; } = "duration";
public static XName ProtocolInfo { get; } = "protocolInfo";
public static XName ServiceStateTable { get; } = Svc + "serviceStateTable";
public static XName StateVariable { get; } = Svc + "stateVariable";
} }
} }

View file

@ -64,14 +64,14 @@ namespace Emby.Dlna.Profiles
new DirectPlayProfile new DirectPlayProfile
{ {
// play all // play all
Container = "", Container = string.Empty,
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new DirectPlayProfile new DirectPlayProfile
{ {
// play all // play all
Container = "", Container = string.Empty,
Type = DlnaProfileType.Audio Type = DlnaProfileType.Audio
} }
}; };
@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
public void AddXmlRootAttribute(string name, string value) public void AddXmlRootAttribute(string name, string value)
{ {
var atts = XmlRootAttributes ?? new XmlAttribute[] { }; var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
var list = atts.ToList(); var list = atts.ToList();
list.Add(new XmlAttribute list.Add(new XmlAttribute

View file

@ -28,7 +28,7 @@ namespace Emby.Dlna.Profiles
}, },
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = System.Array.Empty<ResponseProfile>();
} }
} }
} }

View file

@ -123,7 +123,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = System.Array.Empty<ResponseProfile>();
} }
} }
} }

View file

@ -24,7 +24,7 @@ namespace Emby.Dlna.Profiles
{ {
Match = HeaderMatchType.Substring, Match = HeaderMatchType.Substring,
Name = "User-Agent", Name = "User-Agent",
Value ="Zip_" Value = "Zip_"
} }
} }
}; };
@ -81,7 +81,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -124,7 +124,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile new CodecProfile
{ {
Type = CodecType.Video, Type = CodecType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -161,7 +161,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3,he-aac", Codec = "ac3,he-aac",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -177,7 +177,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "aac", Codec = "aac",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -192,7 +192,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile new CodecProfile
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Conditions = new [] Conditions = new[]
{ {
// The device does not have any audio switching capabilities // The device does not have any audio switching capabilities
new ProfileCondition new ProfileCondition

View file

@ -72,7 +72,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = System.Array.Empty<ResponseProfile>();
} }
} }
} }

View file

@ -84,7 +84,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] ResponseProfiles = new[]
{ {
new ResponseProfile new ResponseProfile
{ {

View file

@ -32,7 +32,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] ResponseProfiles = new[]
{ {
new ResponseProfile new ResponseProfile
{ {

View file

@ -37,7 +37,7 @@ namespace Emby.Dlna.Profiles
}, },
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = System.Array.Empty<ResponseProfile>();
} }
} }
} }

View file

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles namespace Emby.Dlna.Profiles
@ -37,7 +38,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = Array.Empty<ResponseProfile>();
} }
} }
} }

View file

@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {

View file

@ -93,8 +93,8 @@ namespace Emby.Dlna.Profiles
new CodecProfile new CodecProfile
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec="h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition(ProfileConditionType.EqualsAny, ProfileConditionValue.VideoProfile, "baseline|constrained baseline"), new ProfileCondition(ProfileConditionType.EqualsAny, ProfileConditionValue.VideoProfile, "baseline|constrained baseline"),
new ProfileCondition new ProfileCondition
@ -122,7 +122,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile new CodecProfile
{ {
Type = CodecType.Video, Type = CodecType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "aac", Codec = "aac",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Audio, Type = CodecType.Audio,
Codec = "aac", Codec = "aac",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -182,7 +182,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Audio, Type = CodecType.Audio,
Codec = "mp3", Codec = "mp3",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -202,7 +202,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] ResponseProfiles = new[]
{ {
new ResponseProfile new ResponseProfile
{ {

View file

@ -139,7 +139,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {

View file

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles namespace Emby.Dlna.Profiles
@ -149,7 +150,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -177,7 +178,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3", Codec = "ac3",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -196,7 +197,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = Array.Empty<ResponseProfile>();
} }
} }
} }

View file

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles namespace Emby.Dlna.Profiles
@ -149,7 +150,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -177,7 +178,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3", Codec = "ac3",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -196,7 +197,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = Array.Empty<ResponseProfile>();
} }
} }
} }

View file

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles namespace Emby.Dlna.Profiles
@ -137,7 +138,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -165,7 +166,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3", Codec = "ac3",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -184,7 +185,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = Array.Empty<ResponseProfile>();
} }
} }
} }

View file

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.Profiles namespace Emby.Dlna.Profiles
@ -137,7 +138,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -165,7 +166,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3", Codec = "ac3",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -184,7 +185,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
} }
}; };
ResponseProfiles = new ResponseProfile[] { }; ResponseProfiles = Array.Empty<ResponseProfile>();
} }
} }
} }

View file

@ -114,7 +114,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -156,7 +156,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3", Codec = "ac3",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -172,7 +172,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "aac", Codec = "aac",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles
VideoCodec = "h264,mpeg4,vc1", VideoCodec = "h264,mpeg4,vc1",
AudioCodec = "ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },

View file

@ -102,13 +102,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -128,13 +128,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/mpeg", MimeType = "video/mpeg",
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -148,28 +148,28 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="mpeg2video", VideoCodec = "mpeg2video",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
{ {
Container = "mpeg", Container = "mpeg",
VideoCodec="mpeg1video,mpeg2video", VideoCodec = "mpeg1video,mpeg2video",
MimeType = "video/mpeg", MimeType = "video/mpeg",
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
} }
}; };
@ -180,7 +180,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -204,7 +204,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -243,7 +243,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "mpeg2video", Codec = "mpeg2video",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -275,7 +275,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile new CodecProfile
{ {
Type = CodecType.Video, Type = CodecType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -303,7 +303,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3", Codec = "ac3",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -319,7 +319,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "aac", Codec = "aac",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -341,7 +341,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "mp3,mp2", Codec = "mp3,mp2",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {

View file

@ -120,7 +120,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -143,13 +143,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -169,13 +169,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/mpeg", MimeType = "video/mpeg",
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -189,28 +189,28 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="mpeg2video", VideoCodec = "mpeg2video",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
{ {
Container = "mpeg", Container = "mpeg",
VideoCodec="mpeg1video,mpeg2video", VideoCodec = "mpeg1video,mpeg2video",
MimeType = "video/mpeg", MimeType = "video/mpeg",
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
@ -227,7 +227,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -266,7 +266,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "mpeg2video", Codec = "mpeg2video",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -298,7 +298,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile new CodecProfile
{ {
Type = CodecType.Video, Type = CodecType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -326,7 +326,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3", Codec = "ac3",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -364,7 +364,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "mp3,mp2", Codec = "mp3,mp2",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {

View file

@ -131,13 +131,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -157,13 +157,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/mpeg", MimeType = "video/mpeg",
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -177,28 +177,28 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="mpeg2video", VideoCodec = "mpeg2video",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
{ {
Container = "mpeg", Container = "mpeg",
VideoCodec="mpeg1video,mpeg2video", VideoCodec = "mpeg1video,mpeg2video",
MimeType = "video/mpeg", MimeType = "video/mpeg",
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
@ -215,7 +215,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -282,7 +282,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "mp3,mp2", Codec = "mp3,mp2",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {

View file

@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/mpeg", MimeType = "video/mpeg",
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="mpeg2video", VideoCodec = "mpeg2video",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
{ {
Container = "mpeg", Container = "mpeg",
VideoCodec="mpeg1video,mpeg2video", VideoCodec = "mpeg1video,mpeg2video",
MimeType = "video/mpeg", MimeType = "video/mpeg",
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles
} }
}; };
CodecProfiles = new[] CodecProfiles = new[]
{ {
new CodecProfile new CodecProfile
{ {
Type = CodecType.Video, Type = CodecType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "mp3,mp2", Codec = "mp3,mp2",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {

View file

@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/mpeg", MimeType = "video/mpeg",
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="h264", VideoCodec = "h264",
AudioCodec="ac3,aac,mp3", AudioCodec = "ac3,aac,mp3",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
{ {
Container = "ts,mpegts", Container = "ts,mpegts",
VideoCodec="mpeg2video", VideoCodec = "mpeg2video",
MimeType = "video/vnd.dlna.mpeg-tts", MimeType = "video/vnd.dlna.mpeg-tts",
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
{ {
Container = "mpeg", Container = "mpeg",
VideoCodec="mpeg1video,mpeg2video", VideoCodec = "mpeg1video,mpeg2video",
MimeType = "video/mpeg", MimeType = "video/mpeg",
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new ResponseProfile new ResponseProfile
@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles
} }
}; };
CodecProfiles = new[] CodecProfiles = new[]
{ {
new CodecProfile new CodecProfile
{ {
Type = CodecType.Video, Type = CodecType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "mp3,mp2", Codec = "mp3,mp2",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {

View file

@ -108,7 +108,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -133,7 +133,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -176,7 +176,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3", Codec = "ac3",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -201,7 +201,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "wmapro", Codec = "wmapro",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "aac", Codec = "aac",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -235,7 +235,7 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "mp4,mov", Container = "mp4,mov",
AudioCodec="aac", AudioCodec = "aac",
MimeType = "video/mp4", MimeType = "video/mp4",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
@ -244,7 +244,7 @@ namespace Emby.Dlna.Profiles
{ {
Container = "avi", Container = "avi",
MimeType = "video/divx", MimeType = "video/divx",
OrgPn="AVI", OrgPn = "AVI",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },

View file

@ -110,7 +110,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -135,7 +135,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3", Codec = "ac3",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -203,7 +203,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "wmapro", Codec = "wmapro",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -219,7 +219,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "aac", Codec = "aac",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -237,7 +237,7 @@ namespace Emby.Dlna.Profiles
new ResponseProfile new ResponseProfile
{ {
Container = "mp4,mov", Container = "mp4,mov",
AudioCodec="aac", AudioCodec = "aac",
MimeType = "video/mp4", MimeType = "video/mp4",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
@ -246,7 +246,7 @@ namespace Emby.Dlna.Profiles
{ {
Container = "avi", Container = "avi",
MimeType = "video/divx", MimeType = "video/divx",
OrgPn="AVI", OrgPn = "AVI",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },

View file

@ -20,7 +20,7 @@ namespace Emby.Dlna.Profiles
Headers = new[] Headers = new[]
{ {
new HttpHeaderInfo {Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring}, new HttpHeaderInfo { Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring },
new HttpHeaderInfo new HttpHeaderInfo
{ {
Name = "User-Agent", Name = "User-Agent",
@ -168,7 +168,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = DlnaProfileType.Photo, Type = DlnaProfileType.Photo,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -193,7 +193,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -221,7 +221,7 @@ namespace Emby.Dlna.Profiles
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "aac", Codec = "aac",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {

View file

@ -119,7 +119,7 @@ namespace Emby.Dlna.Profiles
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
Container = "mp4,mov", Container = "mp4,mov",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "mpeg4", Codec = "mpeg4",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -187,7 +187,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "h264", Codec = "h264",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -236,7 +236,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.Video, Type = CodecType.Video,
Codec = "wmv2,wmv3,vc1", Codec = "wmv2,wmv3,vc1",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -284,7 +284,7 @@ namespace Emby.Dlna.Profiles
new CodecProfile new CodecProfile
{ {
Type = CodecType.Video, Type = CodecType.Video,
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -307,7 +307,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "ac3,wmav2,wmapro", Codec = "ac3,wmav2,wmapro",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {
@ -323,7 +323,7 @@ namespace Emby.Dlna.Profiles
{ {
Type = CodecType.VideoAudio, Type = CodecType.VideoAudio,
Codec = "aac", Codec = "aac",
Conditions = new [] Conditions = new[]
{ {
new ProfileCondition new ProfileCondition
{ {

View file

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Security;
using System.Text; using System.Text;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
foreach (var att in attributes) foreach (var att in attributes)
{ {
builder.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value); builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value);
} }
builder.Append(">"); builder.Append('>');
builder.Append("<specVersion>"); builder.Append("<specVersion>");
builder.Append("<major>1</major>"); builder.Append("<major>1</major>");
@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
if (!EnableAbsoluteUrls) if (!EnableAbsoluteUrls)
{ {
builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>"); builder.Append("<URLBase>")
.Append(SecurityElement.Escape(_serverAddress))
.Append("</URLBase>");
} }
AppendDeviceInfo(builder); AppendDeviceInfo(builder);
@ -93,91 +96,14 @@ namespace Emby.Dlna.Server
AppendIconList(builder); AppendIconList(builder);
builder.Append("<presentationURL>" + Escape(_serverAddress) + "/web/index.html</presentationURL>"); builder.Append("<presentationURL>")
.Append(SecurityElement.Escape(_serverAddress))
.Append("/web/index.html</presentationURL>");
AppendServiceList(builder); AppendServiceList(builder);
builder.Append("</device>"); builder.Append("</device>");
} }
private static readonly char[] s_escapeChars = new char[]
{
'<',
'>',
'"',
'\'',
'&'
};
private static readonly string[] s_escapeStringPairs = new[]
{
"<",
"&lt;",
">",
"&gt;",
"\"",
"&quot;",
"'",
"&apos;",
"&",
"&amp;"
};
private static string GetEscapeSequence(char c)
{
int num = s_escapeStringPairs.Length;
for (int i = 0; i < num; i += 2)
{
string text = s_escapeStringPairs[i];
string result = s_escapeStringPairs[i + 1];
if (text[0] == c)
{
return result;
}
}
return c.ToString(CultureInfo.InvariantCulture);
}
/// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
/// <returns>The input string with invalid characters replaced.</returns>
/// <param name="str">The string within which to escape invalid characters. </param>
public static string Escape(string str)
{
if (str == null)
{
return null;
}
StringBuilder stringBuilder = null;
int length = str.Length;
int num = 0;
while (true)
{
int num2 = str.IndexOfAny(s_escapeChars, num);
if (num2 == -1)
{
break;
}
if (stringBuilder == null)
{
stringBuilder = new StringBuilder();
}
stringBuilder.Append(str, num, num2 - num);
stringBuilder.Append(GetEscapeSequence(str[num2]));
num = num2 + 1;
}
if (stringBuilder == null)
{
return str;
}
stringBuilder.Append(str, num, length - num);
return stringBuilder.ToString();
}
private void AppendDeviceProperties(StringBuilder builder) private void AppendDeviceProperties(StringBuilder builder)
{ {
builder.Append("<dlna:X_DLNACAP/>"); builder.Append("<dlna:X_DLNACAP/>");
@ -187,32 +113,54 @@ namespace Emby.Dlna.Server
builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>"); builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>"); builder.Append("<friendlyName>")
builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>"); .Append(SecurityElement.Escape(GetFriendlyName()))
builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>"); .Append("</friendlyName>");
builder.Append("<manufacturer>")
.Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
.Append("</manufacturer>");
builder.Append("<manufacturerURL>")
.Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
.Append("</manufacturerURL>");
builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>"); builder.Append("<modelDescription>")
builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>"); .Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
.Append("</modelDescription>");
builder.Append("<modelName>")
.Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
.Append("</modelName>");
builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>"); builder.Append("<modelNumber>")
builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>"); .Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
.Append("</modelNumber>");
builder.Append("<modelURL>")
.Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
.Append("</modelURL>");
if (string.IsNullOrEmpty(_profile.SerialNumber)) if (string.IsNullOrEmpty(_profile.SerialNumber))
{ {
builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>"); builder.Append("<serialNumber>")
.Append(SecurityElement.Escape(_serverId))
.Append("</serialNumber>");
} }
else else
{ {
builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>"); builder.Append("<serialNumber>")
.Append(SecurityElement.Escape(_profile.SerialNumber))
.Append("</serialNumber>");
} }
builder.Append("<UPC/>"); builder.Append("<UPC/>");
builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>"); builder.Append("<UDN>uuid:")
.Append(SecurityElement.Escape(_serverUdn))
.Append("</UDN>");
if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags)) if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
{ {
builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>"); builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">")
.Append(SecurityElement.Escape(_profile.SonyAggregationFlags))
.Append("</av:aggregationFlags>");
} }
} }
@ -250,11 +198,21 @@ namespace Emby.Dlna.Server
{ {
builder.Append("<icon>"); builder.Append("<icon>");
builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>"); builder.Append("<mimetype>")
builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>"); .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>"); .Append("</mimetype>");
builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>"); builder.Append("<width>")
builder.Append("<url>" + BuildUrl(icon.Url) + "</url>"); .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
.Append("</width>");
builder.Append("<height>")
.Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
.Append("</height>");
builder.Append("<depth>")
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
.Append("</depth>");
builder.Append("<url>")
.Append(BuildUrl(icon.Url))
.Append("</url>");
builder.Append("</icon>"); builder.Append("</icon>");
} }
@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
{ {
builder.Append("<service>"); builder.Append("<service>");
builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>"); builder.Append("<serviceType>")
builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>"); .Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>"); .Append("</serviceType>");
builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>"); builder.Append("<serviceId>")
builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>"); .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
.Append("</serviceId>");
builder.Append("<SCPDURL>")
.Append(BuildUrl(service.ScpdUrl))
.Append("</SCPDURL>");
builder.Append("<controlURL>")
.Append(BuildUrl(service.ControlUrl))
.Append("</controlURL>");
builder.Append("<eventSubURL>")
.Append(BuildUrl(service.EventSubUrl))
.Append("</eventSubURL>");
builder.Append("</service>"); builder.Append("</service>");
} }
@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + url; url = _serverAddress.TrimEnd('/') + url;
} }
return Escape(url); return SecurityElement.Escape(url);
} }
private IEnumerable<DeviceIcon> GetIcons() private IEnumerable<DeviceIcon> GetIcons()

View file

@ -15,11 +15,7 @@ namespace Emby.Dlna.Service
{ {
public abstract class BaseControlHandler public abstract class BaseControlHandler
{ {
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
protected IServerConfigurationManager Config { get; }
protected ILogger Logger { get; }
protected BaseControlHandler(IServerConfigurationManager config, ILogger logger) protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
{ {
@ -27,6 +23,10 @@ namespace Emby.Dlna.Service
Logger = logger; Logger = logger;
} }
protected IServerConfigurationManager Config { get; }
protected ILogger Logger { get; }
public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{ {
try try
@ -80,10 +80,10 @@ namespace Emby.Dlna.Service
{ {
writer.WriteStartDocument(true); writer.WriteStartDocument(true);
writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV); writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/"); writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV); writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI); writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI);
WriteResult(requestInfo.LocalName, requestInfo.Headers, writer); WriteResult(requestInfo.LocalName, requestInfo.Headers, writer);
@ -210,15 +210,6 @@ namespace Emby.Dlna.Service
} }
} }
private class ControlRequestInfo
{
public string LocalName { get; set; }
public string NamespaceURI { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter); protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
private void LogRequest(ControlRequest request) private void LogRequest(ControlRequest request)
@ -240,5 +231,14 @@ namespace Emby.Dlna.Service
Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml); Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
} }
private class ControlRequestInfo
{
public string LocalName { get; set; }
public string NamespaceURI { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
} }
} }

View file

@ -1,25 +1,23 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Net.Http;
using Emby.Dlna.Eventing; using Emby.Dlna.Eventing;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Service namespace Emby.Dlna.Service
{ {
public class BaseService : IEventManager public class BaseService : IDlnaEventManager
{ {
protected IEventManager EventManager; protected BaseService(ILogger<BaseService> logger, IHttpClientFactory httpClientFactory)
protected IHttpClient HttpClient;
protected ILogger Logger;
protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
{ {
Logger = logger; Logger = logger;
HttpClient = httpClient; EventManager = new DlnaEventManager(logger, httpClientFactory);
EventManager = new EventManager(logger, HttpClient);
} }
protected IDlnaEventManager EventManager { get; }
protected ILogger Logger { get; }
public EventSubscriptionResponse CancelEventSubscription(string subscriptionId) public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
{ {
return EventManager.CancelEventSubscription(subscriptionId); return EventManager.CancelEventSubscription(subscriptionId);

View file

@ -10,7 +10,7 @@ namespace Emby.Dlna.Service
{ {
public static class ControlErrorHandler public static class ControlErrorHandler
{ {
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
public static ControlResponse GetResponse(Exception ex) public static ControlResponse GetResponse(Exception ex)
{ {
@ -26,11 +26,11 @@ namespace Emby.Dlna.Service
{ {
writer.WriteStartDocument(true); writer.WriteStartDocument(true);
writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV); writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/"); writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV); writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
writer.WriteStartElement("SOAP-ENV", "Fault", NS_SOAPENV); writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv);
writer.WriteElementString("faultcode", "500"); writer.WriteElementString("faultcode", "500");
writer.WriteElementString("faultstring", ex.Message); writer.WriteElementString("faultstring", ex.Message);

View file

@ -1,9 +1,9 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using System.Security;
using System.Text; using System.Text;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Server;
namespace Emby.Dlna.Service namespace Emby.Dlna.Service
{ {
@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
{ {
builder.Append("<action>"); builder.Append("<action>");
builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>"); builder.Append("<name>")
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
.Append("</name>");
builder.Append("<argumentList>"); builder.Append("<argumentList>");
@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
{ {
builder.Append("<argument>"); builder.Append("<argument>");
builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>"); builder.Append("<name>")
builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>"); .Append(SecurityElement.Escape(argument.Name ?? string.Empty))
builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>"); .Append("</name>");
builder.Append("<direction>")
.Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
.Append("</direction>");
builder.Append("<relatedStateVariable>")
.Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
.Append("</relatedStateVariable>");
builder.Append("</argument>"); builder.Append("</argument>");
} }
@ -68,17 +76,25 @@ namespace Emby.Dlna.Service
{ {
var sendEvents = item.SendsEvents ? "yes" : "no"; var sendEvents = item.SendsEvents ? "yes" : "no";
builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">"); builder.Append("<stateVariable sendEvents=\"")
.Append(sendEvents)
.Append("\">");
builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>"); builder.Append("<name>")
builder.Append("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>"); .Append(SecurityElement.Escape(item.Name ?? string.Empty))
.Append("</name>");
builder.Append("<dataType>")
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
.Append("</dataType>");
if (item.AllowedValues.Length > 0) if (item.AllowedValues.Count > 0)
{ {
builder.Append("<allowedValueList>"); builder.Append("<allowedValueList>");
foreach (var allowedValue in item.AllowedValues) foreach (var allowedValue in item.AllowedValues)
{ {
builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</allowedValue>"); builder.Append("<allowedValue>")
.Append(SecurityElement.Escape(allowedValue))
.Append("</allowedValue>");
} }
builder.Append("</allowedValueList>"); builder.Append("</allowedValueList>");

View file

@ -3,9 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Events;
using Rssdp; using Rssdp;
using Rssdp.Infrastructure; using Rssdp.Infrastructure;
@ -17,9 +17,17 @@ namespace Emby.Dlna.Ssdp
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private SsdpDeviceLocator _deviceLocator;
private ISsdpCommunicationsServer _commsServer;
private int _listenerCount; private int _listenerCount;
private bool _disposed; private bool _disposed;
public DeviceDiscovery(IServerConfigurationManager config)
{
_config = config;
}
private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal; private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal;
/// <inheritdoc /> /// <inheritdoc />
@ -49,15 +57,6 @@ namespace Emby.Dlna.Ssdp
/// <inheritdoc /> /// <inheritdoc />
public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft; public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
private SsdpDeviceLocator _deviceLocator;
private ISsdpCommunicationsServer _commsServer;
public DeviceDiscovery(IServerConfigurationManager config)
{
_config = config;
}
// Call this method from somewhere in your code to start the search. // Call this method from somewhere in your code to start the search.
public void Start(ISsdpCommunicationsServer communicationsServer) public void Start(ISsdpCommunicationsServer communicationsServer)
{ {

View file

@ -5,7 +5,7 @@ using System.Xml.Linq;
namespace Emby.Dlna.Ssdp namespace Emby.Dlna.Ssdp
{ {
public static class Extensions public static class SsdpExtensions
{ {
public static string GetValue(this XElement container, XName name) public static string GetValue(this XElement container, XName name)
{ {

View file

@ -448,21 +448,21 @@ namespace Emby.Drawing
/// or /// or
/// filename. /// filename.
/// </exception> /// </exception>
public string GetCachePath(string path, string filename) public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename)
{ {
if (string.IsNullOrEmpty(path)) if (path.IsEmpty)
{ {
throw new ArgumentNullException(nameof(path)); throw new ArgumentException("Path can't be empty.", nameof(path));
} }
if (string.IsNullOrEmpty(filename)) if (path.IsEmpty)
{ {
throw new ArgumentNullException(nameof(filename)); throw new ArgumentException("Filename can't be empty.", nameof(filename));
} }
var prefix = filename.Substring(0, 1); var prefix = filename.Slice(0, 1);
return Path.Combine(path, prefix, filename); return Path.Join(path, prefix, filename);
} }
/// <inheritdoc /> /// <inheritdoc />

View file

@ -136,8 +136,8 @@ namespace Emby.Naming.Common
CleanDateTimes = new[] CleanDateTimes = new[]
{ {
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*", @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*" @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
}; };
CleanStrings = new[] CleanStrings = new[]
@ -277,7 +277,7 @@ namespace Emby.Naming.Common
// This isn't a Kodi naming rule, but the expression below causes false positives, // This isn't a Kodi naming rule, but the expression below causes false positives,
// so we make sure this one gets tested first. // so we make sure this one gets tested first.
// "Foo Bar 889" // "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$") new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
{ {
IsNamed = true IsNamed = true
}, },
@ -300,32 +300,32 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming // *** End Kodi Standard Naming
// [bar] Foo - 1 [baz] // [bar] Foo - 1 [baz]
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$") new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d+))[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
// "01.avi" // "01.avi"
new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$") new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
@ -335,34 +335,34 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"([0-9]+)-([0-9]+)"), new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi" // "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "01.blah.avi" // "01.blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.[^\\\/]+$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\.[^\\\/]+$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah" // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "01 episode title.avi" // "01 episode title.avi"
new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>\d{1,3})([^\\\/]*)$") new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "Episode 16", "Episode 16 - Title" // "Episode 16", "Episode 16 - Title"
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
@ -625,17 +625,17 @@ namespace Emby.Naming.Common
AudioBookPartsExpressions = new[] AudioBookPartsExpressions = new[]
{ {
// Detect specified chapters, like CH 01 // Detect specified chapters, like CH 01
@"ch(?:apter)?[\s_-]?(?<chapter>\d+)", @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
// Detect specified parts, like Part 02 // Detect specified parts, like Part 02
@"p(?:ar)?t[\s_-]?(?<part>\d+)", @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)",
// Chapter is often beginning of filename // Chapter is often beginning of filename
@"^(?<chapter>\d+)", "^(?<chapter>[0-9]+)",
// Part if often ending of filename // Part if often ending of filename
@"(?<part>\d+)$", "(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part) // Sometimes named as 0001_005 (chapter_part)
@"(?<chapter>\d+)_(?<part>\d+)", "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number. // Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?<chapter>\d+)" @"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
}; };
var extensions = VideoFileExtensions.ToList(); var extensions = VideoFileExtensions.ToList();
@ -675,16 +675,16 @@ namespace Emby.Naming.Common
MultipleEpisodeExpressions = new string[] MultipleEpisodeExpressions = new string[]
{ {
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$" @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$"
}.Select(i => new EpisodeExpression(i) }.Select(i => new EpisodeExpression(i)
{ {
IsNamed = true IsNamed = true

View file

@ -10,6 +10,15 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -23,10 +32,15 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl> <VersionPrefix>10.7.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> --> <!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->

View file

@ -77,7 +77,7 @@ namespace Emby.Naming.TV
if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase)) if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase))
{ {
var testFilename = filename.Substring(1); var testFilename = filename.AsSpan().Slice(1);
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{ {

View file

@ -1,191 +0,0 @@
#pragma warning disable CS1591
#pragma warning disable SA1402
#pragma warning disable SA1649
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Notifications;
using MediaBrowser.Model.Services;
namespace Emby.Notifications.Api
{
[Route("/Notifications/{UserId}", "GET", Summary = "Gets notifications")]
public class GetNotifications : IReturn<NotificationResult>
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UserId { get; set; } = string.Empty;
[ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsRead { get; set; }
[ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? StartIndex { get; set; }
[ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? Limit { get; set; }
}
public class Notification
{
public string Id { get; set; } = string.Empty;
public string UserId { get; set; } = string.Empty;
public DateTime Date { get; set; }
public bool IsRead { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public NotificationLevel Level { get; set; }
}
public class NotificationResult
{
public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>();
public int TotalRecordCount { get; set; }
}
public class NotificationsSummary
{
public int UnreadCount { get; set; }
public NotificationLevel MaxUnreadNotificationLevel { get; set; }
}
[Route("/Notifications/{UserId}/Summary", "GET", Summary = "Gets a notification summary for a user")]
public class GetNotificationsSummary : IReturn<NotificationsSummary>
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UserId { get; set; } = string.Empty;
}
[Route("/Notifications/Types", "GET", Summary = "Gets notification types")]
public class GetNotificationTypes : IReturn<List<NotificationTypeInfo>>
{
}
[Route("/Notifications/Services", "GET", Summary = "Gets notification types")]
public class GetNotificationServices : IReturn<List<NameIdPair>>
{
}
[Route("/Notifications/Admin", "POST", Summary = "Sends a notification to all admin users")]
public class AddAdminNotification : IReturnVoid
{
[ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
public string Name { get; set; } = string.Empty;
[ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
public string Description { get; set; } = string.Empty;
[ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string? ImageUrl { get; set; }
[ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string? Url { get; set; }
[ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public NotificationLevel Level { get; set; }
}
[Route("/Notifications/{UserId}/Read", "POST", Summary = "Marks notifications as read")]
public class MarkRead : IReturnVoid
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string UserId { get; set; } = string.Empty;
[ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
public string Ids { get; set; } = string.Empty;
}
[Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
public class MarkUnread : IReturnVoid
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string UserId { get; set; } = string.Empty;
[ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
public string Ids { get; set; } = string.Empty;
}
[Authenticated]
public class NotificationsService : IService
{
private readonly INotificationManager _notificationManager;
private readonly IUserManager _userManager;
public NotificationsService(INotificationManager notificationManager, IUserManager userManager)
{
_notificationManager = notificationManager;
_userManager = userManager;
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetNotificationTypes request)
{
return _notificationManager.GetNotificationTypes();
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetNotificationServices request)
{
return _notificationManager.GetNotificationServices().ToList();
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetNotificationsSummary request)
{
return new NotificationsSummary();
}
public Task Post(AddAdminNotification request)
{
// This endpoint really just exists as post of a real with sickbeard
var notification = new NotificationRequest
{
Date = DateTime.UtcNow,
Description = request.Description,
Level = request.Level,
Name = request.Name,
Url = request.Url,
UserIds = _userManager.Users
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
.Select(user => user.Id)
.ToArray()
};
return _notificationManager.SendNotification(notification, CancellationToken.None);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public void Post(MarkRead request)
{
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public void Post(MarkUnread request)
{
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetNotifications request)
{
return new NotificationResult();
}
}
}

View file

@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -13,7 +14,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Notifications; using MediaBrowser.Controller.Notifications;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Activity; using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Notifications; using MediaBrowser.Model.Notifications;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

View file

@ -1,590 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Notifications;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Activity
{
/// <summary>
/// Entry point for the activity logger.
/// </summary>
public sealed class ActivityLogEntryPoint : IServerEntryPoint
{
private readonly ILogger<ActivityLogEntryPoint> _logger;
private readonly IInstallationManager _installationManager;
private readonly ISessionManager _sessionManager;
private readonly ITaskManager _taskManager;
private readonly IActivityManager _activityManager;
private readonly ILocalizationManager _localization;
private readonly ISubtitleManager _subManager;
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="taskManager">The task manager.</param>
/// <param name="activityManager">The activity manager.</param>
/// <param name="localization">The localization manager.</param>
/// <param name="installationManager">The installation manager.</param>
/// <param name="subManager">The subtitle manager.</param>
/// <param name="userManager">The user manager.</param>
public ActivityLogEntryPoint(
ILogger<ActivityLogEntryPoint> logger,
ISessionManager sessionManager,
ITaskManager taskManager,
IActivityManager activityManager,
ILocalizationManager localization,
IInstallationManager installationManager,
ISubtitleManager subManager,
IUserManager userManager)
{
_logger = logger;
_sessionManager = sessionManager;
_taskManager = taskManager;
_activityManager = activityManager;
_localization = localization;
_installationManager = installationManager;
_subManager = subManager;
_userManager = userManager;
}
/// <inheritdoc />
public Task RunAsync()
{
_taskManager.TaskCompleted += OnTaskCompleted;
_installationManager.PluginInstalled += OnPluginInstalled;
_installationManager.PluginUninstalled += OnPluginUninstalled;
_installationManager.PluginUpdated += OnPluginUpdated;
_installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
_sessionManager.SessionStarted += OnSessionStarted;
_sessionManager.AuthenticationFailed += OnAuthenticationFailed;
_sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded;
_sessionManager.SessionEnded += OnSessionEnded;
_sessionManager.PlaybackStart += OnPlaybackStart;
_sessionManager.PlaybackStopped += OnPlaybackStopped;
_subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
_userManager.OnUserCreated += OnUserCreated;
_userManager.OnUserPasswordChanged += OnUserPasswordChanged;
_userManager.OnUserDeleted += OnUserDeleted;
_userManager.OnUserLockedOut += OnUserLockedOut;
return Task.CompletedTask;
}
private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserLockedOutWithName"),
e.Argument.Username),
NotificationType.UserLockedOut.ToString(),
e.Argument.Id)
{
LogSeverity = LogLevel.Error
}).ConfigureAwait(false);
}
private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
e.Provider,
Notifications.NotificationEntryPoint.GetItemName(e.Item)),
"SubtitleDownloadFailure",
Guid.Empty)
{
ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
ShortOverview = e.Exception.Message
}).ConfigureAwait(false);
}
private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
{
var item = e.MediaInfo;
if (item == null)
{
_logger.LogWarning("PlaybackStopped reported with null media info.");
return;
}
if (e.Item != null && e.Item.IsThemeMedia)
{
// Don't report theme song or local trailer playback
return;
}
if (e.Users.Count == 0)
{
return;
}
var user = e.Users[0];
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
user.Username,
GetItemName(item),
e.DeviceName),
GetPlaybackStoppedNotificationType(item.MediaType),
user.Id))
.ConfigureAwait(false);
}
private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
{
var item = e.MediaInfo;
if (item == null)
{
_logger.LogWarning("PlaybackStart reported with null media info.");
return;
}
if (e.Item != null && e.Item.IsThemeMedia)
{
// Don't report theme song or local trailer playback
return;
}
if (e.Users.Count == 0)
{
return;
}
var user = e.Users.First();
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
user.Username,
GetItemName(item),
e.DeviceName),
GetPlaybackNotificationType(item.MediaType),
user.Id))
.ConfigureAwait(false);
}
private static string GetItemName(BaseItemDto item)
{
var name = item.Name;
if (!string.IsNullOrEmpty(item.SeriesName))
{
name = item.SeriesName + " - " + name;
}
if (item.Artists != null && item.Artists.Count > 0)
{
name = item.Artists[0] + " - " + name;
}
return name;
}
private static string GetPlaybackNotificationType(string mediaType)
{
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
return NotificationType.AudioPlayback.ToString();
}
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
return NotificationType.VideoPlayback.ToString();
}
return null;
}
private static string GetPlaybackStoppedNotificationType(string mediaType)
{
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
return NotificationType.AudioPlaybackStopped.ToString();
}
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
return NotificationType.VideoPlaybackStopped.ToString();
}
return null;
}
private async void OnSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
if (string.IsNullOrEmpty(session.UserName))
{
return;
}
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserOfflineFromDevice"),
session.UserName,
session.DeviceName),
"SessionEnded",
session.UserId)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
session.RemoteEndPoint),
}).ConfigureAwait(false);
}
private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
{
var user = e.Argument.User;
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
user.Name),
"AuthenticationSucceeded",
user.Id)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
e.Argument.SessionInfo.RemoteEndPoint),
}).ConfigureAwait(false);
}
private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
e.Argument.Username),
"AuthenticationFailed",
Guid.Empty)
{
LogSeverity = LogLevel.Error,
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
e.Argument.RemoteEndPoint),
}).ConfigureAwait(false);
}
private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserDeletedWithName"),
e.Argument.Username),
"UserDeleted",
Guid.Empty))
.ConfigureAwait(false);
}
private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPasswordChangedWithName"),
e.Argument.Username),
"UserPasswordChanged",
e.Argument.Id))
.ConfigureAwait(false);
}
private async void OnUserCreated(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserCreatedWithName"),
e.Argument.Username),
"UserCreated",
e.Argument.Id))
.ConfigureAwait(false);
}
private async void OnSessionStarted(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
if (string.IsNullOrEmpty(session.UserName))
{
return;
}
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserOnlineFromDevice"),
session.UserName,
session.DeviceName),
"SessionStarted",
session.UserId)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
session.RemoteEndPoint)
}).ConfigureAwait(false);
}
private async void OnPluginUpdated(object sender, InstallationInfo e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginUpdatedWithName"),
e.Name),
NotificationType.PluginUpdateInstalled.ToString(),
Guid.Empty)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
e.Version),
Overview = e.Changelog
}).ConfigureAwait(false);
}
private async void OnPluginUninstalled(object sender, IPlugin e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginUninstalledWithName"),
e.Name),
NotificationType.PluginUninstalled.ToString(),
Guid.Empty))
.ConfigureAwait(false);
}
private async void OnPluginInstalled(object sender, InstallationInfo e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginInstalledWithName"),
e.Name),
NotificationType.PluginInstalled.ToString(),
Guid.Empty)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
e.Version)
}).ConfigureAwait(false);
}
private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
{
var installationInfo = e.InstallationInfo;
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameInstallFailed"),
installationInfo.Name),
NotificationType.InstallationFailed.ToString(),
Guid.Empty)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
installationInfo.Version),
Overview = e.Exception.Message
}).ConfigureAwait(false);
}
private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
{
var result = e.Result;
var task = e.Task;
if (task.ScheduledTask is IConfigurableScheduledTask activityTask
&& !activityTask.IsLogged)
{
return;
}
var time = result.EndTimeUtc - result.StartTimeUtc;
var runningTime = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelRunningTimeValue"),
ToUserFriendlyString(time));
if (result.Status == TaskCompletionStatus.Failed)
{
var vals = new List<string>();
if (!string.IsNullOrEmpty(e.Result.ErrorMessage))
{
vals.Add(e.Result.ErrorMessage);
}
if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
{
vals.Add(e.Result.LongErrorMessage);
}
await CreateLogEntry(new ActivityLog(
string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
NotificationType.TaskFailed.ToString(),
Guid.Empty)
{
LogSeverity = LogLevel.Error,
Overview = string.Join(Environment.NewLine, vals),
ShortOverview = runningTime
}).ConfigureAwait(false);
}
}
private async Task CreateLogEntry(ActivityLog entry)
=> await _activityManager.CreateAsync(entry).ConfigureAwait(false);
/// <inheritdoc />
public void Dispose()
{
_taskManager.TaskCompleted -= OnTaskCompleted;
_installationManager.PluginInstalled -= OnPluginInstalled;
_installationManager.PluginUninstalled -= OnPluginUninstalled;
_installationManager.PluginUpdated -= OnPluginUpdated;
_installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
_sessionManager.SessionStarted -= OnSessionStarted;
_sessionManager.AuthenticationFailed -= OnAuthenticationFailed;
_sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded;
_sessionManager.SessionEnded -= OnSessionEnded;
_sessionManager.PlaybackStart -= OnPlaybackStart;
_sessionManager.PlaybackStopped -= OnPlaybackStopped;
_subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
_userManager.OnUserCreated -= OnUserCreated;
_userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
_userManager.OnUserDeleted -= OnUserDeleted;
_userManager.OnUserLockedOut -= OnUserLockedOut;
}
/// <summary>
/// Constructs a user-friendly string for this TimeSpan instance.
/// </summary>
private static string ToUserFriendlyString(TimeSpan span)
{
const int DaysInYear = 365;
const int DaysInMonth = 30;
// Get each non-zero value from TimeSpan component
var values = new List<string>();
// Number of years
int days = span.Days;
if (days >= DaysInYear)
{
int years = days / DaysInYear;
values.Add(CreateValueString(years, "year"));
days %= DaysInYear;
}
// Number of months
if (days >= DaysInMonth)
{
int months = days / DaysInMonth;
values.Add(CreateValueString(months, "month"));
days = days % DaysInMonth;
}
// Number of days
if (days >= 1)
{
values.Add(CreateValueString(days, "day"));
}
// Number of hours
if (span.Hours >= 1)
{
values.Add(CreateValueString(span.Hours, "hour"));
}
// Number of minutes
if (span.Minutes >= 1)
{
values.Add(CreateValueString(span.Minutes, "minute"));
}
// Number of seconds (include when 0 if no other components included)
if (span.Seconds >= 1 || values.Count == 0)
{
values.Add(CreateValueString(span.Seconds, "second"));
}
// Combine values into string
var builder = new StringBuilder();
for (int i = 0; i < values.Count; i++)
{
if (builder.Length > 0)
{
builder.Append(i == values.Count - 1 ? " and " : ", ");
}
builder.Append(values[i]);
}
// Return result
return builder.ToString();
}
/// <summary>
/// Constructs a string description of a time-span value.
/// </summary>
/// <param name="value">The value of this item.</param>
/// <param name="description">The name of this item (singular form).</param>
private static string CreateValueString(int value, string description)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0:#,##0} {1}",
value,
value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description));
}
}
}

View file

@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error loading configuration file: {path}", path); Logger.LogError(ex, "Error loading configuration file: {Path}", path);
return Activator.CreateInstance(configurationType); return Activator.CreateInstance(configurationType);
} }

View file

@ -1,3 +1,5 @@
#nullable enable
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -22,7 +24,7 @@ namespace Emby.Server.Implementations.AppBase
{ {
object configuration; object configuration;
byte[] buffer = null; byte[]? buffer = null;
// Use try/catch to avoid the extra file system lookup using File.Exists // Use try/catch to avoid the extra file system lookup using File.Exists
try try
@ -36,19 +38,23 @@ namespace Emby.Server.Implementations.AppBase
configuration = Activator.CreateInstance(type); configuration = Activator.CreateInstance(type);
} }
using var stream = new MemoryStream(); using var stream = new MemoryStream(buffer?.Length ?? 0);
xmlSerializer.SerializeToStream(configuration, stream); xmlSerializer.SerializeToStream(configuration, stream);
// Take the object we just got and serialize it back to bytes // Take the object we just got and serialize it back to bytes
var newBytes = stream.ToArray(); byte[] newBytes = stream.GetBuffer();
int newBytesLen = (int)stream.Length;
// If the file didn't exist before, or if something has changed, re-save // If the file didn't exist before, or if something has changed, re-save
if (buffer == null || !buffer.SequenceEqual(newBytes)) if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
{ {
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path));
// Save it after load in case we got new items // Save it after load in case we got new items
File.WriteAllBytes(path, newBytes); using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
fs.Write(newBytes, 0, newBytesLen);
}
} }
return configuration; return configuration;

View file

@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
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;
@ -38,23 +37,23 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization; using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session; using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV; using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.Updates;
using Emby.Server.Implementations.SyncPlay; using Jellyfin.Api.Helpers;
using MediaBrowser.Api;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events; using MediaBrowser.Common.Events;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
@ -73,13 +72,14 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.TV;
using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
@ -90,20 +90,19 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Subtitles; using MediaBrowser.Providers.Subtitles;
using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime; using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations namespace Emby.Server.Implementations
{ {
@ -124,14 +123,18 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder; private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
private IHttpServer _httpServer; private IHttpClientFactory _httpClientFactory;
private IHttpClient _httpClient; private IWebSocketManager _webSocketManager;
private string[] _urlPrefixes;
/// <summary> /// <summary>
/// Gets a value indicating whether this instance can self restart. /// Gets a value indicating whether this instance can self restart.
/// </summary> /// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null; public bool CanSelfRestart => _startupOptions.RestartPath != null;
public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser public virtual bool CanLaunchWebBrowser
{ {
get get
@ -175,6 +178,8 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
protected ILogger<ApplicationHost> Logger { get; } protected ILogger<ApplicationHost> Logger { get; }
protected IServiceCollection ServiceCollection { get; }
private IPlugin[] _plugins; private IPlugin[] _plugins;
/// <summary> /// <summary>
@ -192,7 +197,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the application paths. /// Gets or sets the application paths.
/// </summary> /// </summary>
/// <value>The application paths.</value> /// <value>The application paths.</value>
protected ServerApplicationPaths ApplicationPaths { get; set; } protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary> /// <summary>
/// Gets or sets all concrete types. /// Gets or sets all concrete types.
@ -236,13 +241,15 @@ namespace Emby.Server.Implementations
/// Initializes a new instance of the <see cref="ApplicationHost" /> class. /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
/// </summary> /// </summary>
public ApplicationHost( public ApplicationHost(
ServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStartupOptions options, IStartupOptions options,
IFileSystem fileSystem, IFileSystem fileSystem,
INetworkManager networkManager) INetworkManager networkManager,
IServiceCollection serviceCollection)
{ {
_xmlSerializer = new MyXmlSerializer(); _xmlSerializer = new MyXmlSerializer();
ServiceCollection = serviceCollection;
_networkManager = networkManager; _networkManager = networkManager;
networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
@ -273,6 +280,10 @@ namespace Emby.Server.Implementations
Password = ServerConfigurationManager.Configuration.CertificatePassword Password = ServerConfigurationManager.Configuration.CertificatePassword
}; };
Certificate = GetCertificate(CertificateInfo); Certificate = GetCertificate(CertificateInfo);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
} }
public string ExpandVirtualPath(string path) public string ExpandVirtualPath(string path)
@ -302,16 +313,16 @@ namespace Emby.Server.Implementations
} }
/// <inheritdoc /> /// <inheritdoc />
public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version; public Version ApplicationVersion { get; }
/// <inheritdoc /> /// <inheritdoc />
public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3); public string ApplicationVersionString { get; }
/// <summary> /// <summary>
/// Gets the current application user agent. /// Gets the current application user agent.
/// </summary> /// </summary>
/// <value>The application user agent.</value> /// <value>The application user agent.</value>
public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString; public string ApplicationUserAgent { get; }
/// <summary> /// <summary>
/// Gets the email address for use within a comment section of a user agent field. /// Gets the email address for use within a comment section of a user agent field.
@ -442,8 +453,7 @@ namespace Emby.Server.Implementations
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");
_httpServer.GlobalResponse = null; CoreStartupHasCompleted = true;
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);
@ -466,7 +476,7 @@ namespace Emby.Server.Implementations
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Init(IServiceCollection serviceCollection) public void Init()
{ {
HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@ -484,12 +494,10 @@ namespace Emby.Server.Implementations
foreach (var plugin in Plugins) foreach (var plugin in Plugins)
{ {
pluginBuilder.AppendLine( pluginBuilder.Append(plugin.Name)
string.Format( .Append(' ')
CultureInfo.InvariantCulture, .Append(plugin.Version)
"{0} {1}", .AppendLine();
plugin.Name,
plugin.Version));
} }
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString()); Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
@ -497,147 +505,141 @@ namespace Emby.Server.Implementations
DiscoverTypes(); DiscoverTypes();
RegisterServices(serviceCollection); RegisterServices();
} }
public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
=> _httpServer.RequestHandler(context);
/// <summary> /// <summary>
/// Registers services/resources with the service collection that will be available via DI. /// Registers services/resources with the service collection that will be available via DI.
/// </summary> /// </summary>
protected virtual void RegisterServices(IServiceCollection serviceCollection) protected virtual void RegisterServices()
{ {
serviceCollection.AddSingleton(_startupOptions); ServiceCollection.AddSingleton(_startupOptions);
serviceCollection.AddMemoryCache(); ServiceCollection.AddMemoryCache();
serviceCollection.AddSingleton(ConfigurationManager); ServiceCollection.AddSingleton(ConfigurationManager);
serviceCollection.AddSingleton<IApplicationHost>(this); ServiceCollection.AddSingleton<IApplicationHost>(this);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>(); ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
serviceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton(_fileSystemManager);
serviceCollection.AddSingleton<TvdbClientManager>(); ServiceCollection.AddSingleton<TvdbClientManager>();
serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>(); ServiceCollection.AddSingleton(_networkManager);
serviceCollection.AddSingleton(_networkManager); ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
serviceCollection.AddSingleton<IIsoManager, IsoManager>(); ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
serviceCollection.AddSingleton<ITaskManager, TaskManager>(); ServiceCollection.AddSingleton(_xmlSerializer);
serviceCollection.AddSingleton(_xmlSerializer); ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
serviceCollection.AddSingleton<IStreamHelper, StreamHelper>(); ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>(); ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
serviceCollection.AddSingleton<ISocketFactory, SocketFactory>(); ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
serviceCollection.AddSingleton<IInstallationManager, InstallationManager>(); ServiceCollection.AddSingleton<IZipClient, ZipClient>();
serviceCollection.AddSingleton<IZipClient, ZipClient>(); ServiceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>(); ServiceCollection.AddSingleton(ServerConfigurationManager);
serviceCollection.AddSingleton<IServerApplicationHost>(this); ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton(ServerConfigurationManager); ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>)); ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
// TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>)); ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<IMediaEncoder>(provider =>
ActivatorUtilities.CreateInstance<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(provider, _startupOptions.FFmpegPath ?? string.Empty));
// 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>));
serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>)); ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>)); ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>(); ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
serviceCollection.AddSingleton<IMusicManager, MusicManager>(); ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>(); ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>(); ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
serviceCollection.AddSingleton<ServiceController>(); ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>(); ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>(); ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>(); ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
serviceCollection.AddSingleton<IProviderManager, ProviderManager>(); ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>)); ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
serviceCollection.AddSingleton<IDtoService, DtoService>(); ServiceCollection.AddSingleton<IDtoService, DtoService>();
serviceCollection.AddSingleton<IChannelManager, ChannelManager>(); ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
serviceCollection.AddSingleton<ISessionManager, SessionManager>(); ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>(); ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>(); ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
serviceCollection.AddSingleton<LiveTvDtoService>(); ServiceCollection.AddSingleton<LiveTvDtoService>();
serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>(); ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
serviceCollection.AddSingleton<INotificationManager, NotificationManager>(); ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>(); ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
serviceCollection.AddSingleton<ISessionContext, SessionContext>(); ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
serviceCollection.AddSingleton<IAuthService, AuthService>(); ServiceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>(); ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
serviceCollection.AddSingleton<EncodingHelper>(); ServiceCollection.AddSingleton<EncodingHelper>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
ServiceCollection.AddSingleton<TranscodingJobHelper>();
ServiceCollection.AddScoped<MediaInfoHelper>();
ServiceCollection.AddScoped<AudioHelper>();
ServiceCollection.AddScoped<DynamicHlsHelper>();
} }
/// <summary> /// <summary>
@ -651,10 +653,9 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>(); _mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>(); _sessionManager = Resolve<ISessionManager>();
_httpServer = Resolve<IHttpServer>(); _httpClientFactory = Resolve<IHttpClientFactory>();
_httpClient = Resolve<IHttpClient>(); _webSocketManager = Resolve<IWebSocketManager>();
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties(); SetStaticProperties();
@ -754,7 +755,6 @@ namespace Emby.Server.Implementations
CollectionFolder.XmlSerializer = _xmlSerializer; CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>(); CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this; CollectionFolder.ApplicationHost = this;
AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
} }
/// <summary> /// <summary>
@ -774,7 +774,8 @@ namespace Emby.Server.Implementations
.Where(i => i != null) .Where(i => i != null)
.ToArray(); .ToArray();
_httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes()); _urlPrefixes = GetUrlPrefixes().ToArray();
_webSocketManager.Init(GetExports<IWebSocketListener>());
Resolve<ILibraryManager>().AddParts( Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(), GetExports<IResolverIgnoreRule>(),
@ -799,7 +800,6 @@ namespace Emby.Server.Implementations
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>()); Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
} }
@ -839,6 +839,8 @@ namespace Emby.Server.Implementations
{ {
hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s)); hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
} }
plugin.RegisterServices(ServiceCollection);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -873,6 +875,11 @@ namespace Emby.Server.Implementations
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
continue; continue;
} }
catch (TypeLoadException ex)
{
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
continue;
}
foreach (Type type in exportedTypes) foreach (Type type in exportedTypes)
{ {
@ -934,7 +941,7 @@ namespace Emby.Server.Implementations
} }
} }
if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
{ {
requiresRestart = true; requiresRestart = true;
} }
@ -1111,12 +1118,6 @@ namespace Emby.Server.Implementations
} }
} }
// Include composable parts in the Api assembly
yield return typeof(ApiEntryPoint).Assembly;
// Include composable parts in the Dashboard assembly
yield return typeof(DashboardService).Assembly;
// Include composable parts in the Model assembly // Include composable parts in the Model assembly
yield return typeof(SystemInfo).Assembly; yield return typeof(SystemInfo).Assembly;
@ -1232,7 +1233,7 @@ namespace Emby.Server.Implementations
return null; return null;
} }
return GetLocalApiUrl(addresses.First()); return GetLocalApiUrl(addresses[0]);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -1305,13 +1306,13 @@ namespace Emby.Server.Implementations
var addresses = ServerConfigurationManager var addresses = ServerConfigurationManager
.Configuration .Configuration
.LocalNetworkAddresses .LocalNetworkAddresses
.Select(NormalizeConfiguredLocalAddress) .Select(x => NormalizeConfiguredLocalAddress(x))
.Where(i => i != null) .Where(i => i != null)
.ToList(); .ToList();
if (addresses.Count == 0) if (addresses.Count == 0)
{ {
addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces)); addresses.AddRange(_networkManager.GetLocalIpAddresses());
} }
var resultList = new List<IPAddress>(); var resultList = new List<IPAddress>();
@ -1326,8 +1327,7 @@ namespace Emby.Server.Implementations
} }
} }
var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
if (valid)
{ {
resultList.Add(address); resultList.Add(address);
@ -1341,13 +1341,12 @@ namespace Emby.Server.Implementations
return resultList; return resultList;
} }
public IPAddress NormalizeConfiguredLocalAddress(string address) public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
{ {
var index = address.Trim('/').IndexOf('/'); var index = address.Trim('/').IndexOf('/');
if (index != -1) if (index != -1)
{ {
address = address.Substring(index + 1); address = address.Slice(index + 1);
} }
if (IPAddress.TryParse(address.Trim('/'), out IPAddress result)) if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
@ -1377,25 +1376,17 @@ namespace Emby.Server.Implementations
try try
{ {
using (var response = await _httpClient.SendAsync( using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
new HttpRequestOptions using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
{ .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
Url = apiUrl,
LogErrorResponseBody = false,
BufferContent = false,
CancellationToken = cancellationToken
}, HttpMethod.Post).ConfigureAwait(false))
{
using (var reader = new StreamReader(response.Content))
{
var result = await reader.ReadToEndAsync().ConfigureAwait(false);
var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
_validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid); var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false);
return valid; var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
}
} _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
return valid;
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@ -1473,6 +1464,20 @@ namespace Emby.Server.Implementations
_plugins = list.ToArray(); _plugins = list.ToArray();
} }
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes
.Where(i => typeof(ControllerBase).IsAssignableFrom(i))
.Select(i => i.Assembly)
.Distinct();
foreach (var assembly in assemblies)
{
Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName);
yield return assembly;
}
}
public virtual void LaunchUrl(string url) public virtual void LaunchUrl(string url)
{ {
if (!CanLaunchWebBrowser) if (!CanLaunchWebBrowser)

View file

@ -1,5 +1,7 @@
using System; using System;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Browser namespace Emby.Server.Implementations.Browser
@ -24,7 +26,7 @@ namespace Emby.Server.Implementations.Browser
/// <param name="appHost">The app host.</param> /// <param name="appHost">The app host.</param>
public static void OpenSwaggerPage(IServerApplicationHost appHost) public static void OpenSwaggerPage(IServerApplicationHost appHost)
{ {
TryOpenUrl(appHost, "/swagger/index.html"); TryOpenUrl(appHost, "/api-docs/swagger");
} }
/// <summary> /// <summary>

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