mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-22 08:45:23 +03:00
Compare commits
62 Commits
v10.7.0-rc
...
v10.5.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3d048a0d9 | ||
|
|
21a6c2e1e2 | ||
|
|
7873fe22fb | ||
|
|
1f5625d7f9 | ||
|
|
a0b053e4a1 | ||
|
|
3af63bf439 | ||
|
|
77f72dc607 | ||
|
|
16d9318e08 | ||
|
|
d8f865e93c | ||
|
|
3044dfc114 | ||
|
|
ace1e70c63 | ||
|
|
6a9a677111 | ||
|
|
cc35876f6b | ||
|
|
5611b2c038 | ||
|
|
163fb94bde | ||
|
|
5c4326daf4 | ||
|
|
2d369ca614 | ||
|
|
3c05079333 | ||
|
|
0c204f4706 | ||
|
|
1e3a524a7a | ||
|
|
99e22c499d | ||
|
|
dbbf97e588 | ||
|
|
4e9df69ffd | ||
|
|
7f38ef4c3c | ||
|
|
16549dead9 | ||
|
|
9bd1a9d19c | ||
|
|
67194994f9 | ||
|
|
c249e15f48 | ||
|
|
48ba5a9a30 | ||
|
|
dd13f8d16a | ||
|
|
b43a8a56dc | ||
|
|
3ec18f085e | ||
|
|
f2728b5a92 | ||
|
|
ee47a75f9f | ||
|
|
66e7e8bcd2 | ||
|
|
0de3a9465e | ||
|
|
b2f7417365 | ||
|
|
bf0c07abfe | ||
|
|
3a4cd01b13 | ||
|
|
7059761806 | ||
|
|
2cde59af44 | ||
|
|
52a850cc9b | ||
|
|
54435a1243 | ||
|
|
899be44388 | ||
|
|
354079862e | ||
|
|
01db9af821 | ||
|
|
9da2635d86 | ||
|
|
4caa597cde | ||
|
|
da34bd940e | ||
|
|
54efde4073 | ||
|
|
02cfa15582 | ||
|
|
0e171d794c | ||
|
|
d5f2384375 | ||
|
|
816b3f5c2e | ||
|
|
a234388552 | ||
|
|
30d38bd5c6 | ||
|
|
9f49fe2e99 | ||
|
|
f720a0fca2 | ||
|
|
aadff77531 | ||
|
|
89fc5aa11a | ||
|
|
cb91595a24 | ||
|
|
6233bae7f0 |
@@ -1,59 +0,0 @@
|
|||||||
parameters:
|
|
||||||
- name: LinuxImage
|
|
||||||
type: string
|
|
||||||
default: "ubuntu-latest"
|
|
||||||
- name: GeneratorVersion
|
|
||||||
type: string
|
|
||||||
default: "5.0.0-beta2"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- job: GenerateApiClients
|
|
||||||
displayName: 'Generate Api Clients'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
|
||||||
dependsOn: Test
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: "${{ parameters.LinuxImage }}"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: DownloadPipelineArtifact@2
|
|
||||||
displayName: 'Download OpenAPI Spec Artifact'
|
|
||||||
inputs:
|
|
||||||
source: 'current'
|
|
||||||
artifact: "OpenAPI Spec"
|
|
||||||
path: "$(System.ArtifactsDirectory)/openapispec"
|
|
||||||
runVersion: "latest"
|
|
||||||
|
|
||||||
- task: CmdLine@2
|
|
||||||
displayName: 'Download OpenApi Generator'
|
|
||||||
inputs:
|
|
||||||
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
|
|
||||||
|
|
||||||
## Authenticate with npm registry
|
|
||||||
- task: npmAuthenticate@0
|
|
||||||
inputs:
|
|
||||||
workingFile: ./.npmrc
|
|
||||||
customEndpoint: 'jellyfin-bot for NPM'
|
|
||||||
|
|
||||||
## Generate npm api client
|
|
||||||
- task: CmdLine@2
|
|
||||||
displayName: 'Build stable typescript axios client'
|
|
||||||
inputs:
|
|
||||||
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
|
|
||||||
|
|
||||||
## Run npm install
|
|
||||||
- task: Npm@1
|
|
||||||
displayName: 'Install npm dependencies'
|
|
||||||
inputs:
|
|
||||||
command: install
|
|
||||||
workingDir: ./apiclient/generated/typescript/axios
|
|
||||||
|
|
||||||
## Publish npm packages
|
|
||||||
- task: Npm@1
|
|
||||||
displayName: 'Publish stable typescript axios client'
|
|
||||||
inputs:
|
|
||||||
command: custom
|
|
||||||
customCommand: publish --access public
|
|
||||||
publishRegistry: useExternalRegistry
|
|
||||||
publishEndpoint: 'jellyfin-bot for NPM'
|
|
||||||
workingDir: ./apiclient/generated/typescript/axios
|
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
parameters:
|
parameters:
|
||||||
- name: Packages
|
- name: Packages
|
||||||
type: object
|
type: object
|
||||||
default: {}
|
default: {}
|
||||||
- name: LinuxImage
|
- name: LinuxImage
|
||||||
type: string
|
type: string
|
||||||
default: "ubuntu-latest"
|
default: "ubuntu-latest"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.100
|
default: 3.1.100
|
||||||
|
|
||||||
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 }}:
|
||||||
@@ -25,7 +23,7 @@ jobs:
|
|||||||
NugetPackageName: ${{ Package.value.NugetPackageName }}
|
NugetPackageName: ${{ Package.value.NugetPackageName }}
|
||||||
AssemblyFileName: ${{ Package.value.AssemblyFileName }}
|
AssemblyFileName: ${{ Package.value.AssemblyFileName }}
|
||||||
maxParallel: 2
|
maxParallel: 2
|
||||||
|
dependsOn: MainBuild
|
||||||
steps:
|
steps:
|
||||||
- checkout: none
|
- checkout: none
|
||||||
|
|
||||||
@@ -35,34 +33,26 @@ jobs:
|
|||||||
packageType: sdk
|
packageType: sdk
|
||||||
version: ${{ parameters.DotNetSdkVersion }}
|
version: ${{ parameters.DotNetSdkVersion }}
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
|
||||||
displayName: 'Install ABI CompatibilityChecker Tool'
|
|
||||||
inputs:
|
|
||||||
command: custom
|
|
||||||
custom: tool
|
|
||||||
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"
|
||||||
enabled: false
|
|
||||||
inputs:
|
inputs:
|
||||||
source: "specific"
|
source: "specific"
|
||||||
artifact: "$(NugetPackageName)"
|
artifact: "$(NugetPackageName)"
|
||||||
@@ -73,21 +63,34 @@ 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"
|
||||||
enabled: false
|
|
||||||
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
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
- task: DownloadGitHubRelease@0
|
||||||
displayName: 'Execute ABI Compatibility Check Tool'
|
displayName: "Download ABI Compatibility Check Tool"
|
||||||
enabled: false
|
|
||||||
inputs:
|
inputs:
|
||||||
command: custom
|
connection: Jellyfin Release Download
|
||||||
custom: compat
|
userRepository: EraYaN/dotnet-compatibility
|
||||||
arguments: 'current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
|
defaultVersionType: "latest"
|
||||||
|
itemPattern: "**-ci.zip"
|
||||||
|
downloadPath: "$(System.ArtifactsDirectory)"
|
||||||
|
|
||||||
|
- task: ExtractFiles@1
|
||||||
|
displayName: "Extract ABI Compatibility Check Tool"
|
||||||
|
inputs:
|
||||||
|
archiveFilePatterns: "$(System.ArtifactsDirectory)/*-ci.zip"
|
||||||
|
destinationFolder: $(System.ArtifactsDirectory)/tools
|
||||||
|
cleanDestinationFolder: true
|
||||||
|
|
||||||
|
# The `--warnings-only` switch will swallow the return code and not emit any errors.
|
||||||
|
- task: CmdLine@2
|
||||||
|
displayName: "Execute ABI Compatibility Check Tool"
|
||||||
|
inputs:
|
||||||
|
script: "dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only"
|
||||||
workingDirectory: $(System.ArtifactsDirectory)
|
workingDirectory: $(System.ArtifactsDirectory)
|
||||||
@@ -1,93 +1,101 @@
|
|||||||
parameters:
|
parameters:
|
||||||
LinuxImage: 'ubuntu-latest'
|
LinuxImage: "ubuntu-latest"
|
||||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
RestoreBuildProjects: "Jellyfin.Server/Jellyfin.Server.csproj"
|
||||||
DotNetSdkVersion: 5.0.100
|
DotNetSdkVersion: 3.1.100
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Build
|
- job: MainBuild
|
||||||
displayName: Build
|
displayName: Main Build
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
Release:
|
Release:
|
||||||
BuildConfiguration: Release
|
BuildConfiguration: Release
|
||||||
Debug:
|
Debug:
|
||||||
BuildConfiguration: Debug
|
BuildConfiguration: Debug
|
||||||
|
maxParallel: 2
|
||||||
pool:
|
pool:
|
||||||
vmImage: '${{ parameters.LinuxImage }}'
|
vmImage: "${{ parameters.LinuxImage }}"
|
||||||
steps:
|
steps:
|
||||||
- checkout: self
|
- checkout: self
|
||||||
clean: true
|
clean: true
|
||||||
submodules: true
|
submodules: true
|
||||||
persistCredentials: true
|
persistCredentials: true
|
||||||
|
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: CmdLine@2
|
||||||
displayName: 'Download Web Branch'
|
displayName: "Clone Web Client (Master, Release, or Tag)"
|
||||||
condition: in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')
|
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||||
inputs:
|
inputs:
|
||||||
path: '$(Agent.TempDirectory)'
|
script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
|
||||||
artifact: 'jellyfin-web-production'
|
|
||||||
source: 'specific'
|
|
||||||
project: 'jellyfin'
|
|
||||||
pipeline: 'Jellyfin Web'
|
|
||||||
runBranch: variables['Build.SourceBranch']
|
|
||||||
|
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: CmdLine@2
|
||||||
displayName: 'Download Web Target'
|
displayName: "Clone Web Client (PR)"
|
||||||
condition: eq(variables['Build.Reason'], 'PullRequest')
|
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
|
||||||
inputs:
|
inputs:
|
||||||
path: '$(Agent.TempDirectory)'
|
script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
|
||||||
artifact: 'jellyfin-web-production'
|
|
||||||
source: 'specific'
|
|
||||||
project: 'jellyfin'
|
|
||||||
pipeline: 'Jellyfin Web'
|
|
||||||
runBranch: variables['System.PullRequest.TargetBranch']
|
|
||||||
|
|
||||||
- task: ExtractFiles@1
|
- task: NodeTool@0
|
||||||
displayName: 'Extract Web Client'
|
displayName: "Install Node"
|
||||||
|
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Agent.TempDirectory)/*.zip'
|
versionSpec: "10.x"
|
||||||
destinationFolder: '$(Build.SourcesDirectory)/MediaBrowser.WebDashboard'
|
|
||||||
cleanDestinationFolder: false
|
- task: CmdLine@2
|
||||||
|
displayName: "Build Web Client"
|
||||||
|
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||||
|
inputs:
|
||||||
|
script: yarn install
|
||||||
|
workingDirectory: $(Agent.TempDirectory)/jellyfin-web
|
||||||
|
|
||||||
|
- task: CopyFiles@2
|
||||||
|
displayName: "Copy Web Client"
|
||||||
|
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||||
|
inputs:
|
||||||
|
sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
|
||||||
|
contents: "**"
|
||||||
|
targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
|
||||||
|
cleanTargetFolder: true
|
||||||
|
overWrite: true
|
||||||
|
flattenFolders: false
|
||||||
|
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Update DotNet'
|
displayName: "Update DotNet"
|
||||||
inputs:
|
inputs:
|
||||||
packageType: sdk
|
packageType: sdk
|
||||||
version: ${{ parameters.DotNetSdkVersion }}
|
version: ${{ parameters.DotNetSdkVersion }}
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
- task: DotNetCoreCLI@2
|
||||||
displayName: 'Publish Server'
|
displayName: "Publish Server"
|
||||||
inputs:
|
inputs:
|
||||||
command: publish
|
command: publish
|
||||||
publishWebProjects: false
|
publishWebProjects: false
|
||||||
projects: '${{ parameters.RestoreBuildProjects }}'
|
projects: "${{ parameters.RestoreBuildProjects }}"
|
||||||
arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
|
arguments: "--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)"
|
||||||
zipAfterPublish: false
|
zipAfterPublish: false
|
||||||
|
|
||||||
- task: PublishPipelineArtifact@1
|
- task: PublishPipelineArtifact@0
|
||||||
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@1
|
- task: PublishPipelineArtifact@0
|
||||||
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@1
|
- task: PublishPipelineArtifact@0
|
||||||
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@1
|
- task: PublishPipelineArtifact@0
|
||||||
displayName: 'Publish Artifact Common'
|
displayName: "Publish Artifact Common"
|
||||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||||
inputs:
|
inputs:
|
||||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
|
targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll"
|
||||||
artifactName: 'Jellyfin.Common'
|
artifactName: "Jellyfin.Common"
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
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
|
|
||||||
Linux.amd64-musl:
|
|
||||||
BuildConfiguration: linux.amd64-musl
|
|
||||||
Linux.arm64:
|
|
||||||
BuildConfiguration: linux.arm64
|
|
||||||
Linux.armhf:
|
|
||||||
BuildConfiguration: linux.armhf
|
|
||||||
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: OpenAPISpec
|
|
||||||
dependsOn: Test
|
|
||||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
|
|
||||||
displayName: 'Push OpenAPI Spec to repository'
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: 'ubuntu-latest'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: DownloadPipelineArtifact@2
|
|
||||||
displayName: 'Download OpenAPI Spec'
|
|
||||||
inputs:
|
|
||||||
source: 'current'
|
|
||||||
artifact: "OpenAPI Spec"
|
|
||||||
path: "$(System.ArtifactsDirectory)/openapispec"
|
|
||||||
runVersion: "latest"
|
|
||||||
|
|
||||||
- task: SSH@0
|
|
||||||
displayName: 'Create target directory on repository server'
|
|
||||||
inputs:
|
|
||||||
sshEndpoint: repository
|
|
||||||
runOptions: 'inline'
|
|
||||||
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
|
|
||||||
|
|
||||||
- task: CopyFilesOverSSH@0
|
|
||||||
displayName: 'Upload artifacts to repository server'
|
|
||||||
inputs:
|
|
||||||
sshEndpoint: repository
|
|
||||||
sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
|
|
||||||
contents: 'openapi.json'
|
|
||||||
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
|
|
||||||
|
|
||||||
- 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: nohup sudo /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: nohup sudo /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'
|
|
||||||
|
|
||||||
variables:
|
|
||||||
- name: JellyfinVersion
|
|
||||||
value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: UseDotNet@2
|
|
||||||
displayName: 'Use .NET 5.0 sdk'
|
|
||||||
inputs:
|
|
||||||
packageType: 'sdk'
|
|
||||||
version: '5.0.x'
|
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
|
||||||
displayName: 'Build Stable Nuget packages'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
|
||||||
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: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
|
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
|
||||||
displayName: 'Build Unstable Nuget packages'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
|
||||||
inputs:
|
|
||||||
command: 'custom'
|
|
||||||
projects: |
|
|
||||||
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: NuGetCommand@2
|
|
||||||
displayName: 'Push Nuget packages to stable feed'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
|
||||||
inputs:
|
|
||||||
command: 'push'
|
|
||||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
|
|
||||||
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
|
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
parameters:
|
parameters:
|
||||||
- name: ImageNames
|
- name: ImageNames
|
||||||
type: object
|
type: object
|
||||||
default:
|
default:
|
||||||
Linux: "ubuntu-latest"
|
Linux: "ubuntu-latest"
|
||||||
Windows: "windows-latest"
|
Windows: "windows-latest"
|
||||||
macOS: "macos-latest"
|
macOS: "macos-latest"
|
||||||
- name: TestProjects
|
- name: TestProjects
|
||||||
type: string
|
type: string
|
||||||
default: "tests/**/*Tests.csproj"
|
default: "tests/**/*Tests.csproj"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.100
|
default: 3.1.100
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Test
|
- job: MainTest
|
||||||
displayName: Test
|
displayName: Main Test
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
${{ each imageName in parameters.ImageNames }}:
|
${{ each imageName in parameters.ImageNames }}:
|
||||||
${{ imageName.key }}:
|
${{ imageName.key }}:
|
||||||
ImageName: ${{ imageName.value }}
|
ImageName: ${{ imageName.value }}
|
||||||
|
maxParallel: 3
|
||||||
pool:
|
pool:
|
||||||
vmImage: "$(ImageName)"
|
vmImage: "$(ImageName)"
|
||||||
steps:
|
steps:
|
||||||
@@ -28,52 +29,25 @@ jobs:
|
|||||||
submodules: true
|
submodules: true
|
||||||
persistCredentials: false
|
persistCredentials: false
|
||||||
|
|
||||||
# This is required for the SonarCloud analyzer
|
|
||||||
- task: UseDotNet@2
|
|
||||||
displayName: "Install .NET SDK 5.x"
|
|
||||||
condition: eq(variables['ImageName'], 'ubuntu-latest')
|
|
||||||
inputs:
|
|
||||||
packageType: sdk
|
|
||||||
version: '5.x'
|
|
||||||
|
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: "Update DotNet"
|
displayName: "Update DotNet"
|
||||||
inputs:
|
inputs:
|
||||||
packageType: sdk
|
packageType: sdk
|
||||||
version: ${{ parameters.DotNetSdkVersion }}
|
version: ${{ parameters.DotNetSdkVersion }}
|
||||||
|
|
||||||
- task: SonarCloudPrepare@1
|
|
||||||
displayName: 'Prepare analysis on SonarCloud'
|
|
||||||
condition: eq(variables['ImageName'], 'ubuntu-latest')
|
|
||||||
enabled: false
|
|
||||||
inputs:
|
|
||||||
SonarCloud: 'Sonarcloud for Jellyfin'
|
|
||||||
organization: 'jellyfin'
|
|
||||||
projectKey: 'jellyfin_jellyfin'
|
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
- task: DotNetCoreCLI@2
|
||||||
displayName: 'Run CLI Tests'
|
displayName: Run .NET Core CLI tests
|
||||||
inputs:
|
inputs:
|
||||||
command: "test"
|
command: "test"
|
||||||
projects: ${{ parameters.TestProjects }}
|
projects: ${{ parameters.TestProjects }}
|
||||||
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
|
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
|
||||||
publishTestResults: true
|
publishTestResults: true
|
||||||
testRunTitle: $(Agent.JobName)
|
testRunTitle: $(Agent.JobName)
|
||||||
workingDirectory: "$(Build.SourcesDirectory)"
|
workingDirectory: "$(Build.SourcesDirectory)"
|
||||||
|
|
||||||
- task: SonarCloudAnalyze@1
|
|
||||||
displayName: 'Run Code Analysis'
|
|
||||||
condition: eq(variables['ImageName'], 'ubuntu-latest')
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
- task: SonarCloudPublish@1
|
|
||||||
displayName: 'Publish Quality Gate Result'
|
|
||||||
condition: eq(variables['ImageName'], 'ubuntu-latest')
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
- 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: ReportGenerator (merge)
|
||||||
inputs:
|
inputs:
|
||||||
reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
|
reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
|
||||||
targetdir: "$(Agent.TempDirectory)/merged/"
|
targetdir: "$(Agent.TempDirectory)/merged/"
|
||||||
@@ -82,17 +56,10 @@ jobs:
|
|||||||
## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
|
## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
|
||||||
- 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
|
||||||
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/net5.0/openapi.json"
|
|
||||||
artifactName: 'OpenAPI Spec'
|
|
||||||
|
|||||||
82
.ci/azure-pipelines-windows.yml
Normal file
82
.ci/azure-pipelines-windows.yml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
parameters:
|
||||||
|
WindowsImage: "windows-latest"
|
||||||
|
TestProjects: "tests/**/*Tests.csproj"
|
||||||
|
DotNetSdkVersion: 3.1.100
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- job: PublishWindows
|
||||||
|
displayName: Publish Windows
|
||||||
|
pool:
|
||||||
|
vmImage: ${{ parameters.WindowsImage }}
|
||||||
|
steps:
|
||||||
|
- checkout: self
|
||||||
|
clean: true
|
||||||
|
submodules: true
|
||||||
|
persistCredentials: true
|
||||||
|
|
||||||
|
- task: CmdLine@2
|
||||||
|
displayName: "Clone Web Client (Master, Release, or Tag)"
|
||||||
|
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||||
|
inputs:
|
||||||
|
script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
|
||||||
|
|
||||||
|
- task: CmdLine@2
|
||||||
|
displayName: "Clone Web Client (PR)"
|
||||||
|
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest'))
|
||||||
|
inputs:
|
||||||
|
script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
|
||||||
|
|
||||||
|
- task: NodeTool@0
|
||||||
|
displayName: "Install Node"
|
||||||
|
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||||
|
inputs:
|
||||||
|
versionSpec: "10.x"
|
||||||
|
|
||||||
|
- task: CmdLine@2
|
||||||
|
displayName: "Build Web Client"
|
||||||
|
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||||
|
inputs:
|
||||||
|
script: yarn install
|
||||||
|
workingDirectory: $(Agent.TempDirectory)/jellyfin-web
|
||||||
|
|
||||||
|
- task: CopyFiles@2
|
||||||
|
displayName: "Copy Web Client"
|
||||||
|
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||||
|
inputs:
|
||||||
|
sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
|
||||||
|
contents: "**"
|
||||||
|
targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
|
||||||
|
cleanTargetFolder: true
|
||||||
|
overWrite: true
|
||||||
|
flattenFolders: false
|
||||||
|
|
||||||
|
- task: CmdLine@2
|
||||||
|
displayName: "Clone UX Repository"
|
||||||
|
inputs:
|
||||||
|
script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
|
||||||
|
|
||||||
|
- task: PowerShell@2
|
||||||
|
displayName: "Build NSIS Installer"
|
||||||
|
inputs:
|
||||||
|
targetType: "filePath"
|
||||||
|
filePath: ./deployment/windows/build-jellyfin.ps1
|
||||||
|
arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
|
||||||
|
errorActionPreference: "stop"
|
||||||
|
workingDirectory: $(Build.SourcesDirectory)
|
||||||
|
|
||||||
|
- task: CopyFiles@2
|
||||||
|
displayName: "Copy NSIS Installer"
|
||||||
|
inputs:
|
||||||
|
sourceFolder: $(Build.SourcesDirectory)/deployment/windows/
|
||||||
|
contents: "jellyfin*.exe"
|
||||||
|
targetFolder: $(System.ArtifactsDirectory)/setup
|
||||||
|
cleanTargetFolder: true
|
||||||
|
overWrite: true
|
||||||
|
flattenFolders: true
|
||||||
|
|
||||||
|
- task: PublishPipelineArtifact@0
|
||||||
|
displayName: "Publish Artifact Setup"
|
||||||
|
condition: succeeded()
|
||||||
|
inputs:
|
||||||
|
targetPath: "$(build.artifactstagingdirectory)/setup"
|
||||||
|
artifactName: "Jellyfin Server Setup"
|
||||||
@@ -1,48 +1,38 @@
|
|||||||
name: $(Date:yyyyMMdd)$(Rev:.r)
|
name: $(Date:yyyyMMdd)$(Rev:.r)
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- name: TestProjects
|
- name: TestProjects
|
||||||
value: 'tests/**/*Tests.csproj'
|
value: "tests/**/*Tests.csproj"
|
||||||
- name: RestoreBuildProjects
|
- name: RestoreBuildProjects
|
||||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
value: "Jellyfin.Server/Jellyfin.Server.csproj"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
value: 5.0.100
|
value: 3.1.100
|
||||||
|
|
||||||
pr:
|
pr:
|
||||||
autoCancel: true
|
autoCancel: true
|
||||||
|
|
||||||
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:
|
||||||
Linux: 'ubuntu-latest'
|
Linux: "ubuntu-latest"
|
||||||
Windows: 'windows-latest'
|
Windows: "windows-latest"
|
||||||
macOS: 'macos-latest'
|
macOS: "macos-latest"
|
||||||
|
|
||||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
- template: azure-pipelines-windows.yml
|
||||||
- template: azure-pipelines-test.yml
|
|
||||||
parameters:
|
parameters:
|
||||||
ImageNames:
|
WindowsImage: "windows-latest"
|
||||||
Linux: 'ubuntu-latest'
|
TestProjects: $(TestProjects)
|
||||||
|
|
||||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
|
- template: azure-pipelines-compat.yml
|
||||||
- template: azure-pipelines-abi.yml
|
|
||||||
parameters:
|
parameters:
|
||||||
Packages:
|
Packages:
|
||||||
Naming:
|
Naming:
|
||||||
@@ -57,10 +47,4 @@ jobs:
|
|||||||
Common:
|
Common:
|
||||||
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
|
|
||||||
|
|
||||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
|
||||||
- template: azure-pipelines-api-client.yml
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../fedora/Makefile
|
|
||||||
59
.copr/Makefile
Normal file
59
.copr/Makefile
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
VERSION := $(shell sed -ne '/^Version:/s/.* *//p' \
|
||||||
|
deployment/fedora-package-x64/pkg-src/jellyfin.spec)
|
||||||
|
|
||||||
|
deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz:
|
||||||
|
curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \
|
||||||
|
https://github.com/jellyfin/jellyfin-web/archive/v$(VERSION).tar.gz \
|
||||||
|
|| curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \
|
||||||
|
https://github.com/jellyfin/jellyfin-web/archive/master.tar.gz \
|
||||||
|
|
||||||
|
srpm: deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz
|
||||||
|
cd deployment/fedora-package-x64; \
|
||||||
|
SOURCE_DIR=../.. \
|
||||||
|
WORKDIR="$${PWD}"; \
|
||||||
|
package_temporary_dir="$${WORKDIR}/pkg-dist-tmp"; \
|
||||||
|
pkg_src_dir="$${WORKDIR}/pkg-src"; \
|
||||||
|
GNU_TAR=1; \
|
||||||
|
tar \
|
||||||
|
--transform "s,^\.,jellyfin-$(VERSION)," \
|
||||||
|
--exclude='.git*' \
|
||||||
|
--exclude='**/.git' \
|
||||||
|
--exclude='**/.hg' \
|
||||||
|
--exclude='**/.vs' \
|
||||||
|
--exclude='**/.vscode' \
|
||||||
|
--exclude='deployment' \
|
||||||
|
--exclude='**/bin' \
|
||||||
|
--exclude='**/obj' \
|
||||||
|
--exclude='**/.nuget' \
|
||||||
|
--exclude='*.deb' \
|
||||||
|
--exclude='*.rpm' \
|
||||||
|
-czf "pkg-src/jellyfin-$(VERSION).tar.gz" \
|
||||||
|
-C $${SOURCE_DIR} ./ || GNU_TAR=0; \
|
||||||
|
if [ $${GNU_TAR} -eq 0 ]; then \
|
||||||
|
package_temporary_dir="$$(mktemp -d)"; \
|
||||||
|
mkdir -p "$${package_temporary_dir}/jellyfin"; \
|
||||||
|
tar \
|
||||||
|
--exclude='.git*' \
|
||||||
|
--exclude='**/.git' \
|
||||||
|
--exclude='**/.hg' \
|
||||||
|
--exclude='**/.vs' \
|
||||||
|
--exclude='**/.vscode' \
|
||||||
|
--exclude='deployment' \
|
||||||
|
--exclude='**/bin' \
|
||||||
|
--exclude='**/obj' \
|
||||||
|
--exclude='**/.nuget' \
|
||||||
|
--exclude='*.deb' \
|
||||||
|
--exclude='*.rpm' \
|
||||||
|
-czf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz" \
|
||||||
|
-C $${SOURCE_DIR} ./; \
|
||||||
|
mkdir -p "$${package_temporary_dir}/jellyfin-$(VERSION)"; \
|
||||||
|
tar -xzf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz" \
|
||||||
|
-C "$${package_temporary_dir}/jellyfin-$(VERSION); \
|
||||||
|
rm -f "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz"; \
|
||||||
|
tar -czf "$${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-$(VERSION).tar.gz" \
|
||||||
|
-C "$${package_temporary_dir}" "jellyfin-$(VERSION); \
|
||||||
|
rm -rf $${package_temporary_dir}; \
|
||||||
|
fi; \
|
||||||
|
rpmbuild -bs pkg-src/jellyfin.spec \
|
||||||
|
--define "_sourcedir $$PWD/pkg-src/" \
|
||||||
|
--define "_srcrpmdir $(outdir)"
|
||||||
@@ -13,7 +13,7 @@ charset = utf-8
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
max_line_length = off
|
max_line_length = null
|
||||||
|
|
||||||
# YAML indentation
|
# YAML indentation
|
||||||
[*.{yml,yaml}]
|
[*.{yml,yaml}]
|
||||||
@@ -22,7 +22,6 @@ indent_size = 2
|
|||||||
# XML indentation
|
# XML indentation
|
||||||
[*.{csproj,xml}]
|
[*.{csproj,xml}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
###############################
|
###############################
|
||||||
# .NET Coding Conventions #
|
# .NET Coding Conventions #
|
||||||
###############################
|
###############################
|
||||||
@@ -52,12 +51,11 @@ dotnet_style_explicit_tuple_names = true:suggestion
|
|||||||
dotnet_style_null_propagation = true:suggestion
|
dotnet_style_null_propagation = true:suggestion
|
||||||
dotnet_style_coalesce_expression = true:suggestion
|
dotnet_style_coalesce_expression = true:suggestion
|
||||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
|
||||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
dotnet_prefer_inferred_tuple_names = true:suggestion
|
||||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||||
dotnet_style_prefer_auto_properties = true:silent
|
dotnet_style_prefer_auto_properties = true:silent
|
||||||
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||||
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||||
|
|
||||||
###############################
|
###############################
|
||||||
# Naming Conventions #
|
# Naming Conventions #
|
||||||
###############################
|
###############################
|
||||||
@@ -69,7 +67,7 @@ dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non
|
|||||||
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
|
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
|
||||||
|
|
||||||
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
|
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
|
||||||
dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
|
dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected
|
||||||
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
|
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
|
||||||
|
|
||||||
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
|
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
|
||||||
@@ -161,7 +159,6 @@ csharp_style_deconstructed_variable_declaration = true:suggestion
|
|||||||
csharp_prefer_simple_default_expression = true:suggestion
|
csharp_prefer_simple_default_expression = true:suggestion
|
||||||
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||||
csharp_style_inlined_variable_declaration = true:suggestion
|
csharp_style_inlined_variable_declaration = true:suggestion
|
||||||
|
|
||||||
###############################
|
###############################
|
||||||
# C# Formatting Rules #
|
# C# Formatting Rules #
|
||||||
###############################
|
###############################
|
||||||
@@ -192,3 +189,9 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
|||||||
# Wrapping preferences
|
# Wrapping preferences
|
||||||
csharp_preserve_single_line_statements = true
|
csharp_preserve_single_line_statements = true
|
||||||
csharp_preserve_single_line_blocks = true
|
csharp_preserve_single_line_blocks = true
|
||||||
|
###############################
|
||||||
|
# VB Coding Conventions #
|
||||||
|
###############################
|
||||||
|
[*.vb]
|
||||||
|
# Modifier preferences
|
||||||
|
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion
|
||||||
|
|||||||
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Joshua must review all changes to deployment and build.sh
|
|
||||||
deployment/* @joshuaboniface
|
|
||||||
build.sh @joshuaboniface
|
|
||||||
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature Request
|
|
||||||
about: Request a new feature
|
|
||||||
title: ''
|
|
||||||
labels: feature-request
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**PLEASE DO NOT OPEN FEATURE REQUEST ISSUES ON GITHUB**
|
|
||||||
|
|
||||||
**Feature requests should be opened on our dedicated [feature request](https://features.jellyfin.org/) hub so they can be appropriately discussed and prioritized.**
|
|
||||||
|
|
||||||
However, if you are willing to contribute to the project by adding a new feature yourself, then please ensure that you first review our [documentation](https://docs.jellyfin.org/general/contributing/development.html) on contributing code. Once you have reviewed the documentation, feel free to come back here and open an issue here outlining your proposed approach so that it can be documented, tracked, and discussed by other team members.
|
|
||||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -1,9 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: nuget
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
time: '12:00'
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
|
|
||||||
36
.github/workflows/codeql-analysis.yml
vendored
36
.github/workflows/codeql-analysis.yml
vendored
@@ -1,36 +0,0 @@
|
|||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
schedule:
|
|
||||||
- cron: '24 2 * * 4'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'csharp' ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Setup .NET Core
|
|
||||||
uses: actions/setup-dotnet@v1
|
|
||||||
with:
|
|
||||||
dotnet-version: '5.0.x'
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
queries: +security-extended
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v1
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
||||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -244,14 +244,14 @@ pip-log.txt
|
|||||||
#########################
|
#########################
|
||||||
|
|
||||||
# Artifacts for debian-x64
|
# Artifacts for debian-x64
|
||||||
debian/.debhelper/
|
deployment/debian-package-x64/pkg-src/.debhelper/
|
||||||
debian/*.debhelper
|
deployment/debian-package-x64/pkg-src/*.debhelper
|
||||||
debian/debhelper-build-stamp
|
deployment/debian-package-x64/pkg-src/debhelper-build-stamp
|
||||||
debian/files
|
deployment/debian-package-x64/pkg-src/files
|
||||||
debian/jellyfin.substvars
|
deployment/debian-package-x64/pkg-src/jellyfin.substvars
|
||||||
debian/jellyfin/
|
deployment/debian-package-x64/pkg-src/jellyfin/
|
||||||
# Don't ignore the debian/bin folder
|
# Don't ignore the debian/bin folder
|
||||||
!debian/bin/
|
!deployment/debian-package-x64/pkg-src/bin/
|
||||||
|
|
||||||
deployment/**/dist/
|
deployment/**/dist/
|
||||||
deployment/**/pkg-dist/
|
deployment/**/pkg-dist/
|
||||||
@@ -271,9 +271,3 @@ dist
|
|||||||
|
|
||||||
# BenchmarkDotNet artifacts
|
# BenchmarkDotNet artifacts
|
||||||
BenchmarkDotNet.Artifacts
|
BenchmarkDotNet.Artifacts
|
||||||
|
|
||||||
# Ignore web artifacts from native builds
|
|
||||||
web/
|
|
||||||
web-src.*
|
|
||||||
MediaBrowser.WebDashboard/jellyfin-web
|
|
||||||
apiclient/generated
|
|
||||||
|
|||||||
3
.npmrc
3
.npmrc
@@ -1,3 +0,0 @@
|
|||||||
registry=https://registry.npmjs.org/
|
|
||||||
@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
|
|
||||||
always-auth=true
|
|
||||||
14
.vscode/extensions.json
vendored
14
.vscode/extensions.json
vendored
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
|
||||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
|
||||||
|
|
||||||
// List of extensions which should be recommended for users of this workspace.
|
|
||||||
"recommendations": [
|
|
||||||
"ms-dotnettools.csharp",
|
|
||||||
"editorconfig.editorconfig"
|
|
||||||
],
|
|
||||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
|
||||||
"unwantedRecommendations": [
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
|
||||||
28
.vscode/launch.json
vendored
28
.vscode/launch.json
vendored
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
@@ -6,25 +9,11 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
|
||||||
"stopAtEntry": false,
|
|
||||||
"internalConsoleOptions": "openOnSessionStart",
|
|
||||||
"serverReadyAction": {
|
|
||||||
"action": "openExternally",
|
|
||||||
"pattern": "Overriding address\\(es\\) \\'(https?:\\S+)\\'",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": ".NET Core Launch (nowebclient)",
|
|
||||||
"type": "coreclr",
|
|
||||||
"request": "launch",
|
|
||||||
"preLaunchTask": "build",
|
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
|
|
||||||
"args": ["--nowebclient"],
|
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
"internalConsoleOptions": "openOnSessionStart"
|
||||||
@@ -35,8 +24,5 @@
|
|||||||
"request": "attach",
|
"request": "attach",
|
||||||
"processId": "${command:pickProcess}"
|
"processId": "${command:pickProcess}"
|
||||||
}
|
}
|
||||||
],
|
,]
|
||||||
"env": {
|
|
||||||
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
.vscode/tasks.json
vendored
17
.vscode/tasks.json
vendored
@@ -10,21 +10,6 @@
|
|||||||
"${workspaceFolder}/Jellyfin.Server/Jellyfin.Server.csproj"
|
"${workspaceFolder}/Jellyfin.Server/Jellyfin.Server.csproj"
|
||||||
],
|
],
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "api tests",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"test",
|
|
||||||
"${workspaceFolder}/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"options": {
|
|
||||||
"env": {
|
|
||||||
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,6 @@
|
|||||||
- [anthonylavado](https://github.com/anthonylavado)
|
- [anthonylavado](https://github.com/anthonylavado)
|
||||||
- [Artiume](https://github.com/Artiume)
|
- [Artiume](https://github.com/Artiume)
|
||||||
- [AThomsen](https://github.com/AThomsen)
|
- [AThomsen](https://github.com/AThomsen)
|
||||||
- [barongreenback](https://github.com/BaronGreenback)
|
|
||||||
- [barronpm](https://github.com/barronpm)
|
|
||||||
- [bilde2910](https://github.com/bilde2910)
|
- [bilde2910](https://github.com/bilde2910)
|
||||||
- [bfayers](https://github.com/bfayers)
|
- [bfayers](https://github.com/bfayers)
|
||||||
- [BnMcG](https://github.com/BnMcG)
|
- [BnMcG](https://github.com/BnMcG)
|
||||||
@@ -17,7 +15,6 @@
|
|||||||
- [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)
|
||||||
@@ -25,7 +22,6 @@
|
|||||||
- [cvium](https://github.com/cvium)
|
- [cvium](https://github.com/cvium)
|
||||||
- [dannymichel](https://github.com/dannymichel)
|
- [dannymichel](https://github.com/dannymichel)
|
||||||
- [DaveChild](https://github.com/DaveChild)
|
- [DaveChild](https://github.com/DaveChild)
|
||||||
- [Delgan](https://github.com/Delgan)
|
|
||||||
- [dcrdev](https://github.com/dcrdev)
|
- [dcrdev](https://github.com/dcrdev)
|
||||||
- [dhartung](https://github.com/dhartung)
|
- [dhartung](https://github.com/dhartung)
|
||||||
- [dinki](https://github.com/dinki)
|
- [dinki](https://github.com/dinki)
|
||||||
@@ -58,7 +54,6 @@
|
|||||||
- [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)
|
||||||
@@ -79,9 +74,7 @@
|
|||||||
- [Nickbert7](https://github.com/Nickbert7)
|
- [Nickbert7](https://github.com/Nickbert7)
|
||||||
- [nvllsvm](https://github.com/nvllsvm)
|
- [nvllsvm](https://github.com/nvllsvm)
|
||||||
- [nyanmisaka](https://github.com/nyanmisaka)
|
- [nyanmisaka](https://github.com/nyanmisaka)
|
||||||
- [OancaAndrei](https://github.com/OancaAndrei)
|
|
||||||
- [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)
|
||||||
@@ -98,15 +91,12 @@
|
|||||||
- [samuel9554](https://github.com/samuel9554)
|
- [samuel9554](https://github.com/samuel9554)
|
||||||
- [scheidleon](https://github.com/scheidleon)
|
- [scheidleon](https://github.com/scheidleon)
|
||||||
- [sebPomme](https://github.com/sebPomme)
|
- [sebPomme](https://github.com/sebPomme)
|
||||||
- [SegiH](https://github.com/SegiH)
|
|
||||||
- [SenorSmartyPants](https://github.com/SenorSmartyPants)
|
- [SenorSmartyPants](https://github.com/SenorSmartyPants)
|
||||||
- [shemanaev](https://github.com/shemanaev)
|
- [shemanaev](https://github.com/shemanaev)
|
||||||
- [skaro13](https://github.com/skaro13)
|
- [skaro13](https://github.com/skaro13)
|
||||||
- [sl1288](https://github.com/sl1288)
|
- [sl1288](https://github.com/sl1288)
|
||||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||||
- [sparky8251](https://github.com/sparky8251)
|
- [sparky8251](https://github.com/sparky8251)
|
||||||
- [spookbits](https://github.com/spookbits)
|
|
||||||
- [ssenart] (https://github.com/ssenart)
|
|
||||||
- [stanionascu](https://github.com/stanionascu)
|
- [stanionascu](https://github.com/stanionascu)
|
||||||
- [stevehayles](https://github.com/stevehayles)
|
- [stevehayles](https://github.com/stevehayles)
|
||||||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||||
@@ -138,10 +128,6 @@
|
|||||||
- [XVicarious](https://github.com/XVicarious)
|
- [XVicarious](https://github.com/XVicarious)
|
||||||
- [YouKnowBlom](https://github.com/YouKnowBlom)
|
- [YouKnowBlom](https://github.com/YouKnowBlom)
|
||||||
- [KristupasSavickas](https://github.com/KristupasSavickas)
|
- [KristupasSavickas](https://github.com/KristupasSavickas)
|
||||||
- [Pusta](https://github.com/pusta)
|
|
||||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
|
||||||
- [skyfrk](https://github.com/skyfrk)
|
|
||||||
- [ianjazz246](https://github.com/ianjazz246)
|
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
@@ -205,4 +191,3 @@
|
|||||||
- [tikuf](https://github.com/tikuf/)
|
- [tikuf](https://github.com/tikuf/)
|
||||||
- [Tim Hobbs](https://github.com/timhobbs)
|
- [Tim Hobbs](https://github.com/timhobbs)
|
||||||
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
||||||
- [olsh](https://github.com/olsh)
|
|
||||||
|
|||||||
42
Dockerfile
42
Dockerfile
@@ -1,20 +1,22 @@
|
|||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=3.1
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=10.5.5
|
||||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
RUN apk add curl git \
|
||||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
&& git clone --branch release-10.5.z --single-branch https://github.com/jellyfin/jellyfin-web.git \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web \
|
||||||
|
&& git checkout tags/v${JELLYFIN_WEB_VERSION} \
|
||||||
&& yarn install \
|
&& yarn install \
|
||||||
|
&& yarn build \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder
|
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
COPY . .
|
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:DebugSymbols=false;DebugType=none"
|
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
|
||||||
|
|
||||||
FROM debian:buster-slim
|
FROM debian:buster-slim
|
||||||
|
|
||||||
@@ -27,15 +29,8 @@ ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
|||||||
|
|
||||||
COPY --from=builder /jellyfin /jellyfin
|
COPY --from=builder /jellyfin /jellyfin
|
||||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||||
|
|
||||||
# https://github.com/intel/compute-runtime/releases
|
|
||||||
ARG GMMLIB_VERSION=20.3.2
|
|
||||||
ARG IGC_VERSION=1.0.5435
|
|
||||||
ARG NEO_VERSION=20.46.18421
|
|
||||||
ARG LEVEL_ZERO_VERSION=1.0.18421
|
|
||||||
|
|
||||||
# Install dependencies:
|
# Install dependencies:
|
||||||
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
|
# mesa-va-drivers: needed for AMD VAAPI
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
|
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
|
||||||
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
|
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
|
||||||
@@ -46,20 +41,6 @@ RUN apt-get update \
|
|||||||
jellyfin-ffmpeg \
|
jellyfin-ffmpeg \
|
||||||
openssl \
|
openssl \
|
||||||
locales \
|
locales \
|
||||||
# Intel VAAPI Tone mapping dependencies:
|
|
||||||
# Prefer NEO to Beignet since the latter one doesn't support Comet Lake or newer for now.
|
|
||||||
# Do not use the intel-opencl-icd package from repo since they will not build with RELEASE_WITH_REGKEYS enabled.
|
|
||||||
&& mkdir intel-compute-runtime \
|
|
||||||
&& cd intel-compute-runtime \
|
|
||||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \
|
|
||||||
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
|
|
||||||
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
|
|
||||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl_${NEO_VERSION}_amd64.deb \
|
|
||||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-ocloc_${NEO_VERSION}_amd64.deb \
|
|
||||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
|
|
||||||
&& dpkg -i *.deb \
|
|
||||||
&& cd .. \
|
|
||||||
&& rm -rf intel-compute-runtime \
|
|
||||||
&& apt-get remove gnupg wget apt-transport-https -y \
|
&& apt-get remove gnupg wget apt-transport-https -y \
|
||||||
&& apt-get clean autoclean -y \
|
&& apt-get clean autoclean -y \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
@@ -69,9 +50,6 @@ RUN apt-get update \
|
|||||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
|
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
|
||||||
|
|
||||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||||
ENV LC_ALL en_US.UTF-8
|
|
||||||
ENV LANG en_US.UTF-8
|
|
||||||
ENV LANGUAGE en_US:en
|
|
||||||
|
|
||||||
EXPOSE 8096
|
EXPOSE 8096
|
||||||
VOLUME /cache /config /media
|
VOLUME /cache /config /media
|
||||||
|
|||||||
@@ -2,26 +2,28 @@
|
|||||||
#####################################
|
#####################################
|
||||||
# Requires binfm_misc registration
|
# Requires binfm_misc registration
|
||||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=3.1
|
||||||
|
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=10.5.5
|
||||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
RUN apk add curl git \
|
||||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
&& git clone --branch release-10.5.z --single-branch https://github.com/jellyfin/jellyfin-web.git \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web \
|
||||||
|
&& git checkout tags/v${JELLYFIN_WEB_VERSION} \
|
||||||
&& yarn install \
|
&& yarn install \
|
||||||
|
&& yarn build \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
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:DebugSymbols=false;DebugType=none"
|
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
|
||||||
|
|
||||||
|
|
||||||
FROM multiarch/qemu-user-static:x86_64-arm as qemu
|
FROM multiarch/qemu-user-static:x86_64-arm as qemu
|
||||||
@@ -52,22 +54,16 @@ RUN apt-get update \
|
|||||||
libraspberrypi0 \
|
libraspberrypi0 \
|
||||||
vainfo \
|
vainfo \
|
||||||
libva2 \
|
libva2 \
|
||||||
locales \
|
|
||||||
&& apt-get remove curl gnupg -y \
|
&& apt-get remove curl gnupg -y \
|
||||||
&& apt-get clean autoclean -y \
|
&& apt-get clean autoclean -y \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& mkdir -p /cache /config /media \
|
&& mkdir -p /cache /config /media \
|
||||||
&& chmod 777 /cache /config /media \
|
&& chmod 777 /cache /config /media
|
||||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
|
|
||||||
|
|
||||||
COPY --from=builder /jellyfin /jellyfin
|
COPY --from=builder /jellyfin /jellyfin
|
||||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||||
|
|
||||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||||
ENV LC_ALL en_US.UTF-8
|
|
||||||
ENV LANG en_US.UTF-8
|
|
||||||
ENV LANGUAGE en_US:en
|
|
||||||
|
|
||||||
EXPOSE 8096
|
EXPOSE 8096
|
||||||
VOLUME /cache /config /media
|
VOLUME /cache /config /media
|
||||||
|
|||||||
@@ -2,26 +2,28 @@
|
|||||||
#####################################
|
#####################################
|
||||||
# Requires binfm_misc registration
|
# Requires binfm_misc registration
|
||||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=3.1
|
||||||
|
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=10.5.5
|
||||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
RUN apk add curl git \
|
||||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
&& git clone --branch release-10.5.z --single-branch https://github.com/jellyfin/jellyfin-web.git \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web \
|
||||||
|
&& git checkout tags/v${JELLYFIN_WEB_VERSION} \
|
||||||
&& yarn install \
|
&& yarn install \
|
||||||
|
&& yarn build \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
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:DebugSymbols=false;DebugType=none"
|
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;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
|
||||||
@@ -42,21 +44,15 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
|
|||||||
libfreetype6 \
|
libfreetype6 \
|
||||||
libomxil-bellagio0 \
|
libomxil-bellagio0 \
|
||||||
libomxil-bellagio-bin \
|
libomxil-bellagio-bin \
|
||||||
locales \
|
|
||||||
&& apt-get clean autoclean -y \
|
&& apt-get clean autoclean -y \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& mkdir -p /cache /config /media \
|
&& mkdir -p /cache /config /media \
|
||||||
&& chmod 777 /cache /config /media \
|
&& chmod 777 /cache /config /media
|
||||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
|
|
||||||
|
|
||||||
COPY --from=builder /jellyfin /jellyfin
|
COPY --from=builder /jellyfin /jellyfin
|
||||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||||
|
|
||||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||||
ENV LC_ALL en_US.UTF-8
|
|
||||||
ENV LANG en_US.UTF-8
|
|
||||||
ENV LANGUAGE en_US:en
|
|
||||||
|
|
||||||
EXPOSE 8096
|
EXPOSE 8096
|
||||||
VOLUME /cache /config /media
|
VOLUME /cache /config /media
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#pragma warning disable CS1591
|
using System;
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace DvdLib
|
namespace DvdLib
|
||||||
@@ -14,12 +12,19 @@ namespace DvdLib
|
|||||||
|
|
||||||
public override ushort ReadUInt16()
|
public override ushort ReadUInt16()
|
||||||
{
|
{
|
||||||
return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2));
|
return BitConverter.ToUInt16(ReadAndReverseBytes(2), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override uint ReadUInt32()
|
public override uint ReadUInt32()
|
||||||
{
|
{
|
||||||
return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4));
|
return BitConverter.ToUInt32(ReadAndReverseBytes(4), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ReadAndReverseBytes(int count)
|
||||||
|
{
|
||||||
|
byte[] val = base.ReadBytes(count);
|
||||||
|
Array.Reverse(val, 0, count);
|
||||||
|
return val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<ProjectGuid>{713F42B5-878E-499D-A878-E4C652B1D5E8}</ProjectGuid>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\SharedVersion.cs" />
|
<Compile Include="..\SharedVersion.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace DvdLib.Ifo
|
namespace DvdLib.Ifo
|
||||||
@@ -7,7 +5,6 @@ namespace DvdLib.Ifo
|
|||||||
public class Cell
|
public class Cell
|
||||||
{
|
{
|
||||||
public CellPlaybackInfo PlaybackInfo { get; private set; }
|
public CellPlaybackInfo PlaybackInfo { get; private set; }
|
||||||
|
|
||||||
public CellPositionInfo PositionInfo { get; private set; }
|
public CellPositionInfo PositionInfo { get; private set; }
|
||||||
|
|
||||||
internal void ParsePlayback(BinaryReader br)
|
internal void ParsePlayback(BinaryReader br)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace DvdLib.Ifo
|
namespace DvdLib.Ifo
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace DvdLib.Ifo
|
namespace DvdLib.Ifo
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
namespace DvdLib.Ifo
|
namespace DvdLib.Ifo
|
||||||
{
|
{
|
||||||
public class Chapter
|
public class Chapter
|
||||||
{
|
{
|
||||||
public ushort ProgramChainNumber { get; private set; }
|
public ushort ProgramChainNumber { get; private set; }
|
||||||
|
|
||||||
public ushort ProgramNumber { get; private set; }
|
public ushort ProgramNumber { get; private set; }
|
||||||
|
|
||||||
public uint ChapterNumber { get; private set; }
|
public uint ChapterNumber { get; private set; }
|
||||||
|
|
||||||
public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)
|
public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
|
||||||
namespace DvdLib.Ifo
|
namespace DvdLib.Ifo
|
||||||
{
|
{
|
||||||
@@ -14,10 +13,13 @@ namespace DvdLib.Ifo
|
|||||||
|
|
||||||
private ushort _titleCount;
|
private ushort _titleCount;
|
||||||
public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>();
|
public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>();
|
||||||
public Dvd(string path)
|
private readonly IFileSystem _fileSystem;
|
||||||
|
|
||||||
|
public Dvd(string path, IFileSystem fileSystem)
|
||||||
{
|
{
|
||||||
|
_fileSystem = fileSystem;
|
||||||
Titles = new List<Title>();
|
Titles = new List<Title>();
|
||||||
var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories);
|
var allFiles = _fileSystem.GetFiles(path, true).ToList();
|
||||||
|
|
||||||
var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ??
|
var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ??
|
||||||
allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase));
|
allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -31,7 +33,7 @@ namespace DvdLib.Ifo
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
var nums = ifo.Name.Split(new [] { '_' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
|
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
|
||||||
{
|
{
|
||||||
ReadVTS(ifoNumber, ifo.FullName);
|
ReadVTS(ifoNumber, ifo.FullName);
|
||||||
@@ -74,7 +76,7 @@ namespace DvdLib.Ifo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
|
private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles)
|
||||||
{
|
{
|
||||||
var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
|
var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
|
||||||
|
|
||||||
@@ -117,19 +119,12 @@ namespace DvdLib.Ifo
|
|||||||
uint chapNum = 1;
|
uint chapNum = 1;
|
||||||
vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
|
vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
|
||||||
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
|
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
|
||||||
if (t == null)
|
if (t == null) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum));
|
t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum));
|
||||||
if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1]))
|
if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1])) break;
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
chapNum++;
|
chapNum++;
|
||||||
}
|
}
|
||||||
while (vtsFs.Position < (baseAddr + endaddr));
|
while (vtsFs.Position < (baseAddr + endaddr));
|
||||||
@@ -154,10 +149,7 @@ namespace DvdLib.Ifo
|
|||||||
uint vtsPgcOffset = vtsRead.ReadUInt32();
|
uint vtsPgcOffset = vtsRead.ReadUInt32();
|
||||||
|
|
||||||
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum));
|
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum));
|
||||||
if (t != null)
|
if (t != null) t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
|
||||||
{
|
|
||||||
t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DvdLib.Ifo
|
namespace DvdLib.Ifo
|
||||||
@@ -15,14 +13,8 @@ namespace DvdLib.Ifo
|
|||||||
Second = GetBCDValue(data[2]);
|
Second = GetBCDValue(data[2]);
|
||||||
Frames = GetBCDValue((byte)(data[3] & 0x3F));
|
Frames = GetBCDValue((byte)(data[3] & 0x3F));
|
||||||
|
|
||||||
if ((data[3] & 0x80) != 0)
|
if ((data[3] & 0x80) != 0) FrameRate = 30;
|
||||||
{
|
else if ((data[3] & 0x40) != 0) FrameRate = 25;
|
||||||
FrameRate = 30;
|
|
||||||
}
|
|
||||||
else if ((data[3] & 0x40) != 0)
|
|
||||||
{
|
|
||||||
FrameRate = 25;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte GetBCDValue(byte data)
|
private static byte GetBCDValue(byte data)
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DvdLib.Ifo
|
namespace DvdLib.Ifo
|
||||||
{
|
{
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
public IReadOnlyList<Cell> Cells { get; }
|
public readonly List<Cell> Cells;
|
||||||
|
|
||||||
public Program(List<Cell> cells)
|
public Program(List<Cell> cells)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -22,9 +20,7 @@ namespace DvdLib.Ifo
|
|||||||
public readonly List<Cell> Cells;
|
public readonly List<Cell> Cells;
|
||||||
|
|
||||||
public DvdTime PlaybackTime { get; private set; }
|
public DvdTime PlaybackTime { get; private set; }
|
||||||
|
|
||||||
public UserOperation ProhibitedUserOperations { get; private set; }
|
public UserOperation ProhibitedUserOperations { get; private set; }
|
||||||
|
|
||||||
public byte[] AudioStreamControl { get; private set; } // 8*2 entries
|
public byte[] AudioStreamControl { get; private set; } // 8*2 entries
|
||||||
public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
|
public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
|
||||||
|
|
||||||
@@ -35,11 +31,9 @@ namespace DvdLib.Ifo
|
|||||||
private ushort _goupProgramNumber;
|
private ushort _goupProgramNumber;
|
||||||
|
|
||||||
public ProgramPlaybackMode PlaybackMode { get; private set; }
|
public ProgramPlaybackMode PlaybackMode { get; private set; }
|
||||||
|
|
||||||
public uint ProgramCount { get; private set; }
|
public uint ProgramCount { get; private set; }
|
||||||
|
|
||||||
public byte StillTime { get; private set; }
|
public byte StillTime { get; private set; }
|
||||||
|
|
||||||
public byte[] Palette { get; private set; } // 16*4 entries
|
public byte[] Palette { get; private set; } // 16*4 entries
|
||||||
|
|
||||||
private ushort _commandTableOffset;
|
private ushort _commandTableOffset;
|
||||||
@@ -75,15 +69,8 @@ namespace DvdLib.Ifo
|
|||||||
|
|
||||||
StillTime = br.ReadByte();
|
StillTime = br.ReadByte();
|
||||||
byte pbMode = br.ReadByte();
|
byte pbMode = br.ReadByte();
|
||||||
if (pbMode == 0)
|
if (pbMode == 0) PlaybackMode = ProgramPlaybackMode.Sequential;
|
||||||
{
|
else PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
|
||||||
PlaybackMode = ProgramPlaybackMode.Sequential;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgramCount = (uint)(pbMode & 0x7F);
|
ProgramCount = (uint)(pbMode & 0x7F);
|
||||||
|
|
||||||
Palette = br.ReadBytes(64);
|
Palette = br.ReadBytes(64);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
@@ -8,11 +6,8 @@ namespace DvdLib.Ifo
|
|||||||
public class Title
|
public class Title
|
||||||
{
|
{
|
||||||
public uint TitleNumber { get; private set; }
|
public uint TitleNumber { get; private set; }
|
||||||
|
|
||||||
public uint AngleCount { get; private set; }
|
public uint AngleCount { get; private set; }
|
||||||
|
|
||||||
public ushort ChapterCount { get; private set; }
|
public ushort ChapterCount { get; private set; }
|
||||||
|
|
||||||
public byte VideoTitleSetNumber { get; private set; }
|
public byte VideoTitleSetNumber { get; private set; }
|
||||||
|
|
||||||
private ushort _parentalManagementMask;
|
private ushort _parentalManagementMask;
|
||||||
@@ -20,7 +15,6 @@ namespace DvdLib.Ifo
|
|||||||
private uint _vtsStartSector; // relative to start of entire disk
|
private uint _vtsStartSector; // relative to start of entire disk
|
||||||
|
|
||||||
public ProgramChain EntryProgramChain { get; private set; }
|
public ProgramChain EntryProgramChain { get; private set; }
|
||||||
|
|
||||||
public readonly List<ProgramChain> ProgramChains;
|
public readonly List<ProgramChain> ProgramChains;
|
||||||
|
|
||||||
public readonly List<Chapter> Chapters;
|
public readonly List<Chapter> Chapters;
|
||||||
@@ -59,10 +53,7 @@ namespace DvdLib.Ifo
|
|||||||
var pgc = new ProgramChain(pgcNum);
|
var pgc = new ProgramChain(pgcNum);
|
||||||
pgc.ParseHeader(br);
|
pgc.ParseHeader(br);
|
||||||
ProgramChains.Add(pgc);
|
ProgramChains.Add(pgc);
|
||||||
if (entryPgc)
|
if (entryPgc) EntryProgramChain = pgc;
|
||||||
{
|
|
||||||
EntryProgramChain = pgc;
|
|
||||||
}
|
|
||||||
|
|
||||||
br.BaseStream.Seek(curPos, SeekOrigin.Begin);
|
br.BaseStream.Seek(curPos, SeekOrigin.Begin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DvdLib.Ifo
|
namespace DvdLib.Ifo
|
||||||
|
|||||||
374
Emby.Dlna/Api/DlnaServerService.cs
Normal file
374
Emby.Dlna/Api/DlnaServerService.cs
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
|
using System;
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Get(GetContentDirectory request)
|
||||||
|
{
|
||||||
|
var xml = ContentDirectory.GetServiceXml();
|
||||||
|
|
||||||
|
return _resultFactory.GetResult(Request, xml, XMLContentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Get(GetMediaReceiverRegistrar request)
|
||||||
|
{
|
||||||
|
var xml = MediaReceiverRegistrar.GetServiceXml();
|
||||||
|
|
||||||
|
return _resultFactory.GetResult(Request, xml, XMLContentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Subscribe(ProcessContentDirectoryEventRequest request)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(ContentDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Subscribe(ProcessConnectionManagerEventRequest request)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(ConnectionManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(MediaReceiverRegistrar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Unsubscribe(ProcessContentDirectoryEventRequest request)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(ContentDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Unsubscribe(ProcessConnectionManagerEventRequest request)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(ConnectionManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
Emby.Dlna/Api/DlnaService.cs
Normal file
86
Emby.Dlna/Api/DlnaService.cs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Get(GetProfileInfos request)
|
||||||
|
{
|
||||||
|
return _dlnaManager.GetProfileInfos().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Get(GetProfile request)
|
||||||
|
{
|
||||||
|
return _dlnaManager.GetProfile(request.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,14 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
namespace Emby.Dlna.Common
|
namespace Emby.Dlna.Common
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// DLNA Query parameter type, used when querying DLNA devices via SOAP.
|
|
||||||
/// </summary>
|
|
||||||
public class Argument
|
public class Argument
|
||||||
{
|
{
|
||||||
/// <summary>
|
public string Name { get; set; }
|
||||||
/// Gets or sets name of the DLNA argument.
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
public string Direction { get; set; }
|
||||||
/// Gets or sets the direction of the parameter.
|
|
||||||
/// </summary>
|
|
||||||
public string Direction { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
public string RelatedStateVariable { get; set; }
|
||||||
/// Gets or sets the related DLNA state variable for this argument.
|
|
||||||
/// </summary>
|
|
||||||
public string RelatedStateVariable { get; set; } = string.Empty;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,30 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Emby.Dlna.Common
|
namespace Emby.Dlna.Common
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Defines the <see cref="DeviceIcon" />.
|
|
||||||
/// </summary>
|
|
||||||
public class DeviceIcon
|
public class DeviceIcon
|
||||||
{
|
{
|
||||||
/// <summary>
|
public string Url { get; set; }
|
||||||
/// Gets or sets the Url.
|
|
||||||
/// </summary>
|
|
||||||
public string Url { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
public string MimeType { get; set; }
|
||||||
/// Gets or sets the MimeType.
|
|
||||||
/// </summary>
|
|
||||||
public string MimeType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the Width.
|
|
||||||
/// </summary>
|
|
||||||
public int Width { get; set; }
|
public int Width { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the Height.
|
|
||||||
/// </summary>
|
|
||||||
public int Height { get; set; }
|
public int Height { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public string Depth { get; set; }
|
||||||
/// Gets or sets the Depth.
|
|
||||||
/// </summary>
|
|
||||||
public string Depth { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format(CultureInfo.InvariantCulture, "{0}x{1}", Height, Width);
|
return string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"{0}x{1}",
|
||||||
|
Height,
|
||||||
|
Width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,22 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
namespace Emby.Dlna.Common
|
namespace Emby.Dlna.Common
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Defines the <see cref="DeviceService" />.
|
|
||||||
/// </summary>
|
|
||||||
public class DeviceService
|
public class DeviceService
|
||||||
{
|
{
|
||||||
/// <summary>
|
public string ServiceType { get; set; }
|
||||||
/// Gets or sets the Service Type.
|
|
||||||
/// </summary>
|
|
||||||
public string ServiceType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
public string ServiceId { get; set; }
|
||||||
/// Gets or sets the Service Id.
|
|
||||||
/// </summary>
|
|
||||||
public string ServiceId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
public string ScpdUrl { get; set; }
|
||||||
/// Gets or sets the Scpd Url.
|
|
||||||
/// </summary>
|
|
||||||
public string ScpdUrl { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
public string ControlUrl { get; set; }
|
||||||
/// Gets or sets the Control Url.
|
|
||||||
/// </summary>
|
|
||||||
public string ControlUrl { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
public string EventSubUrl { get; set; }
|
||||||
/// Gets or sets the EventSubUrl.
|
|
||||||
/// </summary>
|
|
||||||
public string EventSubUrl { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString() => ServiceId;
|
public override string ToString()
|
||||||
|
=> ServiceId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,25 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Emby.Dlna.Common
|
namespace Emby.Dlna.Common
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Defines the <see cref="ServiceAction" />.
|
|
||||||
/// </summary>
|
|
||||||
public class ServiceAction
|
public class ServiceAction
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ServiceAction"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public ServiceAction()
|
public ServiceAction()
|
||||||
{
|
{
|
||||||
ArgumentList = new List<Argument>();
|
ArgumentList = new List<Argument>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public string Name { get; set; }
|
||||||
/// Gets or sets the name of the action.
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
public List<Argument> ArgumentList { get; set; }
|
||||||
/// Gets the ArgumentList.
|
|
||||||
/// </summary>
|
|
||||||
public List<Argument> ArgumentList { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString() => Name;
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,27 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.Common
|
namespace Emby.Dlna.Common
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Defines the <see cref="StateVariable" />.
|
|
||||||
/// </summary>
|
|
||||||
public class StateVariable
|
public class StateVariable
|
||||||
{
|
{
|
||||||
/// <summary>
|
public StateVariable()
|
||||||
/// Gets or sets the name of the state variable.
|
{
|
||||||
/// </summary>
|
AllowedValues = Array.Empty<string>();
|
||||||
public string Name { get; set; } = string.Empty;
|
}
|
||||||
|
|
||||||
/// <summary>
|
public string Name { get; set; }
|
||||||
/// Gets or sets the data type of the state variable.
|
|
||||||
/// </summary>
|
public string DataType { get; set; }
|
||||||
public string DataType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether it sends events.
|
|
||||||
/// </summary>
|
|
||||||
public bool SendsEvents { get; set; }
|
public bool SendsEvents { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public string[] AllowedValues { get; set; }
|
||||||
/// Gets or sets the allowed values range.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyList<string> AllowedValues { get; set; } = Array.Empty<string>();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString() => Name;
|
public override string ToString()
|
||||||
|
=> Name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
namespace Emby.Dlna.Configuration
|
namespace Emby.Dlna.Configuration
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The DlnaOptions class contains the user definable parameters for the dlna subsystems.
|
|
||||||
/// </summary>
|
|
||||||
public class DlnaOptions
|
public class DlnaOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="DlnaOptions"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public DlnaOptions()
|
public DlnaOptions()
|
||||||
{
|
{
|
||||||
EnablePlayTo = true;
|
EnablePlayTo = true;
|
||||||
@@ -17,76 +12,23 @@ namespace Emby.Dlna.Configuration
|
|||||||
BlastAliveMessages = true;
|
BlastAliveMessages = true;
|
||||||
SendOnlyMatchedHost = true;
|
SendOnlyMatchedHost = true;
|
||||||
ClientDiscoveryIntervalSeconds = 60;
|
ClientDiscoveryIntervalSeconds = 60;
|
||||||
AliveMessageIntervalSeconds = 1800;
|
BlastAliveMessageIntervalSeconds = 1800;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna playTo subsystem.
|
|
||||||
/// </summary>
|
|
||||||
public bool EnablePlayTo { get; set; }
|
public bool EnablePlayTo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna server subsystem.
|
|
||||||
/// </summary>
|
|
||||||
public bool EnableServer { get; set; }
|
public bool EnableServer { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether detailed dlna server logs are sent to the console/log.
|
|
||||||
/// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
|
|
||||||
/// </summary>
|
|
||||||
public bool EnableDebugLog { get; set; }
|
public bool EnableDebugLog { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public bool BlastAliveMessages { get; set; }
|
||||||
/// Gets or sets a value indicating whether whether detailed playTo debug logs are sent to the console/log.
|
|
||||||
/// If the setting "Emby.Dlna.PlayTo": "Debug" msut be set in logging.default.json for this property to work.
|
public bool SendOnlyMatchedHost { get; set; }
|
||||||
/// </summary>
|
|
||||||
public bool EnablePlayToTracing { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the ssdp client discovery interval time (in seconds).
|
|
||||||
/// This is the time after which the server will send a ssdp search request.
|
|
||||||
/// </summary>
|
|
||||||
public int ClientDiscoveryIntervalSeconds { get; set; }
|
public int ClientDiscoveryIntervalSeconds { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public int BlastAliveMessageIntervalSeconds { get; set; }
|
||||||
/// Gets or sets the frequency at which ssdp alive notifications are transmitted.
|
|
||||||
/// </summary>
|
|
||||||
public int AliveMessageIntervalSeconds { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the frequency at which ssdp alive notifications are transmitted. MIGRATING - TO BE REMOVED ONCE WEB HAS BEEN ALTERED.
|
|
||||||
/// </summary>
|
|
||||||
public int BlastAliveMessageIntervalSeconds
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return AliveMessageIntervalSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
AliveMessageIntervalSeconds = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the default user account that the dlna server uses.
|
|
||||||
/// </summary>
|
|
||||||
public string DefaultUserId { get; set; }
|
public string DefaultUserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether playTo device profiles should be created.
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoCreatePlayToProfiles { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to blast alive messages.
|
|
||||||
/// </summary>
|
|
||||||
public bool BlastAliveMessages { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// gets or sets a value indicating whether to send only matched host.
|
|
||||||
/// </summary>
|
|
||||||
public bool SendOnlyMatchedHost { get; set; } = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#nullable enable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Configuration;
|
using Emby.Dlna.Configuration;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
|
||||||
@@ -13,4 +14,19 @@ 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)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
Emby.Dlna/ConnectionManager/ConnectionManager.cs
Normal file
42
Emby.Dlna/ConnectionManager/ConnectionManager.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Emby.Dlna.Service;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Controller.Dlna;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Emby.Dlna.ConnectionManager
|
||||||
|
{
|
||||||
|
public class ConnectionManager : BaseService, IConnectionManager
|
||||||
|
{
|
||||||
|
private readonly IDlnaManager _dlna;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IServerConfigurationManager _config;
|
||||||
|
|
||||||
|
public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient)
|
||||||
|
: base(logger, httpClient)
|
||||||
|
{
|
||||||
|
_dlna = dlna;
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetServiceXml()
|
||||||
|
{
|
||||||
|
return new ConnectionManagerXmlBuilder().GetXml();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||||
|
{
|
||||||
|
var profile = _dlna.GetProfile(request.Headers) ??
|
||||||
|
_dlna.GetDefaultProfile();
|
||||||
|
|
||||||
|
return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Emby.Dlna.Service;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Dlna;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.ConnectionManager
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the <see cref="ConnectionManagerService" />.
|
|
||||||
/// </summary>
|
|
||||||
public class ConnectionManagerService : BaseService, IConnectionManager
|
|
||||||
{
|
|
||||||
private readonly IDlnaManager _dlna;
|
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ConnectionManagerService"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dlna">The <see cref="IDlnaManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
|
|
||||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
|
|
||||||
/// <param name="logger">The <see cref="ILogger{ConnectionManagerService}"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
|
|
||||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
|
|
||||||
public ConnectionManagerService(
|
|
||||||
IDlnaManager dlna,
|
|
||||||
IServerConfigurationManager config,
|
|
||||||
ILogger<ConnectionManagerService> logger,
|
|
||||||
IHttpClientFactory httpClientFactory)
|
|
||||||
: base(logger, httpClientFactory)
|
|
||||||
{
|
|
||||||
_dlna = dlna;
|
|
||||||
_config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string GetServiceXml()
|
|
||||||
{
|
|
||||||
return ConnectionManagerXmlBuilder.GetXml();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
|
||||||
{
|
|
||||||
var profile = _dlna.GetProfile(request.Headers) ??
|
|
||||||
_dlna.GetDefaultProfile();
|
|
||||||
|
|
||||||
return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
@@ -6,57 +7,45 @@ using Emby.Dlna.Service;
|
|||||||
|
|
||||||
namespace Emby.Dlna.ConnectionManager
|
namespace Emby.Dlna.ConnectionManager
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class ConnectionManagerXmlBuilder
|
||||||
/// Defines the <see cref="ConnectionManagerXmlBuilder" />.
|
|
||||||
/// </summary>
|
|
||||||
public static class ConnectionManagerXmlBuilder
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
public string GetXml()
|
||||||
/// Gets the ConnectionManager:1 service template.
|
|
||||||
/// See http://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An XML description of this service.</returns>
|
|
||||||
public static string GetXml()
|
|
||||||
{
|
{
|
||||||
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
|
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), GetStateVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the list of state variables for this invocation.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
|
||||||
private static IEnumerable<StateVariable> GetStateVariables()
|
private static IEnumerable<StateVariable> GetStateVariables()
|
||||||
{
|
{
|
||||||
var list = new List<StateVariable>
|
var list = new List<StateVariable>();
|
||||||
{
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "SourceProtocolInfo",
|
Name = "SourceProtocolInfo",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = true
|
SendsEvents = true
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "SinkProtocolInfo",
|
Name = "SinkProtocolInfo",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = true
|
SendsEvents = true
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "CurrentConnectionIDs",
|
Name = "CurrentConnectionIDs",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = true
|
SendsEvents = true
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_ConnectionStatus",
|
Name = "A_ARG_TYPE_ConnectionStatus",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false,
|
SendsEvents = false,
|
||||||
|
|
||||||
AllowedValues = new[]
|
AllowedValues = new string[]
|
||||||
{
|
{
|
||||||
"OK",
|
"OK",
|
||||||
"ContentFormatMismatch",
|
"ContentFormatMismatch",
|
||||||
@@ -64,56 +53,55 @@ namespace Emby.Dlna.ConnectionManager
|
|||||||
"UnreliableChannel",
|
"UnreliableChannel",
|
||||||
"Unknown"
|
"Unknown"
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_ConnectionManager",
|
Name = "A_ARG_TYPE_ConnectionManager",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_Direction",
|
Name = "A_ARG_TYPE_Direction",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false,
|
SendsEvents = false,
|
||||||
|
|
||||||
AllowedValues = new[]
|
AllowedValues = new string[]
|
||||||
{
|
{
|
||||||
"Output",
|
"Output",
|
||||||
"Input"
|
"Input"
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_ProtocolInfo",
|
Name = "A_ARG_TYPE_ProtocolInfo",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_ConnectionID",
|
Name = "A_ARG_TYPE_ConnectionID",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_AVTransportID",
|
Name = "A_ARG_TYPE_AVTransportID",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_RcsID",
|
Name = "A_ARG_TYPE_RcsID",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -11,19 +12,10 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Emby.Dlna.ConnectionManager
|
namespace Emby.Dlna.ConnectionManager
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Defines the <see cref="ControlHandler" />.
|
|
||||||
/// </summary>
|
|
||||||
public class ControlHandler : BaseControlHandler
|
public class ControlHandler : BaseControlHandler
|
||||||
{
|
{
|
||||||
private readonly DeviceProfile _profile;
|
private readonly DeviceProfile _profile;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
|
||||||
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
|
||||||
/// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
|
||||||
public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile)
|
public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile)
|
||||||
: base(config, logger)
|
: base(config, logger)
|
||||||
{
|
{
|
||||||
@@ -42,10 +34,6 @@ namespace Emby.Dlna.ConnectionManager
|
|||||||
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds the response to the GetProtocolInfo request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
|
||||||
private void HandleGetProtocolInfo(XmlWriter xmlWriter)
|
private void HandleGetProtocolInfo(XmlWriter xmlWriter)
|
||||||
{
|
{
|
||||||
xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);
|
xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
|
|
||||||
namespace Emby.Dlna.ConnectionManager
|
namespace Emby.Dlna.ConnectionManager
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class ServiceActionListBuilder
|
||||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
|
||||||
/// </summary>
|
|
||||||
public static class ServiceActionListBuilder
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
public IEnumerable<ServiceAction> GetActions()
|
||||||
/// Returns an enumerable of the ConnectionManagar:1 DLNA actions.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
|
||||||
public static IEnumerable<ServiceAction> GetActions()
|
|
||||||
{
|
{
|
||||||
var list = new List<ServiceAction>
|
var list = new List<ServiceAction>
|
||||||
{
|
{
|
||||||
@@ -28,10 +22,6 @@ namespace Emby.Dlna.ConnectionManager
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "PrepareForConnection".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction PrepareForConnection()
|
private static ServiceAction PrepareForConnection()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -91,10 +81,6 @@ namespace Emby.Dlna.ConnectionManager
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "GetCurrentConnectionInfo".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetCurrentConnectionInfo()
|
private static ServiceAction GetCurrentConnectionInfo()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -161,11 +147,7 @@ namespace Emby.Dlna.ConnectionManager
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private ServiceAction GetProtocolInfo()
|
||||||
/// Returns the action details for "GetProtocolInfo".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetProtocolInfo()
|
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
{
|
{
|
||||||
@@ -189,11 +171,7 @@ namespace Emby.Dlna.ConnectionManager
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private ServiceAction GetCurrentConnectionIDs()
|
||||||
/// Returns the action details for "GetCurrentConnectionIDs".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetCurrentConnectionIDs()
|
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
{
|
{
|
||||||
@@ -210,11 +188,7 @@ namespace Emby.Dlna.ConnectionManager
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private ServiceAction ConnectionComplete()
|
||||||
/// Returns the action details for "ConnectionComplete".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction ConnectionComplete()
|
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
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 MediaBrowser.Common.Net;
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.TV;
|
using MediaBrowser.Controller.TV;
|
||||||
@@ -19,10 +18,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Emby.Dlna.ContentDirectory
|
namespace Emby.Dlna.ContentDirectory
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class ContentDirectory : BaseService, IContentDirectory
|
||||||
/// Defines the <see cref="ContentDirectoryService" />.
|
|
||||||
/// </summary>
|
|
||||||
public class ContentDirectoryService : BaseService, IContentDirectory
|
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
@@ -36,31 +32,14 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly ITVSeriesManager _tvSeriesManager;
|
private readonly ITVSeriesManager _tvSeriesManager;
|
||||||
|
|
||||||
/// <summary>
|
public ContentDirectory(IDlnaManager dlna,
|
||||||
/// Initializes a new instance of the <see cref="ContentDirectoryService"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dlna">The <see cref="IDlnaManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="userDataManager">The <see cref="IUserDataManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="imageProcessor">The <see cref="IImageProcessor"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="libraryManager">The <see cref="ILibraryManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="userManager">The <see cref="IUserManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="logger">The <see cref="ILogger{ContentDirectoryService}"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="httpClient">The <see cref="IHttpClientFactory"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="localization">The <see cref="ILocalizationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="userViewManager">The <see cref="IUserViewManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
/// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
|
||||||
public ContentDirectoryService(
|
|
||||||
IDlnaManager dlna,
|
|
||||||
IUserDataManager userDataManager,
|
IUserDataManager userDataManager,
|
||||||
IImageProcessor imageProcessor,
|
IImageProcessor imageProcessor,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
ILogger<ContentDirectoryService> logger,
|
ILogger logger,
|
||||||
IHttpClientFactory httpClient,
|
IHttpClient httpClient,
|
||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
IUserViewManager userViewManager,
|
IUserViewManager userViewManager,
|
||||||
@@ -81,10 +60,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
_tvSeriesManager = tvSeriesManager;
|
_tvSeriesManager = tvSeriesManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private int SystemUpdateId
|
||||||
/// Gets the system id. (A unique id which changes on when our definition changes.)
|
|
||||||
/// </summary>
|
|
||||||
private static int SystemUpdateId
|
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@@ -97,18 +73,14 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string GetServiceXml()
|
public string GetServiceXml()
|
||||||
{
|
{
|
||||||
return ContentDirectoryXmlBuilder.GetXml();
|
return new ContentDirectoryXmlBuilder().GetXml();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||||
{
|
{
|
||||||
if (request == null)
|
var profile = _dlna.GetProfile(request.Headers) ??
|
||||||
{
|
_dlna.GetDefaultProfile();
|
||||||
throw new ArgumentNullException(nameof(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile();
|
|
||||||
|
|
||||||
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
|
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
@@ -133,11 +105,6 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
.ProcessControlRequestAsync(request);
|
.ProcessControlRequestAsync(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the user stored in the device profile.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
|
||||||
/// <returns>The <see cref="User"/>.</returns>
|
|
||||||
private User GetUser(DeviceProfile profile)
|
private User GetUser(DeviceProfile profile)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(profile.UserId))
|
if (!string.IsNullOrEmpty(profile.UserId))
|
||||||
@@ -164,13 +131,18 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
|
|
||||||
foreach (var user in _userManager.Users)
|
foreach (var user in _userManager.Users)
|
||||||
{
|
{
|
||||||
if (user.HasPermission(PermissionKind.IsAdministrator))
|
if (user.Policy.IsAdministrator)
|
||||||
{
|
{
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _userManager.Users.FirstOrDefault();
|
foreach (var user in _userManager.Users)
|
||||||
|
{
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
@@ -6,154 +7,142 @@ using Emby.Dlna.Service;
|
|||||||
|
|
||||||
namespace Emby.Dlna.ContentDirectory
|
namespace Emby.Dlna.ContentDirectory
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class ContentDirectoryXmlBuilder
|
||||||
/// Defines the <see cref="ContentDirectoryXmlBuilder" />.
|
|
||||||
/// </summary>
|
|
||||||
public static class ContentDirectoryXmlBuilder
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
public string GetXml()
|
||||||
/// Gets the ContentDirectory:1 service template.
|
|
||||||
/// See http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An XML description of this service.</returns>
|
|
||||||
public static string GetXml()
|
|
||||||
{
|
{
|
||||||
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
|
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
|
||||||
|
GetStateVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the list of state variables for this invocation.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
|
||||||
private static IEnumerable<StateVariable> GetStateVariables()
|
private static IEnumerable<StateVariable> GetStateVariables()
|
||||||
{
|
{
|
||||||
var list = new List<StateVariable>
|
var list = new List<StateVariable>();
|
||||||
{
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_Filter",
|
Name = "A_ARG_TYPE_Filter",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_SortCriteria",
|
Name = "A_ARG_TYPE_SortCriteria",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_Index",
|
Name = "A_ARG_TYPE_Index",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_Count",
|
Name = "A_ARG_TYPE_Count",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_UpdateID",
|
Name = "A_ARG_TYPE_UpdateID",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "SearchCapabilities",
|
Name = "SearchCapabilities",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "SortCapabilities",
|
Name = "SortCapabilities",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "SystemUpdateID",
|
Name = "SystemUpdateID",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = true
|
SendsEvents = true
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_SearchCriteria",
|
Name = "A_ARG_TYPE_SearchCriteria",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_Result",
|
Name = "A_ARG_TYPE_Result",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_ObjectID",
|
Name = "A_ARG_TYPE_ObjectID",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_BrowseFlag",
|
Name = "A_ARG_TYPE_BrowseFlag",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false,
|
SendsEvents = false,
|
||||||
|
|
||||||
AllowedValues = new[]
|
AllowedValues = new string[]
|
||||||
{
|
{
|
||||||
"BrowseMetadata",
|
"BrowseMetadata",
|
||||||
"BrowseDirectChildren"
|
"BrowseDirectChildren"
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_BrowseLetter",
|
Name = "A_ARG_TYPE_BrowseLetter",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_CategoryType",
|
Name = "A_ARG_TYPE_CategoryType",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_RID",
|
Name = "A_ARG_TYPE_RID",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_PosSec",
|
Name = "A_ARG_TYPE_PosSec",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_Featurelist",
|
Name = "A_ARG_TYPE_Featurelist",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.ContentDirectory
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the <see cref="ServerItem" />.
|
|
||||||
/// </summary>
|
|
||||||
internal class ServerItem
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ServerItem"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The <see cref="BaseItem"/>.</param>
|
|
||||||
public ServerItem(BaseItem item)
|
|
||||||
{
|
|
||||||
Item = item;
|
|
||||||
|
|
||||||
if (item is IItemByName && !(item is Folder))
|
|
||||||
{
|
|
||||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the underlying base item.
|
|
||||||
/// </summary>
|
|
||||||
public BaseItem Item { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the DLNA item type.
|
|
||||||
/// </summary>
|
|
||||||
public StubType? StubType { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
|
|
||||||
namespace Emby.Dlna.ContentDirectory
|
namespace Emby.Dlna.ContentDirectory
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class ServiceActionListBuilder
|
||||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
|
||||||
/// </summary>
|
|
||||||
public static class ServiceActionListBuilder
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
public IEnumerable<ServiceAction> GetActions()
|
||||||
/// Returns a list of services that this instance provides.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
|
||||||
public static IEnumerable<ServiceAction> GetActions()
|
|
||||||
{
|
{
|
||||||
return new[]
|
return new[]
|
||||||
{
|
{
|
||||||
@@ -27,10 +23,6 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "GetSystemUpdateID".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetGetSystemUpdateIDAction()
|
private static ServiceAction GetGetSystemUpdateIDAction()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -48,10 +40,6 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "GetSearchCapabilities".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetSearchCapabilitiesAction()
|
private static ServiceAction GetSearchCapabilitiesAction()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -69,10 +57,6 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "GetSortCapabilities".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetSortCapabilitiesAction()
|
private static ServiceAction GetSortCapabilitiesAction()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -90,10 +74,6 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "X_GetFeatureList".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetX_GetFeatureListAction()
|
private static ServiceAction GetX_GetFeatureListAction()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -111,10 +91,6 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "Search".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetSearchAction()
|
private static ServiceAction GetSearchAction()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -195,11 +171,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private ServiceAction GetBrowseAction()
|
||||||
/// Returns the action details for "Browse".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetBrowseAction()
|
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
{
|
{
|
||||||
@@ -279,11 +251,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private ServiceAction GetBrowseByLetterAction()
|
||||||
/// Returns the action details for "X_BrowseByLetter".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetBrowseByLetterAction()
|
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
{
|
{
|
||||||
@@ -370,11 +338,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private ServiceAction GetXSetBookmarkAction()
|
||||||
/// Returns the action details for "X_SetBookmark".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetXSetBookmarkAction()
|
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
#pragma warning disable SA1602
|
|
||||||
|
|
||||||
namespace Emby.Dlna.ContentDirectory
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the DLNA item types.
|
|
||||||
/// </summary>
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -7,17 +8,17 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
public class ControlRequest
|
public class ControlRequest
|
||||||
{
|
{
|
||||||
public ControlRequest(IHeaderDictionary headers)
|
public IHeaderDictionary Headers { get; set; }
|
||||||
{
|
|
||||||
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
@@ -11,16 +12,10 @@ namespace Emby.Dlna
|
|||||||
Headers = new Dictionary<string, string>();
|
Headers = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<string, string> Headers { get; }
|
public IDictionary<string, string> Headers { get; set; }
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -6,13 +7,14 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
using Emby.Dlna.Configuration;
|
||||||
using Emby.Dlna.ContentDirectory;
|
using Emby.Dlna.ContentDirectory;
|
||||||
using Jellyfin.Data.Entities;
|
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
@@ -22,25 +24,18 @@ using MediaBrowser.Model.Entities;
|
|||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
|
||||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
|
||||||
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
|
|
||||||
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
|
|
||||||
using Season = MediaBrowser.Controller.Entities.TV.Season;
|
|
||||||
using Series = MediaBrowser.Controller.Entities.TV.Series;
|
|
||||||
using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.Didl
|
namespace Emby.Dlna.Didl
|
||||||
{
|
{
|
||||||
public class DidlBuilder
|
public class DidlBuilder
|
||||||
{
|
{
|
||||||
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 readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||||
|
|
||||||
|
private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||||
|
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;
|
||||||
private readonly string _serverAddress;
|
private readonly string _serverAddress;
|
||||||
@@ -51,7 +46,6 @@ namespace Emby.Dlna.Didl
|
|||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
|
|
||||||
public DidlBuilder(
|
public DidlBuilder(
|
||||||
DeviceProfile profile,
|
DeviceProfile profile,
|
||||||
@@ -63,8 +57,7 @@ namespace Emby.Dlna.Didl
|
|||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder)
|
||||||
ILibraryManager libraryManager)
|
|
||||||
{
|
{
|
||||||
_profile = profile;
|
_profile = profile;
|
||||||
_user = user;
|
_user = user;
|
||||||
@@ -76,7 +69,6 @@ namespace Emby.Dlna.Didl
|
|||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_libraryManager = libraryManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string NormalizeDlnaMediaUrl(string url)
|
public static string NormalizeDlnaMediaUrl(string url)
|
||||||
@@ -84,7 +76,7 @@ namespace Emby.Dlna.Didl
|
|||||||
return url + "&dlnaheaders=true";
|
return url + "&dlnaheaders=true";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo)
|
public string GetItemDidl(DlnaOptions options, BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo)
|
||||||
{
|
{
|
||||||
var settings = new XmlWriterSettings
|
var settings = new XmlWriterSettings
|
||||||
{
|
{
|
||||||
@@ -96,24 +88,23 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
|
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
|
||||||
{
|
{
|
||||||
// If this using are changed to single lines, then write.Flush needs to be appended before the return.
|
|
||||||
using (var writer = XmlWriter.Create(builder, settings))
|
using (var writer = XmlWriter.Create(builder, settings))
|
||||||
{
|
{
|
||||||
// writer.WriteStartDocument();
|
//writer.WriteStartDocument();
|
||||||
|
|
||||||
writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
|
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
|
||||||
|
|
||||||
writer.WriteAttributeString("xmlns", "dc", null, NsDc);
|
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
|
||||||
writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
|
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
|
||||||
writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
|
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
|
||||||
// didl.SetAttribute("xmlns:sec", NS_SEC);
|
//didl.SetAttribute("xmlns:sec", NS_SEC);
|
||||||
|
|
||||||
WriteXmlRootAttributes(_profile, writer);
|
WriteXmlRootAttributes(_profile, writer);
|
||||||
|
|
||||||
WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo);
|
WriteItemElement(options, writer, item, user, context, null, deviceId, filter, streamInfo);
|
||||||
|
|
||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
// writer.WriteEndDocument();
|
//writer.WriteEndDocument();
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
@@ -124,7 +115,7 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
foreach (var att in profile.XmlRootAttributes)
|
foreach (var att in profile.XmlRootAttributes)
|
||||||
{
|
{
|
||||||
var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (parts.Length == 2)
|
if (parts.Length == 2)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString(parts[0], parts[1], null, att.Value);
|
writer.WriteAttributeString(parts[0], parts[1], null, att.Value);
|
||||||
@@ -137,6 +128,7 @@ namespace Emby.Dlna.Didl
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void WriteItemElement(
|
public void WriteItemElement(
|
||||||
|
DlnaOptions options,
|
||||||
XmlWriter writer,
|
XmlWriter writer,
|
||||||
BaseItem item,
|
BaseItem item,
|
||||||
User user,
|
User user,
|
||||||
@@ -148,7 +140,7 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
var clientId = GetClientId(item, null);
|
var clientId = GetClientId(item, null);
|
||||||
|
|
||||||
writer.WriteStartElement(string.Empty, "item", NsDidl);
|
writer.WriteStartElement(string.Empty, "item", NS_DIDL);
|
||||||
|
|
||||||
writer.WriteAttributeString("restricted", "1");
|
writer.WriteAttributeString("restricted", "1");
|
||||||
writer.WriteAttributeString("id", clientId);
|
writer.WriteAttributeString("id", clientId);
|
||||||
@@ -173,23 +165,25 @@ namespace Emby.Dlna.Didl
|
|||||||
// refID?
|
// refID?
|
||||||
// storeAttribute(itemNode, object, ClassProperties.REF_ID, false);
|
// storeAttribute(itemNode, object, ClassProperties.REF_ID, false);
|
||||||
|
|
||||||
if (item is IHasMediaSources)
|
var hasMediaSources = item as IHasMediaSources;
|
||||||
|
|
||||||
|
if (hasMediaSources != null)
|
||||||
{
|
{
|
||||||
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
AddAudioResource(writer, item, deviceId, filter, streamInfo);
|
AddAudioResource(options, writer, item, deviceId, filter, streamInfo);
|
||||||
}
|
}
|
||||||
else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
AddVideoResource(writer, item, deviceId, filter, streamInfo);
|
AddVideoResource(options, writer, item, deviceId, filter, streamInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddCover(item, null, writer);
|
AddCover(item, context, null, writer);
|
||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
private void AddVideoResource(DlnaOptions options, XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
||||||
{
|
{
|
||||||
if (streamInfo == null)
|
if (streamInfo == null)
|
||||||
{
|
{
|
||||||
@@ -208,8 +202,7 @@ namespace Emby.Dlna.Didl
|
|||||||
var targetWidth = streamInfo.TargetWidth;
|
var targetWidth = streamInfo.TargetWidth;
|
||||||
var targetHeight = streamInfo.TargetHeight;
|
var targetHeight = streamInfo.TargetHeight;
|
||||||
|
|
||||||
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
|
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container,
|
||||||
streamInfo.Container,
|
|
||||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
targetWidth,
|
targetWidth,
|
||||||
@@ -234,7 +227,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
foreach (var contentFeature in contentFeatureList)
|
foreach (var contentFeature in contentFeatureList)
|
||||||
{
|
{
|
||||||
AddVideoResource(writer, filter, contentFeature, streamInfo);
|
AddVideoResource(writer, video, deviceId, filter, contentFeature, streamInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
var subtitleProfiles = streamInfo.GetSubtitleProfiles(_mediaEncoder, false, _serverAddress, _accessToken);
|
var subtitleProfiles = streamInfo.GetSubtitleProfiles(_mediaEncoder, false, _serverAddress, _accessToken);
|
||||||
@@ -281,7 +274,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", NsDidl);
|
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
||||||
|
|
||||||
writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*");
|
writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*");
|
||||||
|
|
||||||
@@ -290,11 +283,8 @@ namespace Emby.Dlna.Didl
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
||||||
var protocolInfo = string.Format(
|
var protocolInfo = string.Format("http-get:*:text/{0}:*", info.Format.ToLowerInvariant());
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"http-get:*:text/{0}:*",
|
|
||||||
info.Format.ToLowerInvariant());
|
|
||||||
writer.WriteAttributeString("protocolInfo", protocolInfo);
|
writer.WriteAttributeString("protocolInfo", protocolInfo);
|
||||||
|
|
||||||
writer.WriteString(info.Url);
|
writer.WriteString(info.Url);
|
||||||
@@ -304,9 +294,9 @@ namespace Emby.Dlna.Didl
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo)
|
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, string contentFeatures, StreamInfo streamInfo)
|
||||||
{
|
{
|
||||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
||||||
|
|
||||||
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
|
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
|
||||||
|
|
||||||
@@ -346,13 +336,7 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
if (targetWidth.HasValue && targetHeight.HasValue)
|
if (targetWidth.HasValue && targetHeight.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString(
|
writer.WriteAttributeString("resolution", string.Format("{0}x{1}", targetWidth.Value, targetHeight.Value));
|
||||||
"resolution",
|
|
||||||
string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"{0}x{1}",
|
|
||||||
targetWidth.Value,
|
|
||||||
targetHeight.Value));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,8 +350,7 @@ namespace Emby.Dlna.Didl
|
|||||||
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
|
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaProfile = _profile.GetVideoMediaProfile(
|
var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container,
|
||||||
streamInfo.Container,
|
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioBitrate,
|
streamInfo.TargetAudioBitrate,
|
||||||
@@ -387,19 +370,17 @@ namespace Emby.Dlna.Didl
|
|||||||
streamInfo.TargetVideoCodecTag,
|
streamInfo.TargetVideoCodecTag,
|
||||||
streamInfo.IsTargetAVC);
|
streamInfo.IsTargetAVC);
|
||||||
|
|
||||||
var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal));
|
var filename = url.Substring(0, url.IndexOf('?'));
|
||||||
|
|
||||||
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
|
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
|
||||||
? MimeTypes.GetMimeType(filename)
|
? MimeTypes.GetMimeType(filename)
|
||||||
: mediaProfile.MimeType;
|
: mediaProfile.MimeType;
|
||||||
|
|
||||||
writer.WriteAttributeString(
|
writer.WriteAttributeString("protocolInfo", string.Format(
|
||||||
"protocolInfo",
|
|
||||||
string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"http-get:*:{0}:{1}",
|
"http-get:*:{0}:{1}",
|
||||||
mimeType,
|
mimeType,
|
||||||
contentFeatures));
|
contentFeatures
|
||||||
|
));
|
||||||
|
|
||||||
writer.WriteString(url);
|
writer.WriteString(url);
|
||||||
|
|
||||||
@@ -430,6 +411,7 @@ namespace Emby.Dlna.Didl
|
|||||||
case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
|
case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
|
||||||
case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
|
case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
|
||||||
case StubType.Series: return _localization.GetLocalizedString("Shows");
|
case StubType.Series: return _localization.GetLocalizedString("Shows");
|
||||||
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,9 +508,9 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
|
private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
|
||||||
|
|
||||||
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
private void AddAudioResource(DlnaOptions options, XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
||||||
{
|
{
|
||||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
||||||
|
|
||||||
if (streamInfo == null)
|
if (streamInfo == null)
|
||||||
{
|
{
|
||||||
@@ -585,22 +567,20 @@ namespace Emby.Dlna.Didl
|
|||||||
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
|
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaProfile = _profile.GetAudioMediaProfile(
|
var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container,
|
||||||
streamInfo.Container,
|
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
targetChannels,
|
targetChannels,
|
||||||
targetAudioBitrate,
|
targetAudioBitrate,
|
||||||
targetSampleRate,
|
targetSampleRate,
|
||||||
targetAudioBitDepth);
|
targetAudioBitDepth);
|
||||||
|
|
||||||
var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal));
|
var filename = url.Substring(0, url.IndexOf('?'));
|
||||||
|
|
||||||
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
|
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
|
||||||
? MimeTypes.GetMimeType(filename)
|
? MimeTypes.GetMimeType(filename)
|
||||||
: mediaProfile.MimeType;
|
: mediaProfile.MimeType;
|
||||||
|
|
||||||
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
|
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container,
|
||||||
streamInfo.Container,
|
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
targetAudioBitrate,
|
targetAudioBitrate,
|
||||||
targetSampleRate,
|
targetSampleRate,
|
||||||
@@ -610,13 +590,11 @@ namespace Emby.Dlna.Didl
|
|||||||
streamInfo.RunTimeTicks ?? 0,
|
streamInfo.RunTimeTicks ?? 0,
|
||||||
streamInfo.TranscodeSeekInfo);
|
streamInfo.TranscodeSeekInfo);
|
||||||
|
|
||||||
writer.WriteAttributeString(
|
writer.WriteAttributeString("protocolInfo", string.Format(
|
||||||
"protocolInfo",
|
|
||||||
string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"http-get:*:{0}:{1}",
|
"http-get:*:{0}:{1}",
|
||||||
mimeType,
|
mimeType,
|
||||||
contentFeatures));
|
contentFeatures
|
||||||
|
));
|
||||||
|
|
||||||
writer.WriteString(url);
|
writer.WriteString(url);
|
||||||
|
|
||||||
@@ -631,7 +609,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", NsDidl);
|
writer.WriteStartElement(string.Empty, "container", NS_DIDL);
|
||||||
|
|
||||||
writer.WriteAttributeString("restricted", "1");
|
writer.WriteAttributeString("restricted", "1");
|
||||||
writer.WriteAttributeString("searchable", "1");
|
writer.WriteAttributeString("searchable", "1");
|
||||||
@@ -639,7 +617,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
var clientId = GetClientId(folder, stubType);
|
var clientId = GetClientId(folder, stubType);
|
||||||
|
|
||||||
if (string.Equals(requestedId, "0", StringComparison.Ordinal))
|
if (string.Equals(requestedId, "0"))
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("id", "0");
|
writer.WriteAttributeString("id", "0");
|
||||||
writer.WriteAttributeString("parentID", "-1");
|
writer.WriteAttributeString("parentID", "-1");
|
||||||
@@ -668,7 +646,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
AddGeneralProperties(folder, stubType, context, writer, filter);
|
AddGeneralProperties(folder, stubType, context, writer, filter);
|
||||||
|
|
||||||
AddCover(folder, stubType, writer);
|
AddCover(folder, context, stubType, writer);
|
||||||
|
|
||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
}
|
}
|
||||||
@@ -680,7 +658,7 @@ namespace Emby.Dlna.Didl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
XmlAttribute secAttribute = null;
|
MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null;
|
||||||
foreach (var attribute in _profile.XmlRootAttributes)
|
foreach (var attribute in _profile.XmlRootAttributes)
|
||||||
{
|
{
|
||||||
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -701,24 +679,21 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (playbackPositionTicks > 0)
|
if (playbackPositionTicks > 0)
|
||||||
{
|
{
|
||||||
var elementValue = string.Format(
|
var elementValue = string.Format("BM={0}", Convert.ToInt32(TimeSpan.FromTicks(playbackPositionTicks).TotalSeconds).ToString(_usCulture));
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"BM={0}",
|
|
||||||
Convert.ToInt32(TimeSpan.FromTicks(playbackPositionTicks).TotalSeconds));
|
|
||||||
AddValue(writer, "sec", "dcmInfo", elementValue, secAttribute.Value);
|
AddValue(writer, "sec", "dcmInfo", elementValue, secAttribute.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds fields used by both items and folders.
|
/// Adds fields used by both items and folders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
|
private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
|
||||||
{
|
{
|
||||||
// Don't filter on dc:title because not all devices will include it in the filter
|
// Don't filter on dc:title because not all devices will include it in the filter
|
||||||
// MediaMonkey for example won't display content without a title
|
// 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), NsDc);
|
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteObjectClass(writer, item, itemStubType);
|
WriteObjectClass(writer, item, itemStubType);
|
||||||
@@ -727,7 +702,7 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
if (item.PremiereDate.HasValue)
|
if (item.PremiereDate.HasValue)
|
||||||
{
|
{
|
||||||
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
|
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NS_DC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,13 +710,13 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
foreach (var genre in item.Genres)
|
foreach (var genre in item.Genres)
|
||||||
{
|
{
|
||||||
AddValue(writer, "upnp", "genre", genre, NsUpnp);
|
AddValue(writer, "upnp", "genre", genre, NS_UPNP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var studio in item.Studios)
|
foreach (var studio in item.Studios)
|
||||||
{
|
{
|
||||||
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
|
AddValue(writer, "upnp", "publisher", studio, NS_UPNP);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(item is Folder))
|
if (!(item is Folder))
|
||||||
@@ -752,29 +727,27 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(desc))
|
if (!string.IsNullOrWhiteSpace(desc))
|
||||||
{
|
{
|
||||||
AddValue(writer, "dc", "description", desc, NsDc);
|
AddValue(writer, "dc", "description", desc, NS_DC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//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, NsUpnp);
|
// AddValue(writer, "upnp", "longDescription", item.Overview, NS_UPNP);
|
||||||
// }
|
|
||||||
// }
|
// }
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, NsDc);
|
AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.Contains("upnp:rating"))
|
if (filter.Contains("upnp:rating"))
|
||||||
{
|
{
|
||||||
AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp);
|
AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,7 +759,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", NsUpnp);
|
writer.WriteStartElement("upnp", "class", NS_UPNP);
|
||||||
|
|
||||||
if (item.IsDisplayedAsFolder || stubType.HasValue)
|
if (item.IsDisplayedAsFolder || stubType.HasValue)
|
||||||
{
|
{
|
||||||
@@ -859,36 +832,37 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
private void AddPeople(BaseItem item, XmlWriter writer)
|
private void AddPeople(BaseItem item, XmlWriter writer)
|
||||||
{
|
{
|
||||||
if (!item.SupportsPeople)
|
//var types = new[]
|
||||||
{
|
//{
|
||||||
return;
|
// PersonType.Director,
|
||||||
}
|
// PersonType.Writer,
|
||||||
|
// PersonType.Producer,
|
||||||
|
// PersonType.Composer,
|
||||||
|
// "Creator"
|
||||||
|
//};
|
||||||
|
|
||||||
var types = new[]
|
//var people = _libraryManager.GetPeople(item);
|
||||||
{
|
|
||||||
PersonType.Director,
|
|
||||||
PersonType.Writer,
|
|
||||||
PersonType.Producer,
|
|
||||||
PersonType.Composer,
|
|
||||||
"creator"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Seeing some LG models locking up due content with large lists of people
|
//var index = 0;
|
||||||
// The actual issue might just be due to processing a more metadata than it can handle
|
|
||||||
var people = _libraryManager.GetPeople(
|
|
||||||
new InternalPeopleQuery
|
|
||||||
{
|
|
||||||
ItemId = item.Id,
|
|
||||||
Limit = 6
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var actor in people)
|
//// Seeing some LG models locking up due content with large lists of people
|
||||||
{
|
//// The actual issue might just be due to processing a more metadata than it can handle
|
||||||
var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
|
//var limit = 6;
|
||||||
?? PersonType.Actor;
|
|
||||||
|
|
||||||
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
|
//foreach (var actor in people)
|
||||||
}
|
//{
|
||||||
|
// var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
|
||||||
|
// ?? PersonType.Actor;
|
||||||
|
|
||||||
|
// AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP);
|
||||||
|
|
||||||
|
// index++;
|
||||||
|
|
||||||
|
// if (index >= limit)
|
||||||
|
// {
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
|
private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
|
||||||
@@ -901,8 +875,8 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
foreach (var artist in hasArtists.Artists)
|
foreach (var artist in hasArtists.Artists)
|
||||||
{
|
{
|
||||||
AddValue(writer, "upnp", "artist", artist, NsUpnp);
|
AddValue(writer, "upnp", "artist", artist, NS_UPNP);
|
||||||
AddValue(writer, "dc", "creator", artist, NsDc);
|
AddValue(writer, "dc", "creator", artist, NS_DC);
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -922,16 +896,16 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(item.Album))
|
if (!string.IsNullOrWhiteSpace(item.Album))
|
||||||
{
|
{
|
||||||
AddValue(writer, "upnp", "album", item.Album, NsUpnp);
|
AddValue(writer, "upnp", "album", item.Album, NS_UPNP);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.IndexNumber.HasValue)
|
if (item.IndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
|
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
|
||||||
|
|
||||||
if (item is Episode)
|
if (item is Episode)
|
||||||
{
|
{
|
||||||
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
|
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -940,7 +914,7 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
writer.WriteStartElement("upnp", "artist", NsUpnp);
|
writer.WriteStartElement("upnp", "artist", NS_UPNP);
|
||||||
writer.WriteAttributeString("role", "AlbumArtist");
|
writer.WriteAttributeString("role", "AlbumArtist");
|
||||||
|
|
||||||
writer.WriteString(name);
|
writer.WriteString(name);
|
||||||
@@ -949,7 +923,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,11 +935,11 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer)
|
private void AddCover(BaseItem item, BaseItem context, StubType? stubType, XmlWriter writer)
|
||||||
{
|
{
|
||||||
ImageDownloadInfo imageInfo = GetImageInfo(item);
|
ImageDownloadInfo imageInfo = GetImageInfo(item);
|
||||||
|
|
||||||
@@ -976,14 +950,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", NsUpnp);
|
writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP);
|
||||||
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
|
writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _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", NsUpnp, iconUrlInfo.url);
|
writer.WriteElementString("upnp", "icon", NS_UPNP, iconUrlInfo.Url);
|
||||||
|
|
||||||
if (!_profile.EnableAlbumArtInDidl)
|
if (!_profile.EnableAlbumArtInDidl)
|
||||||
{
|
{
|
||||||
@@ -1007,10 +981,20 @@ namespace Emby.Dlna.Didl
|
|||||||
}
|
}
|
||||||
|
|
||||||
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
|
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddImageResElement(
|
private void AddEmbeddedImageAsCover(string name, XmlWriter writer)
|
||||||
BaseItem item,
|
{
|
||||||
|
writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP);
|
||||||
|
writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn);
|
||||||
|
writer.WriteString(_serverAddress + "/Dlna/icons/people480.jpg");
|
||||||
|
writer.WriteFullEndElement();
|
||||||
|
|
||||||
|
writer.WriteElementString("upnp", "icon", NS_UPNP, _serverAddress + "/Dlna/icons/people48.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddImageResElement(BaseItem item,
|
||||||
XmlWriter writer,
|
XmlWriter writer,
|
||||||
int maxWidth,
|
int maxWidth,
|
||||||
int maxHeight,
|
int maxHeight,
|
||||||
@@ -1026,29 +1010,25 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format);
|
var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format);
|
||||||
|
|
||||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
writer.WriteAttributeString(
|
writer.WriteAttributeString("protocolInfo", string.Format(
|
||||||
"protocolInfo",
|
|
||||||
string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"http-get:*:{0}:{1}",
|
"http-get:*:{0}:{1}",
|
||||||
MimeTypes.GetMimeType("file." + format),
|
MimeTypes.GetMimeType("file." + format),
|
||||||
contentFeatures));
|
contentFeatures
|
||||||
|
));
|
||||||
|
|
||||||
writer.WriteAttributeString(
|
writer.WriteAttributeString("resolution", string.Format("{0}x{1}", width, height));
|
||||||
"resolution",
|
|
||||||
string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
|
|
||||||
|
|
||||||
writer.WriteString(albumartUrlInfo.url);
|
writer.WriteString(albumartUrlInfo.Url);
|
||||||
|
|
||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
}
|
}
|
||||||
@@ -1059,12 +1039,10 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
return GetImageInfo(item, ImageType.Primary);
|
return GetImageInfo(item, ImageType.Primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.HasImage(ImageType.Thumb))
|
if (item.HasImage(ImageType.Thumb))
|
||||||
{
|
{
|
||||||
return GetImageInfo(item, ImageType.Thumb);
|
return GetImageInfo(item, ImageType.Thumb);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.HasImage(ImageType.Backdrop))
|
if (item.HasImage(ImageType.Backdrop))
|
||||||
{
|
{
|
||||||
if (item is Channel)
|
if (item is Channel)
|
||||||
@@ -1144,15 +1122,29 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (width == -1 || height == -1)
|
else if (width == -1 || height == -1)
|
||||||
{
|
{
|
||||||
width = null;
|
width = null;
|
||||||
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);
|
||||||
@@ -1169,6 +1161,30 @@ 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);
|
||||||
@@ -1186,11 +1202,9 @@ namespace Emby.Dlna.Didl
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
|
private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
|
||||||
{
|
{
|
||||||
var url = string.Format(
|
var url = string.Format("{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0",
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0",
|
|
||||||
_serverAddress,
|
_serverAddress,
|
||||||
info.ItemId.ToString("N", CultureInfo.InvariantCulture),
|
info.ItemId.ToString("N", CultureInfo.InvariantCulture),
|
||||||
info.Type,
|
info.Type,
|
||||||
@@ -1224,26 +1238,12 @@ namespace Emby.Dlna.Didl
|
|||||||
// just lie
|
// just lie
|
||||||
info.IsDirectStream = true;
|
info.IsDirectStream = true;
|
||||||
|
|
||||||
return (url, width, height);
|
return new ImageUrlInfo
|
||||||
}
|
|
||||||
|
|
||||||
private class ImageDownloadInfo
|
|
||||||
{
|
{
|
||||||
internal Guid ItemId { get; set; }
|
Url = url,
|
||||||
|
Width = width,
|
||||||
internal string ImageTag { get; set; }
|
Height = height
|
||||||
|
};
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using MediaBrowser.Model.Extensions;
|
||||||
|
|
||||||
namespace Emby.Dlna.Didl
|
namespace Emby.Dlna.Didl
|
||||||
{
|
{
|
||||||
@@ -12,18 +14,21 @@ namespace Emby.Dlna.Didl
|
|||||||
public Filter()
|
public Filter()
|
||||||
: this("*")
|
: this("*")
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Filter(string filter)
|
public Filter(string filter)
|
||||||
{
|
{
|
||||||
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
_fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
|
_fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Contains(string field)
|
public bool Contains(string field)
|
||||||
{
|
{
|
||||||
return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase));
|
// 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 true;
|
||||||
|
//return _all || ListHelper.ContainsIgnoreCase(_fields, field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
#pragma warning disable CA1305
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -30,6 +30,7 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public StringWriterWithEncoding(Encoding encoding)
|
public StringWriterWithEncoding(Encoding encoding)
|
||||||
{
|
{
|
||||||
_encoding = encoding;
|
_encoding = encoding;
|
||||||
@@ -53,6 +54,6 @@ namespace Emby.Dlna.Didl
|
|||||||
_encoding = encoding;
|
_encoding = encoding;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Encoding Encoding => _encoding ?? base.Encoding;
|
public override Encoding Encoding => (null == _encoding) ? base.Encoding : _encoding;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
#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)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -31,7 +32,7 @@ namespace Emby.Dlna
|
|||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
private readonly IXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly ILogger<DlnaManager> _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
||||||
@@ -49,20 +50,16 @@ namespace Emby.Dlna
|
|||||||
_xmlSerializer = xmlSerializer;
|
_xmlSerializer = xmlSerializer;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
_logger = loggerFactory.CreateLogger<DlnaManager>();
|
_logger = loggerFactory.CreateLogger("Dlna");
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
_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().ConfigureAwait(false);
|
await ExtractSystemProfilesAsync();
|
||||||
LoadProfiles();
|
LoadProfiles();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -92,6 +89,7 @@ namespace Emby.Dlna
|
|||||||
.Select(i => i.Item2)
|
.Select(i => i.Item2)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeviceProfile GetDefaultProfile()
|
public DeviceProfile GetDefaultProfile()
|
||||||
@@ -126,92 +124,83 @@ namespace Emby.Dlna
|
|||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
||||||
builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
|
builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
|
||||||
builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
|
builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
|
||||||
builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
|
builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
|
||||||
builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
|
builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
|
||||||
builder.Append("ModelName:").AppendLine(profile.ModelName);
|
builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
|
||||||
builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
|
builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
|
||||||
builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
|
builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
|
||||||
builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
|
builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
|
||||||
|
builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
|
||||||
|
|
||||||
_logger.LogInformation(builder.ToString());
|
_logger.LogInformation(builder.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
|
if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
|
||||||
{
|
|
||||||
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
|
||||||
{
|
{
|
||||||
|
if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
|
||||||
|
{
|
||||||
|
if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
|
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
|
||||||
{
|
{
|
||||||
if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
|
if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
|
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
|
||||||
{
|
{
|
||||||
if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
|
if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
|
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
|
||||||
{
|
{
|
||||||
if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
|
if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelName))
|
if (!string.IsNullOrEmpty(profileInfo.ModelName))
|
||||||
{
|
{
|
||||||
if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
|
if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
|
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
|
||||||
{
|
{
|
||||||
if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
|
if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
|
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
|
||||||
{
|
{
|
||||||
if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
|
if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
|
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
|
||||||
{
|
{
|
||||||
if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
|
if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
private bool IsRegexMatch(string input, string pattern)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
return Regex.IsMatch(input, pattern);
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
@@ -235,7 +224,7 @@ namespace Emby.Dlna
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
|
var headerString = string.Join(", ", headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
|
||||||
_logger.LogDebug("No matching device profile found. {0}", headerString);
|
_logger.LogDebug("No matching device profile found. {0}", headerString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +252,7 @@ namespace Emby.Dlna
|
|||||||
return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
|
return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
|
||||||
case HeaderMatchType.Substring:
|
case HeaderMatchType.Substring:
|
||||||
var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
|
var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
|
||||||
// _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
|
//_logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
|
||||||
return isMatch;
|
return isMatch;
|
||||||
case HeaderMatchType.Regex:
|
case HeaderMatchType.Regex:
|
||||||
return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
|
return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
|
||||||
@@ -275,6 +264,10 @@ 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
|
||||||
@@ -378,14 +371,14 @@ namespace Emby.Dlna
|
|||||||
|
|
||||||
foreach (var name in _assembly.GetManifestResourceNames())
|
foreach (var name in _assembly.GetManifestResourceNames())
|
||||||
{
|
{
|
||||||
if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
|
if (!name.StartsWith(namespaceName))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = Path.Join(
|
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
|
||||||
systemProfilesPath,
|
|
||||||
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
|
var path = Path.Combine(systemProfilesPath, filename);
|
||||||
|
|
||||||
using (var stream = _assembly.GetManifestResourceStream(name))
|
using (var stream = _assembly.GetManifestResourceStream(name))
|
||||||
{
|
{
|
||||||
@@ -397,7 +390,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).ConfigureAwait(false);
|
await stream.CopyToAsync(fileStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,7 +440,6 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
throw new ArgumentException("Profile is missing Id");
|
throw new ArgumentException("Profile is missing Id");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(profile.Name))
|
if (string.IsNullOrEmpty(profile.Name))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Profile is missing Name");
|
throw new ArgumentException("Profile is missing Name");
|
||||||
@@ -473,7 +465,6 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
|
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
SerializeToXml(profile, path);
|
SerializeToXml(profile, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,10 +475,10 @@ namespace Emby.Dlna
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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 serialize 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">The device profile.</param>
|
/// <param name="profile"></param>
|
||||||
/// <returns>The re-serialized device profile.</returns>
|
/// <returns></returns>
|
||||||
private DeviceProfile ReserializeProfile(DeviceProfile profile)
|
private DeviceProfile ReserializeProfile(DeviceProfile profile)
|
||||||
{
|
{
|
||||||
if (profile.GetType() == typeof(DeviceProfile))
|
if (profile.GetType() == typeof(DeviceProfile))
|
||||||
@@ -500,9 +491,16 @@ 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 = GetDefaultProfile();
|
var profile = GetProfile(headers) ??
|
||||||
|
GetDefaultProfile();
|
||||||
|
|
||||||
var serverId = _appHost.SystemId;
|
var serverId = _appHost.SystemId;
|
||||||
|
|
||||||
@@ -523,15 +521,7 @@ 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
|
||||||
{
|
{
|
||||||
@@ -577,9 +567,9 @@ namespace Emby.Dlna
|
|||||||
new Foobar2000Profile(),
|
new Foobar2000Profile(),
|
||||||
new SharpSmartTvProfile(),
|
new SharpSmartTvProfile(),
|
||||||
new MediaMonkeyProfile(),
|
new MediaMonkeyProfile(),
|
||||||
// new Windows81Profile(),
|
//new Windows81Profile(),
|
||||||
// new WindowsMediaCenterProfile(),
|
//new WindowsMediaCenterProfile(),
|
||||||
// new WindowsPhoneProfile(),
|
//new WindowsPhoneProfile(),
|
||||||
new DirectTvProfile(),
|
new DirectTvProfile(),
|
||||||
new DishHopperJoeyProfile(),
|
new DishHopperJoeyProfile(),
|
||||||
new DefaultProfile(),
|
new DefaultProfile(),
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<ProjectGuid>{805844AB-E92F-45E6-9D99-4F6D48D129A5}</ProjectGuid>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\SharedVersion.cs" />
|
<Compile Include="..\SharedVersion.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -17,10 +12,10 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
@@ -80,7 +75,6 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
@@ -15,6 +16,6 @@ namespace Emby.Dlna
|
|||||||
|
|
||||||
public string ContentType { get; set; }
|
public string ContentType { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, string> Headers { get; }
|
public Dictionary<string, string> Headers { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -6,7 +7,6 @@ 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;
|
||||||
@@ -15,27 +15,24 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Emby.Dlna.Eventing
|
namespace Emby.Dlna.Eventing
|
||||||
{
|
{
|
||||||
public class DlnaEventManager : IDlnaEventManager
|
public class EventManager : IEventManager
|
||||||
{
|
{
|
||||||
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 IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClient _httpClient;
|
||||||
|
|
||||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
public EventManager(ILogger logger, IHttpClient httpClient)
|
||||||
|
|
||||||
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
|
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
|
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||||
{
|
{
|
||||||
var subscription = GetSubscription(subscriptionId, false);
|
var subscription = GetSubscription(subscriptionId, false);
|
||||||
if (subscription != null)
|
|
||||||
{
|
|
||||||
subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
|
subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
|
||||||
int timeoutSeconds = subscription.TimeoutSeconds;
|
int timeoutSeconds = subscription.TimeoutSeconds;
|
||||||
subscription.SubscriptionTime = DateTime.UtcNow;
|
subscription.SubscriptionTime = DateTime.UtcNow;
|
||||||
@@ -49,20 +46,12 @@ namespace Emby.Dlna.Eventing
|
|||||||
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EventSubscriptionResponse
|
|
||||||
{
|
|
||||||
Content = string.Empty,
|
|
||||||
ContentType = "text/plain"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||||
{
|
{
|
||||||
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(
|
_logger.LogDebug("Creating event subscription for {0} with timeout of {1} to {2}",
|
||||||
"Creating event subscription for {0} with timeout of {1} to {2}",
|
|
||||||
notificationType,
|
notificationType,
|
||||||
timeout,
|
timeout,
|
||||||
callbackUrl);
|
callbackUrl);
|
||||||
@@ -72,8 +61,7 @@ namespace Emby.Dlna.Eventing
|
|||||||
Id = id,
|
Id = id,
|
||||||
CallbackUrl = callbackUrl,
|
CallbackUrl = callbackUrl,
|
||||||
SubscriptionTime = DateTime.UtcNow,
|
SubscriptionTime = DateTime.UtcNow,
|
||||||
TimeoutSeconds = timeout,
|
TimeoutSeconds = timeout
|
||||||
NotificationType = notificationType
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout);
|
return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout);
|
||||||
@@ -84,7 +72,7 @@ namespace Emby.Dlna.Eventing
|
|||||||
if (!string.IsNullOrEmpty(header))
|
if (!string.IsNullOrEmpty(header))
|
||||||
{
|
{
|
||||||
// Starts with SECOND-
|
// Starts with SECOND-
|
||||||
header = header.Split('-')[^1];
|
header = header.Split('-').Last();
|
||||||
|
|
||||||
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
|
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
|
||||||
{
|
{
|
||||||
@@ -99,7 +87,7 @@ namespace Emby.Dlna.Eventing
|
|||||||
{
|
{
|
||||||
_logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
|
_logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
|
||||||
|
|
||||||
_subscriptions.TryRemove(subscriptionId, out _);
|
_subscriptions.TryRemove(subscriptionId, out EventSubscription sub);
|
||||||
|
|
||||||
return new EventSubscriptionResponse
|
return new EventSubscriptionResponse
|
||||||
{
|
{
|
||||||
@@ -108,6 +96,7 @@ 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
|
||||||
@@ -156,30 +145,33 @@ 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>");
|
||||||
.Append('<')
|
builder.Append("<" + key + ">");
|
||||||
.Append(key)
|
builder.Append(stateVariables[key]);
|
||||||
.Append('>')
|
builder.Append("</" + key + ">");
|
||||||
.Append(stateVariables[key])
|
builder.Append("</e:property>");
|
||||||
.Append("</")
|
|
||||||
.Append(key)
|
|
||||||
.Append('>')
|
|
||||||
.Append("</e:property>");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Append("</e:propertyset>");
|
builder.Append("</e:propertyset>");
|
||||||
|
|
||||||
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
|
var options = new HttpRequestOptions
|
||||||
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
|
{
|
||||||
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
|
RequestContent = builder.ToString(),
|
||||||
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
|
RequestContentType = "text/xml",
|
||||||
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
|
Url = subscription.CallbackUrl,
|
||||||
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
|
BufferContent = false
|
||||||
|
};
|
||||||
|
|
||||||
|
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 var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
|
||||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -7,13 +8,10 @@ namespace Emby.Dlna.Eventing
|
|||||||
public class EventSubscription
|
public class EventSubscription
|
||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
|
|
||||||
public string CallbackUrl { get; set; }
|
public string CallbackUrl { get; set; }
|
||||||
|
|
||||||
public string NotificationType { get; set; }
|
public string NotificationType { get; set; }
|
||||||
|
|
||||||
public DateTime SubscriptionTime { get; set; }
|
public DateTime SubscriptionTime { get; set; }
|
||||||
|
|
||||||
public int TimeoutSeconds { get; set; }
|
public int TimeoutSeconds { get; set; }
|
||||||
|
|
||||||
public long TriggerCount { get; set; }
|
public long TriggerCount { get; set; }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
namespace Emby.Dlna
|
namespace Emby.Dlna
|
||||||
{
|
{
|
||||||
public interface IConnectionManager : IDlnaEventManager, IUpnpService
|
public interface IConnectionManager : IEventManager, IUpnpService
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
namespace Emby.Dlna
|
namespace Emby.Dlna
|
||||||
{
|
{
|
||||||
public interface IContentDirectory : IDlnaEventManager, IUpnpService
|
public interface IContentDirectory : IEventManager, IUpnpService
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,24 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
namespace Emby.Dlna
|
namespace Emby.Dlna
|
||||||
{
|
{
|
||||||
public interface IDlnaEventManager
|
public interface IEventManager
|
||||||
{
|
{
|
||||||
/// <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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
namespace Emby.Dlna
|
namespace Emby.Dlna
|
||||||
{
|
{
|
||||||
public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
|
public interface IMediaReceiverRegistrar : IEventManager, IUpnpService
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 278 B After Width: | Height: | Size: 286 B |
@@ -1,16 +1,13 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna.PlayTo;
|
using Emby.Dlna.PlayTo;
|
||||||
using Emby.Dlna.Ssdp;
|
using Emby.Dlna.Ssdp;
|
||||||
using Jellyfin.Networking.Configuration;
|
|
||||||
using Jellyfin.Networking.Manager;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
@@ -34,13 +31,15 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
|||||||
|
|
||||||
namespace Emby.Dlna.Main
|
namespace Emby.Dlna.Main
|
||||||
{
|
{
|
||||||
public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
|
public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
|
||||||
{
|
{
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly ILogger<DlnaEntryPoint> _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
|
|
||||||
|
private PlayToManager _manager;
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IDlnaManager _dlnaManager;
|
private readonly IDlnaManager _dlnaManager;
|
||||||
@@ -49,25 +48,27 @@ 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 readonly NetworkConfiguration _netConfig;
|
|
||||||
private readonly bool _disabled;
|
|
||||||
|
|
||||||
private PlayToManager _manager;
|
|
||||||
private SsdpDevicePublisher _publisher;
|
|
||||||
private ISsdpCommunicationsServer _communicationsServer;
|
private ISsdpCommunicationsServer _communicationsServer;
|
||||||
|
|
||||||
private bool _disposed;
|
internal IContentDirectory ContentDirectory { get; private set; }
|
||||||
|
internal IConnectionManager ConnectionManager { get; private set; }
|
||||||
|
internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
|
||||||
|
|
||||||
public DlnaEntryPoint(
|
public static DlnaEntryPoint Current;
|
||||||
IServerConfigurationManager config,
|
|
||||||
|
public DlnaEntryPoint(IServerConfigurationManager config,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
ISessionManager sessionManager,
|
ISessionManager sessionManager,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClient httpClient,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
IDlnaManager dlnaManager,
|
IDlnaManager dlnaManager,
|
||||||
@@ -85,7 +86,7 @@ namespace Emby.Dlna.Main
|
|||||||
_config = config;
|
_config = config;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClient = httpClient;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_dlnaManager = dlnaManager;
|
_dlnaManager = dlnaManager;
|
||||||
@@ -97,72 +98,39 @@ namespace Emby.Dlna.Main
|
|||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_socketFactory = socketFactory;
|
_socketFactory = socketFactory;
|
||||||
_networkManager = networkManager;
|
_networkManager = networkManager;
|
||||||
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
|
_logger = loggerFactory.CreateLogger("Dlna");
|
||||||
|
|
||||||
ContentDirectory = new ContentDirectory.ContentDirectoryService(
|
ContentDirectory = new ContentDirectory.ContentDirectory(
|
||||||
dlnaManager,
|
dlnaManager,
|
||||||
userDataManager,
|
userDataManager,
|
||||||
imageProcessor,
|
imageProcessor,
|
||||||
libraryManager,
|
libraryManager,
|
||||||
config,
|
config,
|
||||||
userManager,
|
userManager,
|
||||||
loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
|
_logger,
|
||||||
httpClientFactory,
|
httpClient,
|
||||||
localizationManager,
|
localizationManager,
|
||||||
mediaSourceManager,
|
mediaSourceManager,
|
||||||
userViewManager,
|
userViewManager,
|
||||||
mediaEncoder,
|
mediaEncoder,
|
||||||
tvSeriesManager);
|
tvSeriesManager);
|
||||||
|
|
||||||
ConnectionManager = new ConnectionManager.ConnectionManagerService(
|
ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient);
|
||||||
dlnaManager,
|
|
||||||
config,
|
|
||||||
loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
|
|
||||||
httpClientFactory);
|
|
||||||
|
|
||||||
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
|
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config);
|
||||||
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
|
|
||||||
httpClientFactory,
|
|
||||||
config);
|
|
||||||
Current = this;
|
Current = this;
|
||||||
|
|
||||||
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
|
||||||
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
|
|
||||||
if (_disabled)
|
|
||||||
{
|
|
||||||
_logger.LogError("The DLNA specification does not support HTTPS.");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static DlnaEntryPoint Current { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether the dlna server is enabled.
|
|
||||||
/// </summary>
|
|
||||||
public static bool Enabled { get; private set; }
|
|
||||||
|
|
||||||
public IContentDirectory ContentDirectory { get; private set; }
|
|
||||||
|
|
||||||
public IConnectionManager ConnectionManager { get; private set; }
|
|
||||||
|
|
||||||
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
|
|
||||||
|
|
||||||
public async Task RunAsync()
|
public async Task RunAsync()
|
||||||
{
|
{
|
||||||
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (_disabled)
|
|
||||||
{
|
|
||||||
// No use starting as dlna won't work, as we're running purely on HTTPS.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReloadComponents();
|
ReloadComponents();
|
||||||
|
|
||||||
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
|
void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
|
||||||
{
|
{
|
||||||
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -170,16 +138,15 @@ namespace Emby.Dlna.Main
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReloadComponents()
|
private async void ReloadComponents()
|
||||||
{
|
{
|
||||||
var options = _config.GetDlnaConfiguration();
|
var options = _config.GetDlnaConfiguration();
|
||||||
Enabled = options.EnableServer;
|
|
||||||
|
|
||||||
StartSsdpHandler();
|
StartSsdpHandler();
|
||||||
|
|
||||||
if (options.EnableServer)
|
if (options.EnableServer)
|
||||||
{
|
{
|
||||||
StartDevicePublisher(options);
|
await StartDevicePublisher(options).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -205,7 +172,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(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
_communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||||
{
|
{
|
||||||
IsShared = true
|
IsShared = true
|
||||||
};
|
};
|
||||||
@@ -249,29 +216,27 @@ namespace Emby.Dlna.Main
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StartDevicePublisher(Configuration.DlnaOptions options)
|
public async Task StartDevicePublisher(Configuration.DlnaOptions options)
|
||||||
{
|
{
|
||||||
if (!options.BlastAliveMessages)
|
if (!options.BlastAliveMessages)
|
||||||
{
|
{
|
||||||
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;
|
||||||
LogFunction = LogMessage,
|
_Publisher.SupportPnpRootDevice = false;
|
||||||
SupportPnpRootDevice = false
|
|
||||||
};
|
|
||||||
|
|
||||||
RegisterServerEndpoints();
|
await RegisterServerEndpoints().ConfigureAwait(false);
|
||||||
|
|
||||||
_publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
|
_Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -279,32 +244,17 @@ namespace Emby.Dlna.Main
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterServerEndpoints()
|
private async Task RegisterServerEndpoints()
|
||||||
{
|
{
|
||||||
|
var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
var udn = CreateUuid(_appHost.SystemId);
|
var udn = CreateUuid(_appHost.SystemId);
|
||||||
var descriptorUri = "/dlna/" + udn + "/description.xml";
|
|
||||||
|
|
||||||
var bindAddresses = NetworkManager.CreateCollection(
|
foreach (var address in addresses)
|
||||||
_networkManager.GetInternalBindAddresses()
|
|
||||||
.Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)));
|
|
||||||
|
|
||||||
if (bindAddresses.Count == 0)
|
|
||||||
{
|
|
||||||
// No interfaces returned, so use loopback.
|
|
||||||
bindAddresses = _networkManager.GetLoopbacks();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (IPNetAddress address in bindAddresses)
|
|
||||||
{
|
{
|
||||||
if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
{
|
{
|
||||||
// Not supporting IPv6 right now
|
// Not support IPv6 right now
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit to LAN addresses only
|
|
||||||
if (!_networkManager.IsInLocalNetwork(address))
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,17 +262,15 @@ namespace Emby.Dlna.Main
|
|||||||
|
|
||||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
||||||
|
|
||||||
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
|
var descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||||
// DLNA will only work over http, so we must reset to http:// : {port}
|
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
|
||||||
uri.Scheme = "http://";
|
|
||||||
uri.Port = _netConfig.HttpServerPortNumber;
|
|
||||||
|
|
||||||
var device = new SsdpRootDevice
|
var device = new SsdpRootDevice
|
||||||
{
|
{
|
||||||
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
|
CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
|
||||||
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
|
Location = uri, // Must point to the URL that serves your devices UPnP description document.
|
||||||
Address = address.Address,
|
Address = address,
|
||||||
PrefixLength = address.PrefixLength,
|
SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
|
||||||
FriendlyName = "Jellyfin",
|
FriendlyName = "Jellyfin",
|
||||||
Manufacturer = "Jellyfin",
|
Manufacturer = "Jellyfin",
|
||||||
ModelName = "Jellyfin Server",
|
ModelName = "Jellyfin Server",
|
||||||
@@ -331,13 +279,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)
|
||||||
@@ -363,13 +311,12 @@ namespace Emby.Dlna.Main
|
|||||||
{
|
{
|
||||||
guid = text.GetMD5();
|
guid = text.GetMD5();
|
||||||
}
|
}
|
||||||
|
|
||||||
return guid.ToString("N", CultureInfo.InvariantCulture);
|
return guid.ToString("N", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetProperies(SsdpDevice device, string fullDeviceType)
|
private void SetProperies(SsdpDevice device, string fullDeviceType)
|
||||||
{
|
{
|
||||||
var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
|
var service = fullDeviceType.Replace("urn:", string.Empty).Replace(":1", string.Empty);
|
||||||
|
|
||||||
var serviceParts = service.Split(':');
|
var serviceParts = service.Split(':');
|
||||||
|
|
||||||
@@ -380,6 +327,7 @@ 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)
|
||||||
@@ -391,8 +339,7 @@ namespace Emby.Dlna.Main
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_manager = new PlayToManager(
|
_manager = new PlayToManager(_logger,
|
||||||
_logger,
|
|
||||||
_sessionManager,
|
_sessionManager,
|
||||||
_libraryManager,
|
_libraryManager,
|
||||||
_userManager,
|
_userManager,
|
||||||
@@ -400,7 +347,7 @@ namespace Emby.Dlna.Main
|
|||||||
_appHost,
|
_appHost,
|
||||||
_imageProcessor,
|
_imageProcessor,
|
||||||
_deviceDiscovery,
|
_deviceDiscovery,
|
||||||
_httpClientFactory,
|
_httpClient,
|
||||||
_config,
|
_config,
|
||||||
_userDataManager,
|
_userDataManager,
|
||||||
_localization,
|
_localization,
|
||||||
@@ -431,30 +378,13 @@ namespace Emby.Dlna.Main
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error disposing PlayTo manager");
|
_logger.LogError(ex, "Error disposing PlayTo manager");
|
||||||
}
|
}
|
||||||
|
|
||||||
_manager = null;
|
_manager = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -470,8 +400,16 @@ namespace Emby.Dlna.Main
|
|||||||
ConnectionManager = null;
|
ConnectionManager = null;
|
||||||
MediaReceiverRegistrar = null;
|
MediaReceiverRegistrar = null;
|
||||||
Current = null;
|
Current = null;
|
||||||
|
}
|
||||||
|
|
||||||
_disposed = true;
|
public void DisposeDevicePublisher()
|
||||||
|
{
|
||||||
|
if (_Publisher != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Disposing SsdpDevicePublisher");
|
||||||
|
_Publisher.Dispose();
|
||||||
|
_Publisher = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
@@ -8,16 +11,8 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Defines the <see cref="ControlHandler" />.
|
|
||||||
/// </summary>
|
|
||||||
public class ControlHandler : BaseControlHandler
|
public class ControlHandler : BaseControlHandler
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
|
||||||
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
|
||||||
public ControlHandler(IServerConfigurationManager config, ILogger logger)
|
public ControlHandler(IServerConfigurationManager config, ILogger logger)
|
||||||
: base(config, logger)
|
: base(config, logger)
|
||||||
{
|
{
|
||||||
@@ -41,17 +36,9 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||||||
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Records that the handle is authorized in the xml stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
|
||||||
private static void HandleIsAuthorized(XmlWriter xmlWriter)
|
private static void HandleIsAuthorized(XmlWriter xmlWriter)
|
||||||
=> xmlWriter.WriteElementString("Result", "1");
|
=> xmlWriter.WriteElementString("Result", "1");
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Records that the handle is validated in the xml stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
|
||||||
private static void HandleIsValidated(XmlWriter xmlWriter)
|
private static void HandleIsValidated(XmlWriter xmlWriter)
|
||||||
=> xmlWriter.WriteElementString("Result", "1");
|
=> xmlWriter.WriteElementString("Result", "1");
|
||||||
}
|
}
|
||||||
|
|||||||
37
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs
Normal file
37
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Emby.Dlna.Service;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||||
|
{
|
||||||
|
public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar
|
||||||
|
{
|
||||||
|
private readonly IServerConfigurationManager _config;
|
||||||
|
|
||||||
|
public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config)
|
||||||
|
: base(logger, httpClient)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetServiceXml()
|
||||||
|
{
|
||||||
|
return new MediaReceiverRegistrarXmlBuilder().GetXml();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||||
|
{
|
||||||
|
return new ControlHandler(
|
||||||
|
_config,
|
||||||
|
Logger)
|
||||||
|
.ProcessControlRequestAsync(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using System.Net.Http;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Emby.Dlna.Service;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the <see cref="MediaReceiverRegistrarService" />.
|
|
||||||
/// </summary>
|
|
||||||
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
|
|
||||||
{
|
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
|
|
||||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
|
|
||||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
|
|
||||||
public MediaReceiverRegistrarService(
|
|
||||||
ILogger<MediaReceiverRegistrarService> logger,
|
|
||||||
IHttpClientFactory httpClientFactory,
|
|
||||||
IServerConfigurationManager config)
|
|
||||||
: base(logger, httpClientFactory)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string GetServiceXml()
|
|
||||||
{
|
|
||||||
return MediaReceiverRegistrarXmlBuilder.GetXml();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
|
||||||
{
|
|
||||||
return new ControlHandler(
|
|
||||||
_config,
|
|
||||||
Logger)
|
|
||||||
.ProcessControlRequestAsync(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +1,79 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
using Emby.Dlna.Service;
|
using Emby.Dlna.Service;
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class MediaReceiverRegistrarXmlBuilder
|
||||||
/// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />.
|
|
||||||
/// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
|
|
||||||
/// </summary>
|
|
||||||
public static class MediaReceiverRegistrarXmlBuilder
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
public string GetXml()
|
||||||
/// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An XML representation of this service.</returns>
|
|
||||||
public static string GetXml()
|
|
||||||
{
|
{
|
||||||
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
|
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
|
||||||
|
GetStateVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The a list of all the state variables for this invocation.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
|
||||||
private static IEnumerable<StateVariable> GetStateVariables()
|
private static IEnumerable<StateVariable> GetStateVariables()
|
||||||
{
|
{
|
||||||
var list = new List<StateVariable>
|
var list = new List<StateVariable>();
|
||||||
{
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "AuthorizationGrantedUpdateID",
|
Name = "AuthorizationGrantedUpdateID",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = true
|
SendsEvents = true
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_DeviceID",
|
Name = "A_ARG_TYPE_DeviceID",
|
||||||
DataType = "string",
|
DataType = "string",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "AuthorizationDeniedUpdateID",
|
Name = "AuthorizationDeniedUpdateID",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = true
|
SendsEvents = true
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "ValidationSucceededUpdateID",
|
Name = "ValidationSucceededUpdateID",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = true
|
SendsEvents = true
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_RegistrationRespMsg",
|
Name = "A_ARG_TYPE_RegistrationRespMsg",
|
||||||
DataType = "bin.base64",
|
DataType = "bin.base64",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_RegistrationReqMsg",
|
Name = "A_ARG_TYPE_RegistrationReqMsg",
|
||||||
DataType = "bin.base64",
|
DataType = "bin.base64",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "ValidationRevokedUpdateID",
|
Name = "ValidationRevokedUpdateID",
|
||||||
DataType = "ui4",
|
DataType = "ui4",
|
||||||
SendsEvents = true
|
SendsEvents = true
|
||||||
},
|
});
|
||||||
|
|
||||||
new StateVariable
|
list.Add(new StateVariable
|
||||||
{
|
{
|
||||||
Name = "A_ARG_TYPE_Result",
|
Name = "A_ARG_TYPE_Result",
|
||||||
DataType = "int",
|
DataType = "int",
|
||||||
SendsEvents = false
|
SendsEvents = false
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
|
|
||||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class ServiceActionListBuilder
|
||||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
|
||||||
/// </summary>
|
|
||||||
public static class ServiceActionListBuilder
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
public IEnumerable<ServiceAction> GetActions()
|
||||||
/// Returns a list of services that this instance provides.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
|
||||||
public static IEnumerable<ServiceAction> GetActions()
|
|
||||||
{
|
{
|
||||||
return new[]
|
return new[]
|
||||||
{
|
{
|
||||||
@@ -26,10 +22,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "IsValidated".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetIsValidated()
|
private static ServiceAction GetIsValidated()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -52,10 +44,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "IsAuthorized".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetIsAuthorized()
|
private static ServiceAction GetIsAuthorized()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -78,10 +66,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "RegisterDevice".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetRegisterDevice()
|
private static ServiceAction GetRegisterDevice()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -104,10 +88,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the action details for "GetValidationSucceededUpdateID".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetGetValidationSucceededUpdateID()
|
private static ServiceAction GetGetValidationSucceededUpdateID()
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
@@ -124,11 +104,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private ServiceAction GetGetAuthorizationDeniedUpdateID()
|
||||||
/// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetGetAuthorizationDeniedUpdateID()
|
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
{
|
{
|
||||||
@@ -144,11 +120,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private ServiceAction GetGetValidationRevokedUpdateID()
|
||||||
/// Returns the action details for "GetValidationRevokedUpdateID".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetGetValidationRevokedUpdateID()
|
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
{
|
{
|
||||||
@@ -164,11 +136,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private ServiceAction GetGetAuthorizationGrantedUpdateID()
|
||||||
/// Returns the action details for "GetAuthorizationGrantedUpdateID".
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
|
||||||
private static ServiceAction GetGetAuthorizationGrantedUpdateID()
|
|
||||||
{
|
{
|
||||||
var action = new ServiceAction
|
var action = new ServiceAction
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,65 +1,43 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
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.Controller.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Dlna.PlayTo
|
namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
public class Device : IDisposable
|
public class Device : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
#region Fields & Properties
|
||||||
|
|
||||||
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
|
||||||
{
|
{
|
||||||
RefreshVolumeIfNeeded().GetAwaiter().GetResult();
|
RefreshVolumeIfNeeded();
|
||||||
return _volume;
|
return _volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
set => _volume = value;
|
set => _volume = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,21 +45,29 @@ 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.PausedPlayback;
|
public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK;
|
||||||
|
|
||||||
public bool IsStopped => TransportState == TransportState.Stopped;
|
public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IServerConfigurationManager _config;
|
||||||
|
|
||||||
public Action OnDeviceUnavailable { get; set; }
|
public Action OnDeviceUnavailable { get; set; }
|
||||||
|
|
||||||
private TransportCommands AvCommands { get; set; }
|
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
|
||||||
|
{
|
||||||
private TransportCommands RendererCommands { get; set; }
|
Properties = deviceProperties;
|
||||||
|
_httpClient = httpClient;
|
||||||
public UBaseObject CurrentMediaInfo { get; private set; }
|
_logger = logger;
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
@@ -89,25 +75,27 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
|
_timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RefreshVolumeIfNeeded()
|
private DateTime _lastVolumeRefresh;
|
||||||
|
private bool _volumeRefreshActive;
|
||||||
|
private void RefreshVolumeIfNeeded()
|
||||||
{
|
{
|
||||||
if (_volumeRefreshActive
|
if (!_volumeRefreshActive)
|
||||||
&& DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
|
|
||||||
{
|
|
||||||
_lastVolumeRefresh = DateTime.UtcNow;
|
|
||||||
return RefreshVolume();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshVolume(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
|
||||||
|
{
|
||||||
|
_lastVolumeRefresh = DateTime.UtcNow;
|
||||||
|
RefreshVolume(CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void RefreshVolume(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await GetVolume(cancellationToken).ConfigureAwait(false);
|
await GetVolume(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -119,6 +107,7 @@ 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)
|
||||||
@@ -153,6 +142,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Commanding
|
||||||
|
|
||||||
public Task VolumeDown(CancellationToken cancellationToken)
|
public Task VolumeDown(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var sendVolume = Math.Max(Volume - 5, 0);
|
var sendVolume = Math.Max(Volume - 5, 0);
|
||||||
@@ -221,9 +212,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
var service = GetServiceRenderingControl();
|
var service = GetServiceRenderingControl();
|
||||||
|
|
||||||
@@ -235,7 +224,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(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
IsMuted = mute;
|
IsMuted = mute;
|
||||||
@@ -244,20 +233,15 @@ 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);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var service = GetServiceRenderingControl();
|
var service = GetServiceRenderingControl();
|
||||||
|
|
||||||
@@ -270,7 +254,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
// Remote control will perform better
|
// Remote control will perform better
|
||||||
Volume = value;
|
Volume = value;
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,9 +264,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
@@ -291,7 +273,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
throw new InvalidOperationException("Unable to find service");
|
throw new InvalidOperationException("Unable to find service");
|
||||||
}
|
}
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
|
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
@@ -301,20 +283,18 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
url = url.Replace("&", "&", StringComparison.Ordinal);
|
url = url.Replace("&", "&");
|
||||||
|
|
||||||
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
||||||
@@ -325,7 +305,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(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
|
await new SsdpHttpClient(_httpClient).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);
|
||||||
@@ -343,14 +323,14 @@ namespace Emby.Dlna.PlayTo
|
|||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string CreateDidlMeta(string value)
|
private string CreateDidlMeta(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
return SecurityElement.Escape(value);
|
return DescriptionXmlBuilder.Escape(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
|
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||||
@@ -367,12 +347,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
throw new InvalidOperationException("Unable to find service");
|
throw new InvalidOperationException("Unable to find service");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
return new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1));
|
||||||
Properties.BaseUrl,
|
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
avCommands.BuildPost(command, service.ServiceType, 1),
|
|
||||||
cancellationToken: cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetPlay(CancellationToken cancellationToken)
|
public async Task SetPlay(CancellationToken cancellationToken)
|
||||||
@@ -396,7 +371,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
@@ -414,14 +389,19 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
await new SsdpHttpClient(_httpClient).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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Get data
|
||||||
|
|
||||||
|
private int _connectFailureCount;
|
||||||
private async void TimerCallback(object sender)
|
private async void TimerCallback(object sender)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
@@ -450,7 +430,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);
|
||||||
}
|
}
|
||||||
@@ -474,12 +454,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_connectFailureCount = 0;
|
_connectFailureCount = 0;
|
||||||
|
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// If we're not playing anything make sure we don't get data more often than necessary 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();
|
||||||
}
|
}
|
||||||
@@ -496,9 +474,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
|
_logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
|
||||||
|
|
||||||
@@ -514,7 +490,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RestartTimerInactive();
|
RestartTimerInactive();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -541,19 +516,15 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true)
|
||||||
Properties.BaseUrl,
|
.ConfigureAwait(false);
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
rendererCommands.BuildPost(command, service.ServiceType),
|
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (result == null || result.Document == null)
|
if (result == null || result.Document == null)
|
||||||
{
|
{
|
||||||
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))
|
||||||
@@ -591,26 +562,20 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true)
|
||||||
Properties.BaseUrl,
|
.ConfigureAwait(false);
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
rendererCommands.BuildPost(command, service.ServiceType),
|
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (result == null || result.Document == null)
|
if (result == null || result.Document == null)
|
||||||
{
|
|
||||||
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)
|
||||||
@@ -624,12 +589,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), false)
|
||||||
Properties.BaseUrl,
|
.ConfigureAwait(false);
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
avCommands.BuildPost(command, service.ServiceType),
|
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (result == null || result.Document == null)
|
if (result == null || result.Document == null)
|
||||||
{
|
{
|
||||||
@@ -637,12 +598,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 == null ? null : 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;
|
||||||
}
|
}
|
||||||
@@ -650,7 +611,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)
|
||||||
@@ -666,12 +627,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false)
|
||||||
Properties.BaseUrl,
|
.ConfigureAwait(false);
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
rendererCommands.BuildPost(command, service.ServiceType),
|
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (result == null || result.Document == null)
|
if (result == null || result.Document == null)
|
||||||
{
|
{
|
||||||
@@ -685,7 +642,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;
|
||||||
|
|
||||||
@@ -701,13 +658,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
|
||||||
};
|
};
|
||||||
@@ -716,7 +673,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)
|
||||||
@@ -733,23 +690,19 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false)
|
||||||
Properties.BaseUrl,
|
.ConfigureAwait(false);
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
rendererCommands.BuildPost(command, service.ServiceType),
|
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (result == null || result.Document == null)
|
if (result == null || result.Document == null)
|
||||||
{
|
{
|
||||||
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?.Value;
|
var trackUri = trackUriElem == null ? null : 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?.Value;
|
var duration = durationElem == null ? null : durationElem.Value;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(duration)
|
if (!string.IsNullOrWhiteSpace(duration)
|
||||||
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -761,8 +714,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?.Value;
|
var position = positionElem == null ? null : positionElem.Value;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -773,7 +726,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
if (track == null)
|
if (track == null)
|
||||||
{
|
{
|
||||||
// If track is null, some vendors do this, use GetMediaInfo instead.
|
//If track is null, some vendors do this, use GetMediaInfo instead
|
||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,7 +754,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);
|
||||||
|
|
||||||
@@ -810,16 +763,17 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
private XElement ParseResponse(string xml)
|
private XElement ParseResponse(string xml)
|
||||||
{
|
{
|
||||||
// Handle different variations sent back by devices.
|
// Handle different variations sent back by devices
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return XElement.Parse(xml);
|
return XElement.Parse(xml);
|
||||||
}
|
}
|
||||||
catch (XmlException)
|
catch (XmlException)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// first try to add a root node with a dlna namespace.
|
// first try to add a root node with a dlna namesapce
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>")
|
return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>")
|
||||||
@@ -828,41 +782,43 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
catch (XmlException)
|
catch (XmlException)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// some devices send back invalid xml
|
// some devices send back invalid xml
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return XElement.Parse(xml.Replace("&", "&", StringComparison.Ordinal));
|
return XElement.Parse(xml.Replace("&", "&"));
|
||||||
}
|
}
|
||||||
catch (XmlException)
|
catch (XmlException)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = string.Empty,
|
SecondText = "",
|
||||||
Url = url,
|
Url = url,
|
||||||
ProtocolInfo = GetProtocolInfo(container),
|
ProtocolInfo = GetProtocolInfo(container),
|
||||||
MetaData = container.ToString()
|
MetaData = container.ToString()
|
||||||
@@ -876,11 +832,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))
|
||||||
{
|
{
|
||||||
@@ -891,6 +847,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return new string[4];
|
return new string[4];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region From XML
|
||||||
|
|
||||||
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
|
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (AvCommands != null)
|
if (AvCommands != null)
|
||||||
@@ -911,7 +871,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
|
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
|
||||||
|
|
||||||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
var httpClient = new SsdpHttpClient(_httpClient);
|
||||||
|
|
||||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -939,7 +899,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
|
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
|
||||||
|
|
||||||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
var httpClient = new SsdpHttpClient(_httpClient);
|
||||||
_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);
|
||||||
|
|
||||||
@@ -955,12 +915,12 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.Contains('/', StringComparison.Ordinal))
|
if (!url.Contains("/"))
|
||||||
{
|
{
|
||||||
url = "/dmr/" + url;
|
url = "/dmr/" + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.StartsWith('/'))
|
if (!url.StartsWith("/"))
|
||||||
{
|
{
|
||||||
url = "/" + url;
|
url = "/" + url;
|
||||||
}
|
}
|
||||||
@@ -968,21 +928,25 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return baseUrl + url;
|
return baseUrl + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
|
private TransportCommands AvCommands { get; set; }
|
||||||
|
|
||||||
|
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(httpClientFactory);
|
var ssdpHttpClient = new SsdpHttpClient(httpClient);
|
||||||
|
|
||||||
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);
|
||||||
@@ -991,77 +955,77 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var deviceProperties = new DeviceInfo()
|
var deviceProperties = new DeviceInfo()
|
||||||
{
|
{
|
||||||
Name = string.Join(" ", friendlyNames),
|
Name = string.Join(" ", friendlyNames),
|
||||||
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
|
BaseUrl = string.Format("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;
|
||||||
@@ -1078,9 +1042,12 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Device(deviceProperties, httpClientFactory, logger);
|
return new Device(deviceProperties, httpClient, logger, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -1088,11 +1055,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);
|
||||||
@@ -1109,11 +1076,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
|
||||||
{
|
{
|
||||||
@@ -1125,7 +1092,14 @@ namespace Emby.Dlna.PlayTo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
|
public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -1134,7 +1108,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);
|
||||||
}
|
}
|
||||||
@@ -1153,7 +1127,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))
|
||||||
{
|
{
|
||||||
@@ -1166,7 +1140,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))
|
||||||
{
|
{
|
||||||
@@ -1179,7 +1153,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
|
||||||
{
|
{
|
||||||
@@ -1187,7 +1161,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
|
||||||
{
|
{
|
||||||
@@ -1196,17 +1170,16 @@ namespace Emby.Dlna.PlayTo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
#region IDisposable
|
||||||
|
|
||||||
|
bool _disposed;
|
||||||
|
|
||||||
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)
|
||||||
@@ -1225,10 +1198,11 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
#endregion
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
|
return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
@@ -8,9 +9,6 @@ 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";
|
||||||
@@ -36,6 +34,7 @@ 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;
|
||||||
@@ -44,6 +43,7 @@ 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()
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.PlayTo
|
|
||||||
{
|
|
||||||
public class MediaChangedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
public UBaseObject OldMediaInfo { get; set; }
|
|
||||||
|
|
||||||
public UBaseObject NewMediaInfo { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -7,8 +8,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna.Didl;
|
using Emby.Dlna.Didl;
|
||||||
using Jellyfin.Data.Entities;
|
using MediaBrowser.Common.Configuration;
|
||||||
using Jellyfin.Data.Events;
|
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -18,18 +18,17 @@ 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;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Photo = MediaBrowser.Controller.Entities.Photo;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.PlayTo
|
namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
public class PlayToController : ISessionController, IDisposable
|
public class PlayToController : ISessionController, IDisposable
|
||||||
{
|
{
|
||||||
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;
|
||||||
@@ -40,17 +39,16 @@ namespace Emby.Dlna.PlayTo
|
|||||||
private readonly IUserDataManager _userDataManager;
|
private readonly IUserDataManager _userDataManager;
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
|
private readonly IConfigurationManager _config;
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
|
|
||||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||||
private readonly string _serverAddress;
|
private readonly string _serverAddress;
|
||||||
private readonly string _accessToken;
|
private readonly string _accessToken;
|
||||||
|
|
||||||
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
|
public bool IsSessionActive => !_disposed && _device != null;
|
||||||
private Device _device;
|
|
||||||
private int _currentPlaylistIndex;
|
|
||||||
|
|
||||||
private bool _disposed;
|
public bool SupportsMediaControl => IsSessionActive;
|
||||||
|
|
||||||
public PlayToController(
|
public PlayToController(
|
||||||
SessionInfo session,
|
SessionInfo session,
|
||||||
@@ -66,6 +64,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
IUserDataManager userDataManager,
|
IUserDataManager userDataManager,
|
||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
|
IConfigurationManager config,
|
||||||
IMediaEncoder mediaEncoder)
|
IMediaEncoder mediaEncoder)
|
||||||
{
|
{
|
||||||
_session = session;
|
_session = session;
|
||||||
@@ -81,25 +80,22 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_userDataManager = userDataManager;
|
_userDataManager = userDataManager;
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
|
_config = config;
|
||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSessionActive => !_disposed && _device != null;
|
|
||||||
|
|
||||||
public bool SupportsMediaControl => IsSessionActive;
|
|
||||||
|
|
||||||
public void Init(Device device)
|
public void Init(Device device)
|
||||||
{
|
{
|
||||||
_device = device;
|
_device = device;
|
||||||
_device.OnDeviceUnavailable = OnDeviceUnavailable;
|
_device.OnDeviceUnavailable = OnDeviceUnavailable;
|
||||||
_device.PlaybackStart += OnDevicePlaybackStart;
|
_device.PlaybackStart += _device_PlaybackStart;
|
||||||
_device.PlaybackProgress += OnDevicePlaybackProgress;
|
_device.PlaybackProgress += _device_PlaybackProgress;
|
||||||
_device.PlaybackStopped += OnDevicePlaybackStopped;
|
_device.PlaybackStopped += _device_PlaybackStopped;
|
||||||
_device.MediaChanged += OnDeviceMediaChanged;
|
_device.MediaChanged += _device_MediaChanged;
|
||||||
|
|
||||||
_device.Start();
|
_device.Start();
|
||||||
|
|
||||||
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
_deviceDiscovery.DeviceLeft += _deviceDiscovery_DeviceLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDeviceUnavailable()
|
private void OnDeviceUnavailable()
|
||||||
@@ -115,7 +111,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
void _deviceDiscovery_DeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||||
{
|
{
|
||||||
var info = e.Argument;
|
var info = e.Argument;
|
||||||
|
|
||||||
@@ -130,7 +126,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
async void _device_MediaChanged(object sender, MediaChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
@@ -142,18 +138,15 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager);
|
var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||||
if (streamInfo.Item != null)
|
if (streamInfo.Item != null)
|
||||||
{
|
{
|
||||||
var positionTicks = GetProgressPositionTicks(streamInfo);
|
var positionTicks = GetProgressPositionTicks(e.OldMediaInfo, streamInfo);
|
||||||
|
|
||||||
await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
|
ReportPlaybackStopped(e.OldMediaInfo, streamInfo, positionTicks);
|
||||||
}
|
}
|
||||||
|
|
||||||
streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
|
streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||||
if (streamInfo.Item == null)
|
if (streamInfo.Item == null) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newItemProgress = GetProgressInfo(streamInfo);
|
var newItemProgress = GetProgressInfo(e.NewMediaInfo, streamInfo);
|
||||||
|
|
||||||
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -163,7 +156,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e)
|
async void _device_PlaybackStopped(object sender, PlaybackStoppedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
@@ -174,14 +167,11 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
|
var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||||
|
|
||||||
if (streamInfo.Item == null)
|
if (streamInfo.Item == null) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var positionTicks = GetProgressPositionTicks(streamInfo);
|
var positionTicks = GetProgressPositionTicks(e.MediaInfo, streamInfo);
|
||||||
|
|
||||||
await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
|
ReportPlaybackStopped(e.MediaInfo, streamInfo, positionTicks);
|
||||||
|
|
||||||
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
|
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -189,7 +179,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
(_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
|
(_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
|
||||||
mediaSource.RunTimeTicks;
|
mediaSource.RunTimeTicks;
|
||||||
|
|
||||||
var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
|
var playedToCompletion = (positionTicks.HasValue && positionTicks.Value == 0);
|
||||||
|
|
||||||
if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
|
if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
|
||||||
{
|
{
|
||||||
@@ -205,7 +195,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_playlist.Clear();
|
Playlist.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -214,7 +204,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
|
private async void ReportPlaybackStopped(uBaseObject mediaInfo, StreamParams streamInfo, long? positionTicks)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -224,6 +214,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
SessionId = _session.Id,
|
SessionId = _session.Id,
|
||||||
PositionTicks = positionTicks,
|
PositionTicks = positionTicks,
|
||||||
MediaSourceId = streamInfo.MediaSourceId
|
MediaSourceId = streamInfo.MediaSourceId
|
||||||
|
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -232,7 +223,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e)
|
async void _device_PlaybackStart(object sender, PlaybackStartEventArgs e)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
@@ -245,7 +236,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
if (info.Item != null)
|
if (info.Item != null)
|
||||||
{
|
{
|
||||||
var progress = GetProgressInfo(info);
|
var progress = GetProgressInfo(e.MediaInfo, info);
|
||||||
|
|
||||||
await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false);
|
await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -256,7 +247,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e)
|
async void _device_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
@@ -276,7 +267,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
if (info.Item != null)
|
if (info.Item != null)
|
||||||
{
|
{
|
||||||
var progress = GetProgressInfo(info);
|
var progress = GetProgressInfo(e.MediaInfo, info);
|
||||||
|
|
||||||
await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false);
|
await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -287,7 +278,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private long? GetProgressPositionTicks(StreamParams info)
|
private long? GetProgressPositionTicks(uBaseObject mediaInfo, StreamParams info)
|
||||||
{
|
{
|
||||||
var ticks = _device.Position.Ticks;
|
var ticks = _device.Position.Ticks;
|
||||||
|
|
||||||
@@ -299,13 +290,13 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return ticks;
|
return ticks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlaybackStartInfo GetProgressInfo(StreamParams info)
|
private PlaybackStartInfo GetProgressInfo(uBaseObject mediaInfo, StreamParams info)
|
||||||
{
|
{
|
||||||
return new PlaybackStartInfo
|
return new PlaybackStartInfo
|
||||||
{
|
{
|
||||||
ItemId = info.ItemId,
|
ItemId = info.ItemId,
|
||||||
SessionId = _session.Id,
|
SessionId = _session.Id,
|
||||||
PositionTicks = GetProgressPositionTicks(info),
|
PositionTicks = GetProgressPositionTicks(mediaInfo, info),
|
||||||
IsMuted = _device.IsMuted,
|
IsMuted = _device.IsMuted,
|
||||||
IsPaused = _device.IsPaused,
|
IsPaused = _device.IsPaused,
|
||||||
MediaSourceId = info.MediaSourceId,
|
MediaSourceId = info.MediaSourceId,
|
||||||
@@ -320,9 +311,11 @@ namespace Emby.Dlna.PlayTo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
|
#region SendCommands
|
||||||
|
|
||||||
|
public async Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
|
_logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
|
||||||
|
|
||||||
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
|
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
|
||||||
|
|
||||||
@@ -333,52 +326,45 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
var startIndex = command.StartIndex ?? 0;
|
var startIndex = command.StartIndex ?? 0;
|
||||||
int len = items.Count - startIndex;
|
|
||||||
if (startIndex > 0)
|
if (startIndex > 0)
|
||||||
{
|
{
|
||||||
items = items.GetRange(startIndex, len);
|
items = items.Skip(startIndex).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
var playlist = new PlaylistItem[len];
|
var playlist = new List<PlaylistItem>();
|
||||||
|
var isFirst = true;
|
||||||
|
|
||||||
// Not nullable enabled - so this is required.
|
foreach (var item in items)
|
||||||
playlist[0] = CreatePlaylistItem(
|
|
||||||
items[0],
|
|
||||||
user,
|
|
||||||
command.StartPositionTicks ?? 0,
|
|
||||||
command.MediaSourceId ?? string.Empty,
|
|
||||||
command.AudioStreamIndex,
|
|
||||||
command.SubtitleStreamIndex);
|
|
||||||
|
|
||||||
for (int i = 1; i < len; i++)
|
|
||||||
{
|
{
|
||||||
playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
|
if (isFirst && command.StartPositionTicks.HasValue)
|
||||||
|
{
|
||||||
|
playlist.Add(CreatePlaylistItem(item, user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex));
|
||||||
|
isFirst = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
playlist.Add(CreatePlaylistItem(item, user, 0, null, null, null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
|
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
|
||||||
|
|
||||||
if (command.PlayCommand == PlayCommand.PlayLast)
|
if (command.PlayCommand == PlayCommand.PlayLast)
|
||||||
{
|
{
|
||||||
_playlist.AddRange(playlist);
|
Playlist.AddRange(playlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.PlayCommand == PlayCommand.PlayNext)
|
if (command.PlayCommand == PlayCommand.PlayNext)
|
||||||
{
|
{
|
||||||
_playlist.AddRange(playlist);
|
Playlist.AddRange(playlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!command.ControllingUserId.Equals(Guid.Empty))
|
if (!command.ControllingUserId.Equals(Guid.Empty))
|
||||||
{
|
{
|
||||||
_sessionManager.LogSessionActivity(
|
_sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId,
|
||||||
_session.Client,
|
_session.DeviceName, _session.RemoteEndPoint, user);
|
||||||
_session.ApplicationVersion,
|
|
||||||
_session.DeviceId,
|
|
||||||
_session.DeviceName,
|
|
||||||
_session.RemoteEndPoint,
|
|
||||||
user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return PlayItems(playlist, cancellationToken);
|
await PlayItems(playlist).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
|
private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
|
||||||
@@ -386,7 +372,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
switch (command.Command)
|
switch (command.Command)
|
||||||
{
|
{
|
||||||
case PlaystateCommand.Stop:
|
case PlaystateCommand.Stop:
|
||||||
_playlist.Clear();
|
Playlist.Clear();
|
||||||
return _device.SetStop(CancellationToken.None);
|
return _device.SetStop(CancellationToken.None);
|
||||||
|
|
||||||
case PlaystateCommand.Pause:
|
case PlaystateCommand.Pause:
|
||||||
@@ -402,10 +388,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return Seek(command.SeekPositionTicks ?? 0);
|
return Seek(command.SeekPositionTicks ?? 0);
|
||||||
|
|
||||||
case PlaystateCommand.NextTrack:
|
case PlaystateCommand.NextTrack:
|
||||||
return SetPlaylistIndex(_currentPlaylistIndex + 1, cancellationToken);
|
return SetPlaylistIndex(_currentPlaylistIndex + 1);
|
||||||
|
|
||||||
case PlaystateCommand.PreviousTrack:
|
case PlaystateCommand.PreviousTrack:
|
||||||
return SetPlaylistIndex(_currentPlaylistIndex - 1, cancellationToken);
|
return SetPlaylistIndex(_currentPlaylistIndex - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -427,7 +413,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,6 +427,14 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return info.IsDirectStream;
|
return info.IsDirectStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Playlist
|
||||||
|
|
||||||
|
private int _currentPlaylistIndex;
|
||||||
|
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
|
||||||
|
private List<PlaylistItem> Playlist => _playlist;
|
||||||
|
|
||||||
private void AddItemFromId(Guid id, List<BaseItem> list)
|
private void AddItemFromId(Guid id, List<BaseItem> list)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(id);
|
var item = _libraryManager.GetItemById(id);
|
||||||
@@ -451,13 +444,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlaylistItem CreatePlaylistItem(
|
private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
||||||
BaseItem item,
|
|
||||||
User user,
|
|
||||||
long startPostionTicks,
|
|
||||||
string mediaSourceId,
|
|
||||||
int? audioStreamIndex,
|
|
||||||
int? subtitleStreamIndex)
|
|
||||||
{
|
{
|
||||||
var deviceInfo = _device.Properties;
|
var deviceInfo = _device.Properties;
|
||||||
|
|
||||||
@@ -465,27 +452,16 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_dlnaManager.GetDefaultProfile();
|
_dlnaManager.GetDefaultProfile();
|
||||||
|
|
||||||
var mediaSources = item is IHasMediaSources
|
var mediaSources = item is IHasMediaSources
|
||||||
? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray()
|
? (_mediaSourceManager.GetStaticMediaSources(item, true, user))
|
||||||
: Array.Empty<MediaSourceInfo>();
|
: new List<MediaSourceInfo>();
|
||||||
|
|
||||||
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
|
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
|
||||||
playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
|
playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
|
||||||
|
|
||||||
playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken));
|
playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken));
|
||||||
|
|
||||||
var itemXml = new DidlBuilder(
|
var itemXml = new DidlBuilder(profile, user, _imageProcessor, _serverAddress, _accessToken, _userDataManager, _localization, _mediaSourceManager, _logger, _mediaEncoder)
|
||||||
profile,
|
.GetItemDidl(_config.GetDlnaConfiguration(), item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo);
|
||||||
user,
|
|
||||||
_imageProcessor,
|
|
||||||
_serverAddress,
|
|
||||||
_accessToken,
|
|
||||||
_userDataManager,
|
|
||||||
_localization,
|
|
||||||
_mediaSourceManager,
|
|
||||||
_logger,
|
|
||||||
_mediaEncoder,
|
|
||||||
_libraryManager)
|
|
||||||
.GetItemDidl(item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo);
|
|
||||||
|
|
||||||
playlistItem.Didl = itemXml;
|
playlistItem.Didl = itemXml;
|
||||||
|
|
||||||
@@ -500,8 +476,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
||||||
{
|
{
|
||||||
return new ContentFeatureBuilder(profile)
|
return new ContentFeatureBuilder(profile)
|
||||||
.BuildAudioHeader(
|
.BuildAudioHeader(streamInfo.Container,
|
||||||
streamInfo.Container,
|
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioBitrate,
|
streamInfo.TargetAudioBitrate,
|
||||||
streamInfo.TargetAudioSampleRate,
|
streamInfo.TargetAudioSampleRate,
|
||||||
@@ -515,8 +490,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
if (streamInfo.MediaType == DlnaProfileType.Video)
|
if (streamInfo.MediaType == DlnaProfileType.Video)
|
||||||
{
|
{
|
||||||
var list = new ContentFeatureBuilder(profile)
|
var list = new ContentFeatureBuilder(profile)
|
||||||
.BuildVideoHeader(
|
.BuildVideoHeader(streamInfo.Container,
|
||||||
streamInfo.Container,
|
|
||||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetWidth,
|
streamInfo.TargetWidth,
|
||||||
@@ -545,7 +519,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
private PlaylistItem GetPlaylistItem(BaseItem item, List<MediaSourceInfo> mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
||||||
{
|
{
|
||||||
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -554,7 +528,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
|
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
|
||||||
{
|
{
|
||||||
ItemId = item.Id,
|
ItemId = item.Id,
|
||||||
MediaSources = mediaSources,
|
MediaSources = mediaSources.ToArray(),
|
||||||
Profile = profile,
|
Profile = profile,
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
MaxBitrate = profile.MaxStreamingBitrate,
|
MaxBitrate = profile.MaxStreamingBitrate,
|
||||||
@@ -574,7 +548,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
|
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
|
||||||
{
|
{
|
||||||
ItemId = item.Id,
|
ItemId = item.Id,
|
||||||
MediaSources = mediaSources,
|
MediaSources = mediaSources.ToArray(),
|
||||||
Profile = profile,
|
Profile = profile,
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
MaxBitrate = profile.MaxStreamingBitrate,
|
MaxBitrate = profile.MaxStreamingBitrate,
|
||||||
@@ -587,7 +561,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return PlaylistItemFactory.Create((Photo)item, profile);
|
return new PlaylistItemFactory().Create((Photo)item, profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ArgumentException("Unrecognized item type.");
|
throw new ArgumentException("Unrecognized item type.");
|
||||||
@@ -597,31 +571,30 @@ namespace Emby.Dlna.PlayTo
|
|||||||
/// Plays the items.
|
/// Plays the items.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="items">The items.</param>
|
/// <param name="items">The items.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <returns></returns>
|
||||||
/// <returns><c>true</c> on success.</returns>
|
private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items)
|
||||||
private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items, CancellationToken cancellationToken = default)
|
|
||||||
{
|
{
|
||||||
_playlist.Clear();
|
Playlist.Clear();
|
||||||
_playlist.AddRange(items);
|
Playlist.AddRange(items);
|
||||||
_logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, _playlist.Count);
|
_logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, Playlist.Count);
|
||||||
|
|
||||||
await SetPlaylistIndex(0, cancellationToken).ConfigureAwait(false);
|
await SetPlaylistIndex(0).ConfigureAwait(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SetPlaylistIndex(int index, CancellationToken cancellationToken = default)
|
private async Task SetPlaylistIndex(int index)
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= _playlist.Count)
|
if (index < 0 || index >= Playlist.Count)
|
||||||
{
|
{
|
||||||
_playlist.Clear();
|
Playlist.Clear();
|
||||||
await _device.SetStop(cancellationToken).ConfigureAwait(false);
|
await _device.SetStop(CancellationToken.None);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentPlaylistIndex = index;
|
_currentPlaylistIndex = index;
|
||||||
var currentitem = _playlist[index];
|
var currentitem = Playlist[index];
|
||||||
|
|
||||||
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, CancellationToken.None);
|
||||||
|
|
||||||
var streamInfo = currentitem.StreamInfo;
|
var streamInfo = currentitem.StreamInfo;
|
||||||
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
||||||
@@ -630,17 +603,16 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
#endregion
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
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)
|
||||||
@@ -653,20 +625,24 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_device.Dispose();
|
_device.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_device.PlaybackStart -= OnDevicePlaybackStart;
|
_device.PlaybackStart -= _device_PlaybackStart;
|
||||||
_device.PlaybackProgress -= OnDevicePlaybackProgress;
|
_device.PlaybackProgress -= _device_PlaybackProgress;
|
||||||
_device.PlaybackStopped -= OnDevicePlaybackStopped;
|
_device.PlaybackStopped -= _device_PlaybackStopped;
|
||||||
_device.MediaChanged -= OnDeviceMediaChanged;
|
_device.MediaChanged -= _device_MediaChanged;
|
||||||
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
|
_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
|
||||||
_device.OnDeviceUnavailable = null;
|
_device.OnDeviceUnavailable = null;
|
||||||
_device = null;
|
_device = null;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
|
||||||
|
|
||||||
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
switch (command.Name)
|
if (Enum.TryParse(command.Name, true, out GeneralCommandType commandType))
|
||||||
|
{
|
||||||
|
switch (commandType)
|
||||||
{
|
{
|
||||||
case GeneralCommandType.VolumeDown:
|
case GeneralCommandType.VolumeDown:
|
||||||
return _device.VolumeDown(cancellationToken);
|
return _device.VolumeDown(cancellationToken);
|
||||||
@@ -679,9 +655,10 @@ 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 (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
|
if (command.Arguments.TryGetValue("Index", out string arg))
|
||||||
|
{
|
||||||
|
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
|
||||||
{
|
{
|
||||||
return SetAudioStreamIndex(val);
|
return SetAudioStreamIndex(val);
|
||||||
}
|
}
|
||||||
@@ -690,10 +667,12 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
|
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
|
||||||
|
}
|
||||||
case GeneralCommandType.SetSubtitleStreamIndex:
|
case GeneralCommandType.SetSubtitleStreamIndex:
|
||||||
if (command.Arguments.TryGetValue("Index", out index))
|
|
||||||
{
|
{
|
||||||
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
|
if (command.Arguments.TryGetValue("Index", out string arg))
|
||||||
|
{
|
||||||
|
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
|
||||||
{
|
{
|
||||||
return SetSubtitleStreamIndex(val);
|
return SetSubtitleStreamIndex(val);
|
||||||
}
|
}
|
||||||
@@ -702,10 +681,12 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
|
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
|
||||||
|
}
|
||||||
case GeneralCommandType.SetVolume:
|
case GeneralCommandType.SetVolume:
|
||||||
if (command.Arguments.TryGetValue("Volume", out string vol))
|
|
||||||
{
|
{
|
||||||
if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
|
if (command.Arguments.TryGetValue("Volume", out string arg))
|
||||||
|
{
|
||||||
|
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var volume))
|
||||||
{
|
{
|
||||||
return _device.SetVolume(volume, cancellationToken);
|
return _device.SetVolume(volume, cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -714,11 +695,15 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw new ArgumentException("Volume argument cannot be null");
|
throw new ArgumentException("Volume argument cannot be null");
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SetAudioStreamIndex(int? newIndex)
|
private async Task SetAudioStreamIndex(int? newIndex)
|
||||||
{
|
{
|
||||||
var media = _device.CurrentMediaInfo;
|
var media = _device.CurrentMediaInfo;
|
||||||
@@ -729,7 +714,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
if (info.Item != null)
|
if (info.Item != null)
|
||||||
{
|
{
|
||||||
var newPosition = GetProgressPositionTicks(info) ?? 0;
|
var newPosition = GetProgressPositionTicks(media, info) ?? 0;
|
||||||
|
|
||||||
var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null;
|
var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null;
|
||||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex);
|
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex);
|
||||||
@@ -754,7 +739,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
if (info.Item != null)
|
if (info.Item != null)
|
||||||
{
|
{
|
||||||
var newPosition = GetProgressPositionTicks(info) ?? 0;
|
var newPosition = GetProgressPositionTicks(media, info) ?? 0;
|
||||||
|
|
||||||
var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null;
|
var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null;
|
||||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex);
|
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex);
|
||||||
@@ -771,19 +756,127 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken)
|
private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
|
await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class StreamParams
|
||||||
|
{
|
||||||
|
public Guid ItemId { get; set; }
|
||||||
|
|
||||||
|
public bool IsDirectStream { get; set; }
|
||||||
|
|
||||||
|
public long StartPositionTicks { get; set; }
|
||||||
|
|
||||||
|
public int? AudioStreamIndex { get; set; }
|
||||||
|
|
||||||
|
public int? SubtitleStreamIndex { get; set; }
|
||||||
|
|
||||||
|
public string DeviceProfileId { get; set; }
|
||||||
|
public string DeviceId { get; set; }
|
||||||
|
|
||||||
|
public string MediaSourceId { get; set; }
|
||||||
|
public string LiveStreamId { get; set; }
|
||||||
|
|
||||||
|
public BaseItem Item { get; set; }
|
||||||
|
private MediaSourceInfo MediaSource;
|
||||||
|
|
||||||
|
private IMediaSourceManager _mediaSourceManager;
|
||||||
|
|
||||||
|
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (MediaSource != null)
|
||||||
|
{
|
||||||
|
return MediaSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasMediaSources = Item as IHasMediaSources;
|
||||||
|
|
||||||
|
if (hasMediaSources == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return MediaSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Guid GetItemId(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = url.Split('/');
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Length; i++)
|
||||||
|
{
|
||||||
|
var part = parts[i];
|
||||||
|
|
||||||
|
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (parts.Length > i + 1)
|
||||||
|
{
|
||||||
|
return Guid.Parse(parts[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = new StreamParams
|
||||||
|
{
|
||||||
|
ItemId = GetItemId(url)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.ItemId.Equals(Guid.Empty))
|
||||||
|
{
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = url.IndexOf('?');
|
||||||
|
if (index == -1) return request;
|
||||||
|
|
||||||
|
var query = url.Substring(index + 1);
|
||||||
|
Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
|
||||||
|
|
||||||
|
request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId");
|
||||||
|
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
||||||
|
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
||||||
|
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
||||||
|
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
||||||
|
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
||||||
|
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
||||||
|
|
||||||
|
request.Item = libraryManager.GetItemById(request.ItemId);
|
||||||
|
|
||||||
|
request._mediaSourceManager = mediaSourceManager;
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
|
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
|
||||||
{
|
{
|
||||||
var value = values.GetValueOrDefault(name);
|
var value = values.GetValueOrDefault(name);
|
||||||
@@ -808,8 +901,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
|
||||||
public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
@@ -821,17 +913,15 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name == SessionMessageType.Play)
|
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||||
}
|
}
|
||||||
|
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
|
||||||
if (name == SessionMessageType.Playstate)
|
|
||||||
{
|
{
|
||||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||||
}
|
}
|
||||||
|
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
|
||||||
if (name == SessionMessageType.GeneralCommand)
|
|
||||||
{
|
{
|
||||||
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
|
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -839,125 +929,5 @@ namespace Emby.Dlna.PlayTo
|
|||||||
// Not supported or needed right now
|
// Not supported or needed right now
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class StreamParams
|
|
||||||
{
|
|
||||||
private MediaSourceInfo mediaSource;
|
|
||||||
private IMediaSourceManager _mediaSourceManager;
|
|
||||||
|
|
||||||
public Guid ItemId { get; set; }
|
|
||||||
|
|
||||||
public bool IsDirectStream { get; set; }
|
|
||||||
|
|
||||||
public long StartPositionTicks { get; set; }
|
|
||||||
|
|
||||||
public int? AudioStreamIndex { get; set; }
|
|
||||||
|
|
||||||
public int? SubtitleStreamIndex { get; set; }
|
|
||||||
|
|
||||||
public string DeviceProfileId { get; set; }
|
|
||||||
|
|
||||||
public string DeviceId { get; set; }
|
|
||||||
|
|
||||||
public string MediaSourceId { get; set; }
|
|
||||||
|
|
||||||
public string LiveStreamId { get; set; }
|
|
||||||
|
|
||||||
public BaseItem Item { get; set; }
|
|
||||||
|
|
||||||
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (mediaSource != null)
|
|
||||||
{
|
|
||||||
return mediaSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasMediaSources = Item as IHasMediaSources;
|
|
||||||
|
|
||||||
if (hasMediaSources == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_mediaSourceManager != null)
|
|
||||||
{
|
|
||||||
mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mediaSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Guid GetItemId(string url)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(url))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(url));
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts = url.Split('/');
|
|
||||||
|
|
||||||
for (var i = 0; i < parts.Length - 1; i++)
|
|
||||||
{
|
|
||||||
var part = parts[i];
|
|
||||||
|
|
||||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (Guid.TryParse(parts[i + 1], out var result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Guid.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(url))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(url));
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = new StreamParams
|
|
||||||
{
|
|
||||||
ItemId = GetItemId(url)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (request.ItemId.Equals(Guid.Empty))
|
|
||||||
{
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = url.IndexOf('?', StringComparison.Ordinal);
|
|
||||||
if (index == -1)
|
|
||||||
{
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
var query = url.Substring(index + 1);
|
|
||||||
Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
|
|
||||||
|
|
||||||
request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId");
|
|
||||||
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
|
||||||
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
|
||||||
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
|
||||||
|
|
||||||
// Be careful, IsDirectStream==true by default (Static != false or not in query).
|
|
||||||
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
|
|
||||||
request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
|
||||||
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
|
||||||
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
|
||||||
|
|
||||||
request.Item = libraryManager.GetItemById(request.ItemId);
|
|
||||||
|
|
||||||
request._mediaSourceManager = mediaSourceManager;
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net;
|
||||||
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.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
@@ -16,13 +17,14 @@ 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;
|
||||||
|
|
||||||
namespace Emby.Dlna.PlayTo
|
namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
public sealed class PlayToManager : IDisposable
|
public class PlayToManager : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
@@ -32,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 IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IUserDataManager _userDataManager;
|
private readonly IUserDataManager _userDataManager;
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
@@ -45,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, IHttpClientFactory httpClientFactory, 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, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
@@ -55,7 +57,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
_deviceDiscovery = deviceDiscovery;
|
_deviceDiscovery = deviceDiscovery;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClient = httpClient;
|
||||||
_config = config;
|
_config = config;
|
||||||
_userDataManager = userDataManager;
|
_userDataManager = userDataManager;
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
@@ -77,20 +79,17 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var info = e.Argument;
|
var info = e.Argument;
|
||||||
|
|
||||||
if (!info.Headers.TryGetValue("USN", out string usn))
|
if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty;
|
||||||
{
|
|
||||||
usn = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!info.Headers.TryGetValue("NT", out string nt))
|
if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty;
|
||||||
{
|
|
||||||
nt = string.Empty;
|
string location = info.Location.ToString();
|
||||||
}
|
|
||||||
|
|
||||||
// It has to report that it's a media renderer
|
// It has to report that it's a media renderer
|
||||||
if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)
|
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||||
&& !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase))
|
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
|
||||||
{
|
{
|
||||||
|
//_logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,10 +109,11 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await AddDevice(info, cancellationToken).ConfigureAwait(false);
|
await AddDevice(info, location, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -125,50 +125,42 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string GetUuid(string usn)
|
private string GetUuid(string usn)
|
||||||
{
|
{
|
||||||
const string UuidStr = "uuid:";
|
var found = false;
|
||||||
const string UuidColonStr = "::";
|
var index = usn.IndexOf("uuid:", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (index != -1)
|
||||||
|
{
|
||||||
|
usn = usn.Substring(index);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (index != -1)
|
||||||
|
{
|
||||||
|
usn = usn.Substring(0, index);
|
||||||
|
}
|
||||||
|
|
||||||
var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
|
if (found)
|
||||||
if (index == -1)
|
|
||||||
{
|
{
|
||||||
|
return usn;
|
||||||
|
}
|
||||||
|
|
||||||
return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadOnlySpan<char> tmp = usn.AsSpan()[(index + UuidStr.Length)..];
|
private async Task AddDevice(UpnpDeviceInfo info, string location, CancellationToken cancellationToken)
|
||||||
|
|
||||||
index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (index != -1)
|
|
||||||
{
|
|
||||||
tmp = tmp[..index];
|
|
||||||
}
|
|
||||||
|
|
||||||
index = tmp.IndexOf('{');
|
|
||||||
if (index != -1)
|
|
||||||
{
|
|
||||||
int endIndex = tmp.IndexOf('}');
|
|
||||||
if (endIndex != -1)
|
|
||||||
{
|
|
||||||
tmp = tmp[(index + 1)..endIndex];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tmp.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddDevice(UpnpDeviceInfo info, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
var uri = info.Location;
|
var uri = info.Location;
|
||||||
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
|
_logger.LogDebug("Attempting to create PlayToController from location {0}", location);
|
||||||
|
|
||||||
|
_logger.LogDebug("Logging session activity from location {0}", location);
|
||||||
if (info.Headers.TryGetValue("USN", out string uuid))
|
if (info.Headers.TryGetValue("USN", out string uuid))
|
||||||
{
|
{
|
||||||
uuid = GetUuid(uuid);
|
uuid = GetUuid(uuid);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
uuid = location.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
|
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
|
||||||
@@ -177,16 +169,23 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
if (controller == null)
|
if (controller == null)
|
||||||
{
|
{
|
||||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
string deviceName = device.Properties.Name;
|
string deviceName = device.Properties.Name;
|
||||||
|
|
||||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||||
|
|
||||||
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
|
string serverAddress;
|
||||||
|
if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IPAddress.Any) || info.LocalIpAddress.Equals(IPAddress.IPv6Any))
|
||||||
|
{
|
||||||
|
serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
|
||||||
|
}
|
||||||
|
|
||||||
controller = new PlayToController(
|
controller = new PlayToController(sessionInfo,
|
||||||
sessionInfo,
|
|
||||||
_sessionManager,
|
_sessionManager,
|
||||||
_libraryManager,
|
_libraryManager,
|
||||||
_logger,
|
_logger,
|
||||||
@@ -199,6 +198,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_userDataManager,
|
_userDataManager,
|
||||||
_localization,
|
_localization,
|
||||||
_mediaSourceManager,
|
_mediaSourceManager,
|
||||||
|
_config,
|
||||||
_mediaEncoder);
|
_mediaEncoder);
|
||||||
|
|
||||||
sessionInfo.AddController(controller);
|
sessionInfo.AddController(controller);
|
||||||
@@ -212,17 +212,17 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
PlayableMediaTypes = profile.GetSupportedMediaTypes(),
|
PlayableMediaTypes = profile.GetSupportedMediaTypes(),
|
||||||
|
|
||||||
SupportedCommands = new[]
|
SupportedCommands = new string[]
|
||||||
{
|
{
|
||||||
GeneralCommandType.VolumeDown,
|
GeneralCommandType.VolumeDown.ToString(),
|
||||||
GeneralCommandType.VolumeUp,
|
GeneralCommandType.VolumeUp.ToString(),
|
||||||
GeneralCommandType.Mute,
|
GeneralCommandType.Mute.ToString(),
|
||||||
GeneralCommandType.Unmute,
|
GeneralCommandType.Unmute.ToString(),
|
||||||
GeneralCommandType.ToggleMute,
|
GeneralCommandType.ToggleMute.ToString(),
|
||||||
GeneralCommandType.SetVolume,
|
GeneralCommandType.SetVolume.ToString(),
|
||||||
GeneralCommandType.SetAudioStreamIndex,
|
GeneralCommandType.SetAudioStreamIndex.ToString(),
|
||||||
GeneralCommandType.SetSubtitleStreamIndex,
|
GeneralCommandType.SetSubtitleStreamIndex.ToString(),
|
||||||
GeneralCommandType.PlayMediaSource
|
GeneralCommandType.PlayMediaSource.ToString()
|
||||||
},
|
},
|
||||||
|
|
||||||
SupportsMediaControl = true
|
SupportsMediaControl = true
|
||||||
@@ -232,7 +232,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
|
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
|
||||||
@@ -241,13 +240,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
_disposeCancellationTokenSource.Cancel();
|
_disposeCancellationTokenSource.Cancel();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch
|
||||||
{
|
{
|
||||||
_logger.LogDebug(ex, "Error while disposing PlayToManager");
|
|
||||||
}
|
|
||||||
|
|
||||||
_sessionLock.Dispose();
|
}
|
||||||
_disposeCancellationTokenSource.Dispose();
|
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -6,6 +7,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class PlaybackProgressEventArgs : EventArgs
|
public class PlaybackProgressEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public UBaseObject MediaInfo { get; set; }
|
public uBaseObject MediaInfo { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -6,6 +7,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class PlaybackStartEventArgs : EventArgs
|
public class PlaybackStartEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public UBaseObject MediaInfo { get; set; }
|
public uBaseObject MediaInfo { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -6,6 +7,12 @@ 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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -8,9 +9,9 @@ using MediaBrowser.Model.Session;
|
|||||||
|
|
||||||
namespace Emby.Dlna.PlayTo
|
namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
public static class PlaylistItemFactory
|
public class PlaylistItemFactory
|
||||||
{
|
{
|
||||||
public static PlaylistItem Create(Photo item, DeviceProfile profile)
|
public PlaylistItem Create(Photo item, DeviceProfile profile)
|
||||||
{
|
{
|
||||||
var playlistItem = new PlaylistItem
|
var playlistItem = new PlaylistItem
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
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.Mime;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -21,11 +21,11 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||||
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClient _httpClient;
|
||||||
|
|
||||||
public SsdpHttpClient(IHttpClientFactory httpClientFactory)
|
public SsdpHttpClient(IHttpClient httpClient)
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<XDocument> SendCommandAsync(
|
public async Task<XDocument> SendCommandAsync(
|
||||||
@@ -33,23 +33,28 @@ namespace Emby.Dlna.PlayTo
|
|||||||
DeviceService service,
|
DeviceService service,
|
||||||
string command,
|
string command,
|
||||||
string postData,
|
string postData,
|
||||||
string header = null,
|
bool logRequest = true,
|
||||||
CancellationToken cancellationToken = default)
|
string header = null)
|
||||||
{
|
{
|
||||||
|
var cancellationToken = CancellationToken.None;
|
||||||
|
|
||||||
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,
|
||||||
|
logRequest,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false))
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
using (var stream = response.Content)
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -59,7 +64,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return serviceUrl;
|
return serviceUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!serviceUrl.StartsWith('/'))
|
if (!serviceUrl.StartsWith("/"))
|
||||||
{
|
{
|
||||||
serviceUrl = "/" + serviceUrl;
|
serviceUrl = "/" + serviceUrl;
|
||||||
}
|
}
|
||||||
@@ -75,36 +80,55 @@ namespace Emby.Dlna.PlayTo
|
|||||||
int eventport,
|
int eventport,
|
||||||
int timeOut = 3600)
|
int timeOut = 3600)
|
||||||
{
|
{
|
||||||
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
|
var options = new HttpRequestOptions
|
||||||
options.Headers.UserAgent.ParseAdd(USERAGENT);
|
{
|
||||||
options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
|
Url = url,
|
||||||
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
|
UserAgent = USERAGENT,
|
||||||
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
|
LogErrorResponseBody = true,
|
||||||
options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
|
BufferContent = false,
|
||||||
|
};
|
||||||
|
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture);
|
||||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
|
options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">";
|
||||||
.ConfigureAwait(false);
|
options.RequestHeaders["NT"] = "upnp:event";
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
using var options = new HttpRequestMessage(HttpMethod.Get, url);
|
var options = new HttpRequestOptions
|
||||||
options.Headers.UserAgent.ParseAdd(USERAGENT);
|
{
|
||||||
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
Url = url,
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
UserAgent = USERAGENT,
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
LogErrorResponseBody = true,
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
BufferContent = false,
|
||||||
|
|
||||||
|
CancellationToken = cancellationToken
|
||||||
|
};
|
||||||
|
|
||||||
|
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(
|
return XDocument.Parse(
|
||||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
await reader.ReadToEndAsync().ConfigureAwait(false),
|
||||||
LoadOptions.PreserveWhitespace);
|
LoadOptions.PreserveWhitespace);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
private Task<HttpResponseInfo> PostSoapDataAsync(
|
||||||
string url,
|
string url,
|
||||||
string soapAction,
|
string soapAction,
|
||||||
string postData,
|
string postData,
|
||||||
string header,
|
string header,
|
||||||
|
bool logRequest,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (soapAction[0] != '\"')
|
if (soapAction[0] != '\"')
|
||||||
@@ -112,20 +136,29 @@ namespace Emby.Dlna.PlayTo
|
|||||||
soapAction = $"\"{soapAction}\"";
|
soapAction = $"\"{soapAction}\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
using var options = new HttpRequestMessage(HttpMethod.Post, url);
|
var options = new HttpRequestOptions
|
||||||
options.Headers.UserAgent.ParseAdd(USERAGENT);
|
{
|
||||||
options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction);
|
Url = url,
|
||||||
options.Headers.TryAddWithoutValidation("Pragma", "no-cache");
|
UserAgent = USERAGENT,
|
||||||
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
LogErrorResponseBody = true,
|
||||||
|
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.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
|
options.RequestHeaders["contentFeatures.dlna.org"] = header;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml);
|
options.RequestContentType = "text/xml";
|
||||||
|
options.RequestContent = postData;
|
||||||
|
|
||||||
return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
return _httpClient.Post(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
Emby.Dlna/PlayTo/TRANSPORTSTATE.cs
Normal file
14
Emby.Dlna/PlayTo/TRANSPORTSTATE.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
|
namespace Emby.Dlna.PlayTo
|
||||||
|
{
|
||||||
|
public enum TRANSPORTSTATE
|
||||||
|
{
|
||||||
|
STOPPED,
|
||||||
|
PLAYING,
|
||||||
|
TRANSITIONING,
|
||||||
|
PAUSED_PLAYBACK,
|
||||||
|
PAUSED
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
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;
|
||||||
@@ -12,30 +12,36 @@ 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;
|
||||||
public List<ServiceAction> ServiceActions => _serviceActions;
|
set => _serviceActions = value;
|
||||||
|
}
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
@@ -46,19 +52,19 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
private static ServiceAction ServiceActionFromXml(XElement container)
|
private static ServiceAction ServiceActionFromXml(XElement container)
|
||||||
{
|
{
|
||||||
var serviceAction = new ServiceAction
|
var argumentList = new List<Argument>();
|
||||||
{
|
|
||||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
|
||||||
};
|
|
||||||
|
|
||||||
var argumentList = serviceAction.ArgumentList;
|
foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
|
||||||
|
|
||||||
foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument"))
|
|
||||||
{
|
{
|
||||||
argumentList.Add(ArgumentFromXml(arg));
|
argumentList.Add(ArgumentFromXml(arg));
|
||||||
}
|
}
|
||||||
|
|
||||||
return serviceAction;
|
return new ServiceAction
|
||||||
|
{
|
||||||
|
Name = container.GetValue(uPnpNamespaces.svc + "name"),
|
||||||
|
|
||||||
|
ArgumentList = argumentList
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Argument ArgumentFromXml(XElement container)
|
private static Argument ArgumentFromXml(XElement container)
|
||||||
@@ -70,30 +76,30 @@ 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 = Array.Empty<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 = values.Select(child => child.Value).ToArray();
|
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
|
AllowedValues = allowedValues.ToArray()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +109,12 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
foreach (var arg in action.ArgumentList)
|
foreach (var arg in action.ArgumentList)
|
||||||
{
|
{
|
||||||
if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
|
if (arg.Direction == "out")
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
|
if (arg.Name == "InstanceID")
|
||||||
{
|
{
|
||||||
stateString += BuildArgumentXml(arg, "0");
|
stateString += BuildArgumentXml(arg, "0");
|
||||||
}
|
}
|
||||||
@@ -118,7 +124,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
return string.Format(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 = "")
|
||||||
@@ -127,12 +133,12 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
foreach (var arg in action.ArgumentList)
|
foreach (var arg in action.ArgumentList)
|
||||||
{
|
{
|
||||||
if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
|
if (arg.Direction == "out")
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
|
if (arg.Name == "InstanceID")
|
||||||
{
|
{
|
||||||
stateString += BuildArgumentXml(arg, "0");
|
stateString += BuildArgumentXml(arg, "0");
|
||||||
}
|
}
|
||||||
@@ -142,7 +148,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
return string.Format(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)
|
||||||
@@ -151,7 +157,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
foreach (var arg in action.ArgumentList)
|
foreach (var arg in action.ArgumentList)
|
||||||
{
|
{
|
||||||
if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
|
if (arg.Name == "InstanceID")
|
||||||
{
|
{
|
||||||
stateString += BuildArgumentXml(arg, "0");
|
stateString += BuildArgumentXml(arg, "0");
|
||||||
}
|
}
|
||||||
@@ -165,7 +171,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
|
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
|
||||||
@@ -175,12 +181,15 @@ 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.Count > 0 ? state.AllowedValues[0] : value);
|
state.AllowedValues.FirstOrDefault() ??
|
||||||
|
value;
|
||||||
|
|
||||||
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} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
|
return string.Format("<{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>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
#pragma warning disable SA1602
|
|
||||||
|
|
||||||
namespace Emby.Dlna.PlayTo
|
|
||||||
{
|
|
||||||
public enum TransportState
|
|
||||||
{
|
|
||||||
Stopped,
|
|
||||||
Playing,
|
|
||||||
Transitioning,
|
|
||||||
PausedPlayback,
|
|
||||||
Paused
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
@@ -6,22 +7,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.Class)
|
UpnpClass = container.GetValue(uPnpNamespaces.uClass)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
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; }
|
||||||
|
|
||||||
@@ -21,10 +21,20 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
|
|
||||||
public IReadOnlyList<string> ProtocolInfo { get; set; }
|
public 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
|
||||||
@@ -35,12 +45,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
return MediaBrowser.Model.Entities.MediaType.Audio;
|
return MediaBrowser.Model.Entities.MediaType.Audio;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
|
if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
|
||||||
{
|
{
|
||||||
return MediaBrowser.Model.Entities.MediaType.Video;
|
return MediaBrowser.Model.Entities.MediaType.Video;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
|
if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
|
||||||
{
|
{
|
||||||
return MediaBrowser.Model.Entities.MediaType.Photo;
|
return MediaBrowser.Model.Entities.MediaType.Photo;
|
||||||
@@ -49,15 +57,5 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,42 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace Emby.Dlna.PlayTo
|
namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
public static class UPnpNamespaces
|
public class uPnpNamespaces
|
||||||
{
|
{
|
||||||
public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/";
|
public static XNamespace dc = "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 XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
public static XName containers = ns + "container";
|
||||||
|
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 XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0";
|
public static XName ServiceStateTable = svc + "serviceStateTable";
|
||||||
|
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";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
@@ -64,14 +65,14 @@ namespace Emby.Dlna.Profiles
|
|||||||
new DirectPlayProfile
|
new DirectPlayProfile
|
||||||
{
|
{
|
||||||
// play all
|
// play all
|
||||||
Container = string.Empty,
|
Container = "",
|
||||||
Type = DlnaProfileType.Video
|
Type = DlnaProfileType.Video
|
||||||
},
|
},
|
||||||
|
|
||||||
new DirectPlayProfile
|
new DirectPlayProfile
|
||||||
{
|
{
|
||||||
// play all
|
// play all
|
||||||
Container = string.Empty,
|
Container = "",
|
||||||
Type = DlnaProfileType.Audio
|
Type = DlnaProfileType.Audio
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -164,7 +165,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
|
|
||||||
public void AddXmlRootAttribute(string name, string value)
|
public void AddXmlRootAttribute(string name, string value)
|
||||||
{
|
{
|
||||||
var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
|
var atts = XmlRootAttributes ?? new XmlAttribute[] { };
|
||||||
var list = atts.ToList();
|
var list = atts.ToList();
|
||||||
|
|
||||||
list.Add(new XmlAttribute
|
list.Add(new XmlAttribute
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
ResponseProfiles = System.Array.Empty<ResponseProfile>();
|
ResponseProfiles = new ResponseProfile[] { };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ResponseProfiles = System.Array.Empty<ResponseProfile>();
|
ResponseProfiles = new ResponseProfile[] { };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
{
|
{
|
||||||
Match = HeaderMatchType.Substring,
|
Match = HeaderMatchType.Substring,
|
||||||
Name = "User-Agent",
|
Name = "User-Agent",
|
||||||
Value = "Zip_"
|
Value ="Zip_"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -81,7 +82,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 +125,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 +162,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 +178,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 +193,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
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user