mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-12 20:13:01 +03:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da34bd940e | ||
|
|
54efde4073 | ||
|
|
02cfa15582 | ||
|
|
0e171d794c | ||
|
|
d5f2384375 | ||
|
|
816b3f5c2e | ||
|
|
a234388552 | ||
|
|
30d38bd5c6 | ||
|
|
9f49fe2e99 | ||
|
|
f720a0fca2 | ||
|
|
aadff77531 | ||
|
|
89fc5aa11a | ||
|
|
cb91595a24 | ||
|
|
6233bae7f0 |
@@ -1,23 +1,21 @@
|
||||
parameters:
|
||||
- name: Packages
|
||||
type: object
|
||||
default: {}
|
||||
- name: LinuxImage
|
||||
type: string
|
||||
default: "ubuntu-latest"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 6.0.x
|
||||
- name: Packages
|
||||
type: object
|
||||
default: {}
|
||||
- name: LinuxImage
|
||||
type: string
|
||||
default: "ubuntu-latest"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 3.1.100
|
||||
|
||||
jobs:
|
||||
- job: CompatibilityCheck
|
||||
displayName: Compatibility Check
|
||||
dependsOn: Build
|
||||
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
|
||||
|
||||
pool:
|
||||
vmImage: "${{ parameters.LinuxImage }}"
|
||||
|
||||
# only execute for pull requests
|
||||
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
|
||||
strategy:
|
||||
matrix:
|
||||
${{ each Package in parameters.Packages }}:
|
||||
@@ -25,7 +23,7 @@ jobs:
|
||||
NugetPackageName: ${{ Package.value.NugetPackageName }}
|
||||
AssemblyFileName: ${{ Package.value.AssemblyFileName }}
|
||||
maxParallel: 2
|
||||
|
||||
dependsOn: MainBuild
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
@@ -35,34 +33,26 @@ jobs:
|
||||
packageType: sdk
|
||||
version: ${{ parameters.DotNetSdkVersion }}
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Install ABI CompatibilityChecker Tool'
|
||||
inputs:
|
||||
command: custom
|
||||
custom: tool
|
||||
arguments: 'update compatibilitychecker -g'
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download New Assembly Build Artifact'
|
||||
displayName: "Download New Assembly Build Artifact"
|
||||
inputs:
|
||||
source: 'current'
|
||||
source: "current"
|
||||
artifact: "$(NugetPackageName)"
|
||||
path: "$(System.ArtifactsDirectory)/new-artifacts"
|
||||
runVersion: "latest"
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy New Assembly Build Artifact'
|
||||
displayName: "Copy New Assembly Build Artifact"
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
|
||||
contents: '**/*.dll'
|
||||
contents: "**/*.dll"
|
||||
targetFolder: $(System.ArtifactsDirectory)/new-release
|
||||
cleanTargetFolder: true
|
||||
overWrite: true
|
||||
flattenFolders: true
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download Reference Assembly Build Artifact'
|
||||
enabled: false
|
||||
displayName: "Download Reference Assembly Build Artifact"
|
||||
inputs:
|
||||
source: "specific"
|
||||
artifact: "$(NugetPackageName)"
|
||||
@@ -73,21 +63,34 @@ jobs:
|
||||
runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy Reference Assembly Build Artifact'
|
||||
enabled: false
|
||||
displayName: "Copy Reference Assembly Build Artifact"
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
|
||||
contents: '**/*.dll'
|
||||
contents: "**/*.dll"
|
||||
targetFolder: $(System.ArtifactsDirectory)/current-release
|
||||
cleanTargetFolder: true
|
||||
overWrite: true
|
||||
flattenFolders: true
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Execute ABI Compatibility Check Tool'
|
||||
enabled: false
|
||||
- task: DownloadGitHubRelease@0
|
||||
displayName: "Download ABI Compatibility Check Tool"
|
||||
inputs:
|
||||
command: custom
|
||||
custom: compat
|
||||
arguments: 'current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
|
||||
connection: Jellyfin Release Download
|
||||
userRepository: EraYaN/dotnet-compatibility
|
||||
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)
|
||||
@@ -1,100 +1,101 @@
|
||||
parameters:
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
DotNetSdkVersion: 6.0.x
|
||||
LinuxImage: "ubuntu-latest"
|
||||
RestoreBuildProjects: "Jellyfin.Server/Jellyfin.Server.csproj"
|
||||
DotNetSdkVersion: 3.1.100
|
||||
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
- job: MainBuild
|
||||
displayName: Main Build
|
||||
strategy:
|
||||
matrix:
|
||||
Release:
|
||||
BuildConfiguration: Release
|
||||
Debug:
|
||||
BuildConfiguration: Debug
|
||||
maxParallel: 2
|
||||
pool:
|
||||
vmImage: '${{ parameters.LinuxImage }}'
|
||||
vmImage: "${{ parameters.LinuxImage }}"
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
submodules: true
|
||||
persistCredentials: true
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download Web Branch'
|
||||
condition: in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')
|
||||
- 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')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||
inputs:
|
||||
path: '$(Agent.TempDirectory)'
|
||||
artifact: 'jellyfin-web-production'
|
||||
source: 'specific'
|
||||
project: 'jellyfin'
|
||||
pipeline: 'Jellyfin Web'
|
||||
runBranch: variables['Build.SourceBranch']
|
||||
script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download Web Target'
|
||||
condition: eq(variables['Build.Reason'], 'PullRequest')
|
||||
- task: CmdLine@2
|
||||
displayName: "Clone Web Client (PR)"
|
||||
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:
|
||||
path: '$(Agent.TempDirectory)'
|
||||
artifact: 'jellyfin-web-production'
|
||||
source: 'specific'
|
||||
project: 'jellyfin'
|
||||
pipeline: 'Jellyfin Web'
|
||||
runBranch: variables['System.PullRequest.TargetBranch']
|
||||
script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
|
||||
|
||||
- task: ExtractFiles@1
|
||||
displayName: 'Extract Web Client'
|
||||
- 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')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Agent.TempDirectory)/*.zip'
|
||||
destinationFolder: '$(Build.SourcesDirectory)/MediaBrowser.WebDashboard'
|
||||
cleanDestinationFolder: false
|
||||
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')), 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
|
||||
displayName: 'Update DotNet'
|
||||
displayName: "Update DotNet"
|
||||
inputs:
|
||||
packageType: sdk
|
||||
version: ${{ parameters.DotNetSdkVersion }}
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Publish Server'
|
||||
displayName: "Publish Server"
|
||||
inputs:
|
||||
command: publish
|
||||
publishWebProjects: false
|
||||
projects: '${{ parameters.RestoreBuildProjects }}'
|
||||
arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
|
||||
projects: "${{ parameters.RestoreBuildProjects }}"
|
||||
arguments: "--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)"
|
||||
zipAfterPublish: false
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Artifact Naming'
|
||||
- task: PublishPipelineArtifact@0
|
||||
displayName: "Publish Artifact Naming"
|
||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||
inputs:
|
||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
|
||||
artifactName: 'Jellyfin.Naming'
|
||||
targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll"
|
||||
artifactName: "Jellyfin.Naming"
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Artifact Controller'
|
||||
- task: PublishPipelineArtifact@0
|
||||
displayName: "Publish Artifact Controller"
|
||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||
inputs:
|
||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
|
||||
artifactName: 'Jellyfin.Controller'
|
||||
targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll"
|
||||
artifactName: "Jellyfin.Controller"
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Artifact Model'
|
||||
- task: PublishPipelineArtifact@0
|
||||
displayName: "Publish Artifact Model"
|
||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||
inputs:
|
||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
|
||||
artifactName: 'Jellyfin.Model'
|
||||
targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll"
|
||||
artifactName: "Jellyfin.Model"
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Artifact Common'
|
||||
- task: PublishPipelineArtifact@0
|
||||
displayName: "Publish Artifact Common"
|
||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||
inputs:
|
||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
|
||||
artifactName: 'Jellyfin.Common'
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish Artifact Extensions'
|
||||
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
|
||||
inputs:
|
||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
|
||||
artifactName: 'Jellyfin.Extensions'
|
||||
targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll"
|
||||
artifactName: "Jellyfin.Common"
|
||||
|
||||
@@ -1,269 +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: 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')
|
||||
|
||||
- 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:
|
||||
- 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: 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
|
||||
|
||||
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) $(Build.SourceBranch) &
|
||||
|
||||
- job: PublishNuget
|
||||
displayName: 'Publish NuGet packages'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
variables:
|
||||
- name: JellyfinVersion
|
||||
value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Use .NET 6.0 sdk'
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '6.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
|
||||
src/Jellyfin.Extensions/Jellyfin.Extensions.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
|
||||
src/Jellyfin.Extensions/Jellyfin.Extensions.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:
|
||||
- name: ImageNames
|
||||
type: object
|
||||
default:
|
||||
Linux: "ubuntu-latest"
|
||||
Windows: "windows-latest"
|
||||
macOS: "macos-latest"
|
||||
- name: TestProjects
|
||||
type: string
|
||||
default: "tests/**/*Tests.csproj"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 6.0.x
|
||||
- name: ImageNames
|
||||
type: object
|
||||
default:
|
||||
Linux: "ubuntu-latest"
|
||||
Windows: "windows-latest"
|
||||
macOS: "macos-latest"
|
||||
- name: TestProjects
|
||||
type: string
|
||||
default: "tests/**/*Tests.csproj"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 3.1.100
|
||||
|
||||
jobs:
|
||||
- job: Test
|
||||
displayName: Test
|
||||
- job: MainTest
|
||||
displayName: Main Test
|
||||
strategy:
|
||||
matrix:
|
||||
${{ each imageName in parameters.ImageNames }}:
|
||||
${{ imageName.key }}:
|
||||
ImageName: ${{ imageName.value }}
|
||||
maxParallel: 3
|
||||
pool:
|
||||
vmImage: "$(ImageName)"
|
||||
steps:
|
||||
@@ -28,52 +29,25 @@ jobs:
|
||||
submodules: true
|
||||
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
|
||||
displayName: "Update DotNet"
|
||||
inputs:
|
||||
packageType: sdk
|
||||
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
|
||||
displayName: 'Run CLI Tests'
|
||||
displayName: Run .NET Core CLI tests
|
||||
inputs:
|
||||
command: "test"
|
||||
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
|
||||
testRunTitle: $(Agent.JobName)
|
||||
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
|
||||
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:
|
||||
reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
|
||||
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.
|
||||
- task: PublishCodeCoverageResults@1
|
||||
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:
|
||||
codeCoverageTool: "cobertura"
|
||||
#summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
|
||||
summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
|
||||
pathToSources: $(Build.SourcesDirectory)
|
||||
failIfCoverageEmpty: true
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish OpenAPI Artifact'
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
inputs:
|
||||
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.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,46 +1,38 @@
|
||||
name: $(Date:yyyyMMdd)$(Rev:.r)
|
||||
|
||||
variables:
|
||||
- name: TestProjects
|
||||
value: 'tests/**/*Tests.csproj'
|
||||
- name: RestoreBuildProjects
|
||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
- name: TestProjects
|
||||
value: "tests/**/*Tests.csproj"
|
||||
- name: RestoreBuildProjects
|
||||
value: "Jellyfin.Server/Jellyfin.Server.csproj"
|
||||
- name: DotNetSdkVersion
|
||||
value: 3.1.100
|
||||
|
||||
pr:
|
||||
autoCancel: true
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- '*'
|
||||
tags:
|
||||
include:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}:
|
||||
- template: azure-pipelines-main.yml
|
||||
parameters:
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
LinuxImage: "ubuntu-latest"
|
||||
RestoreBuildProjects: $(RestoreBuildProjects)
|
||||
|
||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
|
||||
- template: azure-pipelines-test.yml
|
||||
parameters:
|
||||
ImageNames:
|
||||
Linux: 'ubuntu-latest'
|
||||
Windows: 'windows-latest'
|
||||
macOS: 'macos-latest'
|
||||
|
||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
||||
- template: azure-pipelines-test.yml
|
||||
parameters:
|
||||
ImageNames:
|
||||
Linux: 'ubuntu-latest'
|
||||
Linux: "ubuntu-latest"
|
||||
Windows: "windows-latest"
|
||||
macOS: "macos-latest"
|
||||
|
||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
|
||||
- template: azure-pipelines-abi.yml
|
||||
- template: azure-pipelines-windows.yml
|
||||
parameters:
|
||||
WindowsImage: "windows-latest"
|
||||
TestProjects: $(TestProjects)
|
||||
|
||||
- template: azure-pipelines-compat.yml
|
||||
parameters:
|
||||
Packages:
|
||||
Naming:
|
||||
@@ -55,10 +47,4 @@ jobs:
|
||||
Common:
|
||||
NugetPackageName: Jellyfin.Common
|
||||
AssemblyFileName: MediaBrowser.Common.dll
|
||||
Extensions:
|
||||
NugetPackageName: Jellyfin.Extensions
|
||||
AssemblyFileName: Jellyfin.Extensions.dll
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
|
||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
||||
- template: azure-pipelines-package.yml
|
||||
LinuxImage: "ubuntu-latest"
|
||||
|
||||
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)"
|
||||
30
.drone.yml
Normal file
30
.drone.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: build-debug
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: docker:git
|
||||
commands:
|
||||
- git submodule update --init --recursive
|
||||
|
||||
- name: build
|
||||
image: microsoft/dotnet:2-sdk
|
||||
commands:
|
||||
- dotnet publish "Jellyfin.Server" --configuration Debug --output "../ci/ci-debug"
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: build-release
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: docker:git
|
||||
commands:
|
||||
- git submodule update --init --recursive
|
||||
|
||||
- name: build
|
||||
image: microsoft/dotnet:2-sdk
|
||||
commands:
|
||||
- dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release"
|
||||
|
||||
@@ -13,7 +13,7 @@ charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
max_line_length = off
|
||||
max_line_length = null
|
||||
|
||||
# YAML indentation
|
||||
[*.{yml,yaml}]
|
||||
@@ -22,7 +22,6 @@ indent_size = 2
|
||||
# XML indentation
|
||||
[*.{csproj,xml}]
|
||||
indent_size = 2
|
||||
|
||||
###############################
|
||||
# .NET Coding Conventions #
|
||||
###############################
|
||||
@@ -52,12 +51,11 @@ dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||
|
||||
###############################
|
||||
# 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_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_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_style_pattern_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
|
||||
###############################
|
||||
# C# Formatting Rules #
|
||||
###############################
|
||||
@@ -192,3 +189,9 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
# Wrapping preferences
|
||||
csharp_preserve_single_line_statements = 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
|
||||
|
||||
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -1,4 +0,0 @@
|
||||
# Joshua must review all changes to deployment and build.sh
|
||||
.ci/* @joshuaboniface
|
||||
deployment/* @joshuaboniface
|
||||
build.sh @joshuaboniface
|
||||
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**System (please complete the following information):**
|
||||
- OS: [e.g. Debian, Windows]
|
||||
- Virtualization: [e.g. Docker, KVM, LXC]
|
||||
- Clients: [Browser, Android, Fire Stick, etc.]
|
||||
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
|
||||
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
|
||||
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
|
||||
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
|
||||
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
|
||||
- Base URL: [e.g. none, yes: /example]
|
||||
- Networking: [e.g. Host, Bridge/NAT]
|
||||
- Storage: [e.g. local, NFS, cloud]
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Logs**
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
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.
|
||||
106
.github/ISSUE_TEMPLATE/issue report.yml
vendored
106
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -1,106 +0,0 @@
|
||||
name: Issue Report
|
||||
description: File an issue report
|
||||
title: "[Issue]: "
|
||||
labels: [bug, triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Please describe your bug
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: |
|
||||
The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
|
||||
|
||||
This is my issue.
|
||||
|
||||
Steps to Reproduce
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Jellyfin Version
|
||||
description: What version of Jellyfin are you running?
|
||||
options:
|
||||
- 10.7.7
|
||||
- 10.7.z
|
||||
- 10.6.4
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version-other
|
||||
attributes:
|
||||
label: "if other:"
|
||||
placeholder: Other
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
Examples:
|
||||
- **OS**: [e.g. Debian, Windows]
|
||||
- **Virtualization**: [e.g. Docker, KVM, LXC]
|
||||
- **Clients**: [Browser, Android, Fire Stick, etc.]
|
||||
- **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
|
||||
- **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
|
||||
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
|
||||
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
|
||||
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
|
||||
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
||||
- **Base URL**: [e.g. none, yes: /example]
|
||||
- **Networking**: [e.g. Host, Bridge/NAT]
|
||||
- **Storage**: [e.g. local, NFS, cloud]
|
||||
value: |
|
||||
- OS:
|
||||
- Virtualization:
|
||||
- Clients:
|
||||
- Browser:
|
||||
- FFmpeg Version:
|
||||
- Playback Method:
|
||||
- Hardware Acceleration:
|
||||
- Plugins:
|
||||
- Reverse Proxy:
|
||||
- Base URL:
|
||||
- Networking:
|
||||
- Storage:
|
||||
render: markdown
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Jellyfin logs
|
||||
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||
placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: ffmpeg-logs
|
||||
attributes:
|
||||
label: FFmpeg logs
|
||||
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||
placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: browserlogs
|
||||
attributes:
|
||||
label: Please attach any browser or client logs here
|
||||
placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Please attach any screenshots here
|
||||
placeholder: Images can be pasted directly into the textbox and will be hosted by github.
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
@@ -1,15 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: nuget
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '12:00'
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '12:00'
|
||||
open-pull-requests-limit: 10
|
||||
25
.github/stale.yml
vendored
Normal file
25
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 120
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 21
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- regression
|
||||
- security
|
||||
- dotnet-3.0-future
|
||||
- roadmap
|
||||
- future
|
||||
- feature
|
||||
- enhancement
|
||||
- confirmed
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
76
.github/workflows/automation.yml
vendored
76
.github/workflows/automation.yml
vendored
@@ -1,76 +0,0 @@
|
||||
name: Automation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
issue_comment:
|
||||
|
||||
jobs:
|
||||
label:
|
||||
name: Labeling
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
project:
|
||||
name: Project board
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||
steps:
|
||||
- name: Remove from 'Current Release' project
|
||||
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Current Release
|
||||
action: delete
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add to 'Release Next' project
|
||||
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Release Next
|
||||
column: In progress
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add to 'Current Release' project
|
||||
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Current Release
|
||||
column: In progress
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Check number of comments from the team member
|
||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
|
||||
id: member_comments
|
||||
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
||||
|
||||
- name: Move issue to needs triage
|
||||
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Issue Triage for Main Repo
|
||||
column: Needs triage
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add issue to triage project
|
||||
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Issue Triage for Main Repo
|
||||
column: Pending response
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
37
.github/workflows/codeql-analysis.yml
vendored
37
.github/workflows/codeql-analysis.yml
vendored
@@ -1,37 +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@v3
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: '6.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
|
||||
119
.github/workflows/commands.yml
vendored
119
.github/workflows/commands.yml
vendored
@@ -1,119 +0,0 @@
|
||||
name: Commands
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
- edited
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
rebase:
|
||||
name: Rebase
|
||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
check-backport:
|
||||
name: Check Backport
|
||||
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ github.event.comment != null }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: eyes
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Notify as running
|
||||
id: comment_running
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ github.event.comment != null }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Running backport tests...
|
||||
|
||||
- name: Perform test backport
|
||||
id: run_tests
|
||||
run: |
|
||||
set +o errexit
|
||||
git config --global user.name "Jellyfin Bot"
|
||||
git config --global user.email "team@jellyfin.org"
|
||||
CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
|
||||
git checkout master
|
||||
git merge --no-ff ${CURRENT_BRANCH}
|
||||
MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
|
||||
git fetch --all
|
||||
CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
|
||||
stable_branch="Current stable release branch: ${CURRENT_STABLE}"
|
||||
echo ${stable_branch}
|
||||
echo ::set-output name=branch::${stable_branch}
|
||||
git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
|
||||
git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
|
||||
retcode=$?
|
||||
cat output.txt | grep -v 'hint:'
|
||||
output="$( grep -v 'hint:' output.txt )"
|
||||
output="${output//'%'/'%25'}"
|
||||
output="${output//$'\n'/'%0A'}"
|
||||
output="${output//$'\r'/'%0D'}"
|
||||
echo ::set-output name=output::$output
|
||||
exit ${retcode}
|
||||
|
||||
- name: Notify with result success
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ github.event.comment != null && success() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
||||
body: |
|
||||
${{ steps.run_tests.outputs.branch }}
|
||||
Output from `git cherry-pick`:
|
||||
|
||||
---
|
||||
|
||||
${{ steps.run_tests.outputs.output }}
|
||||
reactions: hooray
|
||||
|
||||
- name: Notify with result failure
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ github.event.comment != null && failure() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
||||
body: |
|
||||
${{ steps.run_tests.outputs.branch }}
|
||||
Output from `git cherry-pick`:
|
||||
|
||||
---
|
||||
|
||||
${{ steps.run_tests.outputs.output }}
|
||||
reactions: confused
|
||||
124
.github/workflows/openapi.yml
vendored
124
.github/workflows/openapi.yml
vendored
@@ -1,124 +0,0 @@
|
||||
name: OpenAPI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
openapi-head:
|
||||
name: OpenAPI - HEAD
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
|
||||
|
||||
openapi-base:
|
||||
name: OpenAPI - BASE
|
||||
if: ${{ github.base_ref != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
|
||||
|
||||
openapi-diff:
|
||||
name: OpenAPI - Difference
|
||||
if: ${{ github.event_name == 'pull_request_target' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- openapi-head
|
||||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
- name: Workaround openapi-diff issue
|
||||
run: |
|
||||
sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
|
||||
sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
|
||||
- name: Calculate OpenAPI difference
|
||||
uses: docker://openapitools/openapi-diff
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
|
||||
- id: read-diff
|
||||
name: Read openapi-diff output
|
||||
run: |
|
||||
body=$(cat openapi-changes.md)
|
||||
body="${body//'%'/'%25'}"
|
||||
body="${body//$'\n'/'%0A'}"
|
||||
body="${body//$'\r'/'%0D'}"
|
||||
echo ::set-output name=body::$body
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
direction: last
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ steps.read-diff.outputs.body != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!--openapi-diff-workflow-comment-->
|
||||
<details>
|
||||
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
|
||||
|
||||
${{ steps.read-diff.outputs.body }}
|
||||
|
||||
</details>
|
||||
- name: Edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!--openapi-diff-workflow-comment-->
|
||||
|
||||
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
||||
27
.github/workflows/repo-stale.yaml
vendored
27
.github/workflows/repo-stale.yaml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Issue Stale Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
days-before-stale: 120
|
||||
days-before-pr-stale: -1
|
||||
days-before-close: 21
|
||||
days-before-pr-close: -1
|
||||
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
|
||||
stale-issue-label: stale
|
||||
stale-issue-message: |-
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
24
.gitignore
vendored
24
.gitignore
vendored
@@ -244,14 +244,14 @@ pip-log.txt
|
||||
#########################
|
||||
|
||||
# Artifacts for debian-x64
|
||||
debian/.debhelper/
|
||||
debian/*.debhelper
|
||||
debian/debhelper-build-stamp
|
||||
debian/files
|
||||
debian/jellyfin.substvars
|
||||
debian/jellyfin/
|
||||
deployment/debian-package-x64/pkg-src/.debhelper/
|
||||
deployment/debian-package-x64/pkg-src/*.debhelper
|
||||
deployment/debian-package-x64/pkg-src/debhelper-build-stamp
|
||||
deployment/debian-package-x64/pkg-src/files
|
||||
deployment/debian-package-x64/pkg-src/jellyfin.substvars
|
||||
deployment/debian-package-x64/pkg-src/jellyfin/
|
||||
# Don't ignore the debian/bin folder
|
||||
!debian/bin/
|
||||
!deployment/debian-package-x64/pkg-src/bin/
|
||||
|
||||
deployment/**/dist/
|
||||
deployment/**/pkg-dist/
|
||||
@@ -268,16 +268,6 @@ doc/
|
||||
# Deployment artifacts
|
||||
dist
|
||||
*.exe
|
||||
*.dll
|
||||
|
||||
# BenchmarkDotNet artifacts
|
||||
BenchmarkDotNet.Artifacts
|
||||
|
||||
# Ignore web artifacts from native builds
|
||||
web/
|
||||
web-src.*
|
||||
MediaBrowser.WebDashboard/jellyfin-web
|
||||
apiclient/generated
|
||||
|
||||
# Omnisharp crash logs
|
||||
mono_crash.*.json
|
||||
|
||||
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": [
|
||||
|
||||
]
|
||||
}
|
||||
32
.vscode/launch.json
vendored
32
.vscode/launch.json
vendored
@@ -1,30 +1,19 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
// 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",
|
||||
"configurations": [
|
||||
{
|
||||
"name": ".NET Core Launch (console)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.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": [],
|
||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||
"console": "internalConsole",
|
||||
"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/net6.0/jellyfin.dll",
|
||||
"args": ["--nowebclient"],
|
||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false,
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
@@ -35,8 +24,5 @@
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}"
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
|
||||
}
|
||||
,]
|
||||
}
|
||||
|
||||
19
.vscode/tasks.json
vendored
19
.vscode/tasks.json
vendored
@@ -10,21 +10,6 @@
|
||||
"${workspaceFolder}/Jellyfin.Server/Jellyfin.Server.csproj"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
P:System.Threading.Tasks.Task`1.Result
|
||||
M:System.Guid.op_Equality(System.Guid,System.Guid)
|
||||
M:System.Guid.op_Inequality(System.Guid,System.Guid)
|
||||
M:System.Guid.Equals(System.Object)
|
||||
@@ -1,6 +1,5 @@
|
||||
# Jellyfin Contributors
|
||||
|
||||
- [1337joe](https://github.com/1337joe)
|
||||
- [97carmine](https://github.com/97carmine)
|
||||
- [Abbe98](https://github.com/Abbe98)
|
||||
- [agrenott](https://github.com/agrenott)
|
||||
@@ -8,8 +7,6 @@
|
||||
- [anthonylavado](https://github.com/anthonylavado)
|
||||
- [Artiume](https://github.com/Artiume)
|
||||
- [AThomsen](https://github.com/AThomsen)
|
||||
- [barongreenback](https://github.com/BaronGreenback)
|
||||
- [barronpm](https://github.com/barronpm)
|
||||
- [bilde2910](https://github.com/bilde2910)
|
||||
- [bfayers](https://github.com/bfayers)
|
||||
- [BnMcG](https://github.com/BnMcG)
|
||||
@@ -18,8 +15,6 @@
|
||||
- [bugfixin](https://github.com/bugfixin)
|
||||
- [chaosinnovator](https://github.com/chaosinnovator)
|
||||
- [ckcr4lyf](https://github.com/ckcr4lyf)
|
||||
- [cocool97](https://github.com/cocool97)
|
||||
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
||||
- [crankdoofus](https://github.com/crankdoofus)
|
||||
- [crobibero](https://github.com/crobibero)
|
||||
- [cromefire](https://github.com/cromefire)
|
||||
@@ -27,7 +22,6 @@
|
||||
- [cvium](https://github.com/cvium)
|
||||
- [dannymichel](https://github.com/dannymichel)
|
||||
- [DaveChild](https://github.com/DaveChild)
|
||||
- [Delgan](https://github.com/Delgan)
|
||||
- [dcrdev](https://github.com/dcrdev)
|
||||
- [dhartung](https://github.com/dhartung)
|
||||
- [dinki](https://github.com/dinki)
|
||||
@@ -46,14 +40,11 @@
|
||||
- [Froghut](https://github.com/Froghut)
|
||||
- [fruhnow](https://github.com/fruhnow)
|
||||
- [geilername](https://github.com/geilername)
|
||||
- [GermanCoding](https://github.com/GermanCoding)
|
||||
- [gnattu](https://github.com/gnattu)
|
||||
- [GodTamIt](https://github.com/GodTamIt)
|
||||
- [grafixeyehero](https://github.com/grafixeyehero)
|
||||
- [h1nk](https://github.com/h1nk)
|
||||
- [hawken93](https://github.com/hawken93)
|
||||
- [HelloWorld017](https://github.com/HelloWorld017)
|
||||
- [ikomhoog](https://github.com/ikomhoog)
|
||||
- [jftuga](https://github.com/jftuga)
|
||||
- [joern-h](https://github.com/joern-h)
|
||||
- [joshuaboniface](https://github.com/joshuaboniface)
|
||||
@@ -63,7 +54,6 @@
|
||||
- [Larvitar](https://github.com/Larvitar)
|
||||
- [LeoVerto](https://github.com/LeoVerto)
|
||||
- [Liggy](https://github.com/Liggy)
|
||||
- [lmaonator](https://github.com/lmaonator)
|
||||
- [LogicalPhallacy](https://github.com/LogicalPhallacy)
|
||||
- [loli10K](https://github.com/loli10K)
|
||||
- [lostmypillow](https://github.com/lostmypillow)
|
||||
@@ -73,12 +63,10 @@
|
||||
- [marius-luca-87](https://github.com/marius-luca-87)
|
||||
- [mark-monteiro](https://github.com/mark-monteiro)
|
||||
- [Matt07211](https://github.com/Matt07211)
|
||||
- [Maxr1998](https://github.com/Maxr1998)
|
||||
- [mcarlton00](https://github.com/mcarlton00)
|
||||
- [mitchfizz05](https://github.com/mitchfizz05)
|
||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||
- [n8225](https://github.com/n8225)
|
||||
- [Nalsai](https://github.com/Nalsai)
|
||||
- [Narfinger](https://github.com/Narfinger)
|
||||
- [NathanPickard](https://github.com/NathanPickard)
|
||||
- [neilsb](https://github.com/neilsb)
|
||||
@@ -86,10 +74,7 @@
|
||||
- [Nickbert7](https://github.com/Nickbert7)
|
||||
- [nvllsvm](https://github.com/nvllsvm)
|
||||
- [nyanmisaka](https://github.com/nyanmisaka)
|
||||
- [OancaAndrei](https://github.com/OancaAndrei)
|
||||
- [obradovichv](https://github.com/obradovichv)
|
||||
- [oddstr13](https://github.com/oddstr13)
|
||||
- [orryverducci](https://github.com/orryverducci)
|
||||
- [petermcneil](https://github.com/petermcneil)
|
||||
- [Phlogi](https://github.com/Phlogi)
|
||||
- [pjeanjean](https://github.com/pjeanjean)
|
||||
@@ -106,19 +91,14 @@
|
||||
- [samuel9554](https://github.com/samuel9554)
|
||||
- [scheidleon](https://github.com/scheidleon)
|
||||
- [sebPomme](https://github.com/sebPomme)
|
||||
- [SegiH](https://github.com/SegiH)
|
||||
- [SenorSmartyPants](https://github.com/SenorSmartyPants)
|
||||
- [shemanaev](https://github.com/shemanaev)
|
||||
- [skaro13](https://github.com/skaro13)
|
||||
- [sl1288](https://github.com/sl1288)
|
||||
- [Smith00101010](https://github.com/Smith00101010)
|
||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||
- [sparky8251](https://github.com/sparky8251)
|
||||
- [spookbits](https://github.com/spookbits)
|
||||
- [ssenart](https://github.com/ssenart)
|
||||
- [stanionascu](https://github.com/stanionascu)
|
||||
- [stevehayles](https://github.com/stevehayles)
|
||||
- [StollD](https://github.com/StollD)
|
||||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||
- [tbraeutigam](https://github.com/tbraeutigam)
|
||||
- [teacupx](https://github.com/teacupx)
|
||||
@@ -147,16 +127,6 @@
|
||||
- [xosdy](https://github.com/xosdy)
|
||||
- [XVicarious](https://github.com/XVicarious)
|
||||
- [YouKnowBlom](https://github.com/YouKnowBlom)
|
||||
- [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)
|
||||
- [peterspenler](https://github.com/peterspenler)
|
||||
- [MBR-0001](https://github.com/MBR-0001)
|
||||
- [jonas-resch](https://github.com/jonas-resch)
|
||||
- [vgambier](https://github.com/vgambier)
|
||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
@@ -220,8 +190,3 @@
|
||||
- [tikuf](https://github.com/tikuf/)
|
||||
- [Tim Hobbs](https://github.com/timhobbs)
|
||||
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
||||
- [olsh](https://github.com/olsh)
|
||||
- [lbenini](https://github.com/lbenini)
|
||||
- [gnuyent](https://github.com/gnuyent)
|
||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||
- [Jakob Kukla](https://github.com/jakobkukla)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<Project>
|
||||
<!-- Sets defaults for all projects in the repo -->
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="$(SolutionDir)/BannedSymbols.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
106
Dockerfile
106
Dockerfile
@@ -1,18 +1,25 @@
|
||||
# DESIGNED FOR BUILDING ON AMD64 ONLY
|
||||
#####################################
|
||||
# Requires binfm_misc registration
|
||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=6.0
|
||||
ARG DOTNET_VERSION=3.1
|
||||
ARG FFMPEG_VERSION=latest
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
FROM node:alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=10.5.2
|
||||
RUN apk add curl git \
|
||||
&& git clone --branch release-10.5.z --single-branch https://github.com/jellyfin/jellyfin-web.git \
|
||||
&& cd jellyfin-web \
|
||||
&& yarn install \
|
||||
&& yarn build \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM debian:stable-slim as app
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# 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
|
||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
|
||||
|
||||
FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg
|
||||
FROM debian:buster-slim
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
@@ -21,72 +28,37 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases
|
||||
ARG GMMLIB_VERSION=22.0.2
|
||||
ARG IGC_VERSION=1.0.10395
|
||||
ARG NEO_VERSION=22.08.22549
|
||||
ARG LEVEL_ZERO_VERSION=1.3.22549
|
||||
|
||||
COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
# Install dependencies:
|
||||
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
|
||||
# curl: healthcheck
|
||||
# libfontconfig1: needed for Skia
|
||||
# libgomp1: needed for ffmpeg
|
||||
# libva-drm2: needed for ffmpeg
|
||||
# mesa-va-drivers: needed for VAAPI
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https curl \
|
||||
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
|
||||
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
libfontconfig1 \
|
||||
libgomp1 \
|
||||
libva-drm2 \
|
||||
mesa-va-drivers \
|
||||
jellyfin-ffmpeg \
|
||||
openssl \
|
||||
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-icd_${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 clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
ca-certificates \
|
||||
vainfo \
|
||||
i965-va-driver \
|
||||
&& apt-get clean autoclean -y\
|
||||
&& apt-get autoremove -y\
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /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
|
||||
&& ln -s /opt/ffmpeg/bin/ffmpeg /usr/local/bin \
|
||||
&& ln -s /opt/ffmpeg/bin/ffprobe /usr/local/bin
|
||||
|
||||
# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# 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
|
||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
|
||||
|
||||
FROM app
|
||||
|
||||
ENV HEALTHCHECK_URL=http://localhost:8096/health
|
||||
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config
|
||||
VOLUME /cache /config /media
|
||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
||||
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
|
||||
"--ffmpeg", "/usr/local/bin/ffmpeg"]
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
# DESIGNED FOR BUILDING ON ARM ONLY
|
||||
# DESIGNED FOR BUILDING ON AMD64 ONLY
|
||||
#####################################
|
||||
# Requires binfm_misc registration
|
||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=6.0
|
||||
ARG DOTNET_VERSION=3.1
|
||||
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
FROM node:alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=10.5.2
|
||||
RUN apk add curl git \
|
||||
&& git clone --branch release-10.5.z --single-branch https://github.com/jellyfin/jellyfin-web.git \
|
||||
&& cd jellyfin-web \
|
||||
&& yarn install \
|
||||
&& yarn build \
|
||||
&& mv dist /dist
|
||||
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
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 arm32v7/debian:stable-slim as app
|
||||
FROM arm32v7/debian:buster-slim
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
@@ -24,8 +36,6 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||
|
||||
# curl: setup & healthcheck
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
|
||||
curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
|
||||
@@ -43,42 +53,20 @@ RUN apt-get update \
|
||||
libraspberrypi0 \
|
||||
vainfo \
|
||||
libva2 \
|
||||
locales \
|
||||
&& apt-get remove gnupg -y \
|
||||
&& apt-get remove curl gnupg -y \
|
||||
&& apt-get clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /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
|
||||
|
||||
# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
|
||||
|
||||
FROM app
|
||||
|
||||
ENV HEALTHCHECK_URL=http://localhost:8096/health
|
||||
|
||||
&& chmod 777 /cache /config /media
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config
|
||||
VOLUME /cache /config /media
|
||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
||||
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg"]
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
# DESIGNED FOR BUILDING ON ARM64 ONLY
|
||||
# DESIGNED FOR BUILDING ON AMD64 ONLY
|
||||
#####################################
|
||||
# Requires binfm_misc registration
|
||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=6.0
|
||||
ARG DOTNET_VERSION=3.1
|
||||
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
FROM node:alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=10.5.2
|
||||
RUN apk add curl git \
|
||||
&& git clone --branch release-10.5.z --single-branch https://github.com/jellyfin/jellyfin-web.git \
|
||||
&& cd jellyfin-web \
|
||||
&& yarn install \
|
||||
&& yarn build \
|
||||
&& mv dist /dist
|
||||
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
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 arm64v8/debian:stable-slim as app
|
||||
FROM arm64v8/debian:buster-slim
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
@@ -24,9 +35,7 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
|
||||
|
||||
# curl: healcheck
|
||||
RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
ffmpeg \
|
||||
libssl-dev \
|
||||
ca-certificates \
|
||||
@@ -34,42 +43,19 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
|
||||
libfreetype6 \
|
||||
libomxil-bellagio0 \
|
||||
libomxil-bellagio-bin \
|
||||
locales \
|
||||
curl \
|
||||
&& apt-get clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /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
|
||||
|
||||
# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
|
||||
|
||||
FROM app
|
||||
|
||||
ENV HEALTHCHECK_URL=http://localhost:8096/health
|
||||
|
||||
&& chmod 777 /cache /config /media
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config
|
||||
VOLUME /cache /config /media
|
||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
"--ffmpeg", "/usr/bin/ffmpeg"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
||||
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace DvdLib
|
||||
@@ -14,12 +12,19 @@ namespace DvdLib
|
||||
|
||||
public override ushort ReadUInt16()
|
||||
{
|
||||
return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2));
|
||||
return BitConverter.ToUInt16(ReadAndReverseBytes(2), 0);
|
||||
}
|
||||
|
||||
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,20 +1,17 @@
|
||||
<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>
|
||||
<Compile Include="..\SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace DvdLib.Ifo
|
||||
@@ -7,7 +5,6 @@ namespace DvdLib.Ifo
|
||||
public class Cell
|
||||
{
|
||||
public CellPlaybackInfo PlaybackInfo { get; private set; }
|
||||
|
||||
public CellPositionInfo PositionInfo { get; private set; }
|
||||
|
||||
internal void ParsePlayback(BinaryReader br)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace DvdLib.Ifo
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace DvdLib.Ifo
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace DvdLib.Ifo
|
||||
{
|
||||
public class Chapter
|
||||
{
|
||||
public ushort ProgramChainNumber { get; private set; }
|
||||
|
||||
public ushort ProgramNumber { get; private set; }
|
||||
|
||||
public uint ChapterNumber { get; private set; }
|
||||
|
||||
public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace DvdLib.Ifo
|
||||
{
|
||||
@@ -15,10 +13,13 @@ namespace DvdLib.Ifo
|
||||
|
||||
private ushort _titleCount;
|
||||
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>();
|
||||
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)) ??
|
||||
allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase));
|
||||
@@ -32,7 +33,7 @@ namespace DvdLib.Ifo
|
||||
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))
|
||||
{
|
||||
ReadVTS(ifoNumber, ifo.FullName);
|
||||
@@ -75,9 +76,9 @@ namespace DvdLib.Ifo
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
|
||||
private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles)
|
||||
{
|
||||
var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
|
||||
var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
|
||||
|
||||
var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
|
||||
allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
|
||||
@@ -118,19 +119,12 @@ namespace DvdLib.Ifo
|
||||
uint chapNum = 1;
|
||||
vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
|
||||
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
|
||||
if (t == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (t == null) continue;
|
||||
|
||||
do
|
||||
{
|
||||
t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum));
|
||||
if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1])) break;
|
||||
chapNum++;
|
||||
}
|
||||
while (vtsFs.Position < (baseAddr + endaddr));
|
||||
@@ -155,10 +149,7 @@ namespace DvdLib.Ifo
|
||||
uint vtsPgcOffset = vtsRead.ReadUInt32();
|
||||
|
||||
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum));
|
||||
if (t != null)
|
||||
{
|
||||
t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
|
||||
}
|
||||
if (t != null) t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace DvdLib.Ifo
|
||||
@@ -15,14 +13,8 @@ namespace DvdLib.Ifo
|
||||
Second = GetBCDValue(data[2]);
|
||||
Frames = GetBCDValue((byte)(data[3] & 0x3F));
|
||||
|
||||
if ((data[3] & 0x80) != 0)
|
||||
{
|
||||
FrameRate = 30;
|
||||
}
|
||||
else if ((data[3] & 0x40) != 0)
|
||||
{
|
||||
FrameRate = 25;
|
||||
}
|
||||
if ((data[3] & 0x80) != 0) FrameRate = 30;
|
||||
else if ((data[3] & 0x40) != 0) FrameRate = 25;
|
||||
}
|
||||
|
||||
private static byte GetBCDValue(byte data)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DvdLib.Ifo
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public IReadOnlyList<Cell> Cells { get; }
|
||||
public readonly List<Cell> Cells;
|
||||
|
||||
public Program(List<Cell> cells)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -22,9 +20,7 @@ namespace DvdLib.Ifo
|
||||
public readonly List<Cell> Cells;
|
||||
|
||||
public DvdTime PlaybackTime { get; private set; }
|
||||
|
||||
public UserOperation ProhibitedUserOperations { get; private set; }
|
||||
|
||||
public byte[] AudioStreamControl { get; private set; } // 8*2 entries
|
||||
public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
|
||||
|
||||
@@ -35,11 +31,9 @@ namespace DvdLib.Ifo
|
||||
private ushort _goupProgramNumber;
|
||||
|
||||
public ProgramPlaybackMode PlaybackMode { get; private set; }
|
||||
|
||||
public uint ProgramCount { get; private set; }
|
||||
|
||||
public byte StillTime { get; private set; }
|
||||
|
||||
public byte[] Palette { get; private set; } // 16*4 entries
|
||||
|
||||
private ushort _commandTableOffset;
|
||||
@@ -75,15 +69,8 @@ namespace DvdLib.Ifo
|
||||
|
||||
StillTime = br.ReadByte();
|
||||
byte pbMode = br.ReadByte();
|
||||
if (pbMode == 0)
|
||||
{
|
||||
PlaybackMode = ProgramPlaybackMode.Sequential;
|
||||
}
|
||||
else
|
||||
{
|
||||
PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
|
||||
}
|
||||
|
||||
if (pbMode == 0) PlaybackMode = ProgramPlaybackMode.Sequential;
|
||||
else PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
|
||||
ProgramCount = (uint)(pbMode & 0x7F);
|
||||
|
||||
Palette = br.ReadBytes(64);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
@@ -8,11 +6,8 @@ namespace DvdLib.Ifo
|
||||
public class Title
|
||||
{
|
||||
public uint TitleNumber { get; private set; }
|
||||
|
||||
public uint AngleCount { get; private set; }
|
||||
|
||||
public ushort ChapterCount { get; private set; }
|
||||
|
||||
public byte VideoTitleSetNumber { get; private set; }
|
||||
|
||||
private ushort _parentalManagementMask;
|
||||
@@ -20,7 +15,6 @@ namespace DvdLib.Ifo
|
||||
private uint _vtsStartSector; // relative to start of entire disk
|
||||
|
||||
public ProgramChain EntryProgramChain { get; private set; }
|
||||
|
||||
public readonly List<ProgramChain> ProgramChains;
|
||||
|
||||
public readonly List<Chapter> Chapters;
|
||||
@@ -59,10 +53,7 @@ namespace DvdLib.Ifo
|
||||
var pgc = new ProgramChain(pgcNum);
|
||||
pgc.ParseHeader(br);
|
||||
ProgramChains.Add(pgc);
|
||||
if (entryPgc)
|
||||
{
|
||||
EntryProgramChain = pgc;
|
||||
}
|
||||
if (entryPgc) EntryProgramChain = pgc;
|
||||
|
||||
br.BaseStream.Seek(curPos, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// DLNA Query parameter type, used when querying DLNA devices via SOAP.
|
||||
/// </summary>
|
||||
public class Argument
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets name of the DLNA argument.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the direction of the parameter.
|
||||
/// </summary>
|
||||
public string Direction { get; set; } = string.Empty;
|
||||
public string Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the related DLNA state variable for this argument.
|
||||
/// </summary>
|
||||
public string RelatedStateVariable { get; set; } = string.Empty;
|
||||
public string RelatedStateVariable { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,30 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="DeviceIcon" />.
|
||||
/// </summary>
|
||||
public class DeviceIcon
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Url.
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MimeType.
|
||||
/// </summary>
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
public string MimeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Width.
|
||||
/// </summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Height.
|
||||
/// </summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Depth.
|
||||
/// </summary>
|
||||
public string Depth { get; set; } = string.Empty;
|
||||
public string Depth { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="DeviceService" />.
|
||||
/// </summary>
|
||||
public class DeviceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Service Type.
|
||||
/// </summary>
|
||||
public string ServiceType { get; set; } = string.Empty;
|
||||
public string ServiceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Service Id.
|
||||
/// </summary>
|
||||
public string ServiceId { get; set; } = string.Empty;
|
||||
public string ServiceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Scpd Url.
|
||||
/// </summary>
|
||||
public string ScpdUrl { get; set; } = string.Empty;
|
||||
public string ScpdUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Control Url.
|
||||
/// </summary>
|
||||
public string ControlUrl { get; set; } = string.Empty;
|
||||
public string ControlUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the EventSubUrl.
|
||||
/// </summary>
|
||||
public string EventSubUrl { get; set; } = string.Empty;
|
||||
public string EventSubUrl { get; set; }
|
||||
|
||||
/// <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;
|
||||
|
||||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceAction" />.
|
||||
/// </summary>
|
||||
public class ServiceAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServiceAction"/> class.
|
||||
/// </summary>
|
||||
public ServiceAction()
|
||||
{
|
||||
ArgumentList = new List<Argument>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the action.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ArgumentList.
|
||||
/// </summary>
|
||||
public List<Argument> ArgumentList { get; }
|
||||
public List<Argument> ArgumentList { get; set; }
|
||||
|
||||
/// <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.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="StateVariable" />.
|
||||
/// </summary>
|
||||
public class StateVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the state variable.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public StateVariable()
|
||||
{
|
||||
AllowedValues = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the data type of the state variable.
|
||||
/// </summary>
|
||||
public string DataType { get; set; } = string.Empty;
|
||||
public string Name { get; set; }
|
||||
|
||||
public string DataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether it sends events.
|
||||
/// </summary>
|
||||
public bool SendsEvents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed values range.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> AllowedValues { get; set; } = Array.Empty<string>();
|
||||
public string[] AllowedValues { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Name;
|
||||
public override string ToString()
|
||||
=> Name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +1,34 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
namespace Emby.Dlna.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// The DlnaOptions class contains the user definable parameters for the dlna subsystems.
|
||||
/// </summary>
|
||||
public class DlnaOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DlnaOptions"/> class.
|
||||
/// </summary>
|
||||
public DlnaOptions()
|
||||
{
|
||||
EnablePlayTo = true;
|
||||
EnableServer = false;
|
||||
EnableServer = true;
|
||||
BlastAliveMessages = true;
|
||||
SendOnlyMatchedHost = true;
|
||||
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; }
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool EnablePlayToTracing { get; set; }
|
||||
public bool BlastAliveMessages { get; set; }
|
||||
|
||||
public bool SendOnlyMatchedHost { 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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the frequency at which ssdp alive notifications are transmitted.
|
||||
/// </summary>
|
||||
public int AliveMessageIntervalSeconds { get; set; }
|
||||
public int BlastAliveMessageIntervalSeconds { 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; }
|
||||
|
||||
/// <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;
|
||||
public string DefaultUserId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
@@ -12,4 +14,19 @@ namespace Emby.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 SA1600
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
@@ -6,57 +7,45 @@ using Emby.Dlna.Service;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ConnectionManagerXmlBuilder" />.
|
||||
/// </summary>
|
||||
public static class ConnectionManagerXmlBuilder
|
||||
public class ConnectionManagerXmlBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// 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()
|
||||
public 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()
|
||||
{
|
||||
var list = new List<StateVariable>
|
||||
var list = new List<StateVariable>();
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SourceProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
},
|
||||
Name = "SourceProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SinkProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "SinkProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "CurrentConnectionIDs",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "CurrentConnectionIDs",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionStatus",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionStatus",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new[]
|
||||
AllowedValues = new string[]
|
||||
{
|
||||
"OK",
|
||||
"ContentFormatMismatch",
|
||||
@@ -64,56 +53,55 @@ namespace Emby.Dlna.ConnectionManager
|
||||
"UnreliableChannel",
|
||||
"Unknown"
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionManager",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionManager",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Direction",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Direction",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new[]
|
||||
AllowedValues = new string[]
|
||||
{
|
||||
"Output",
|
||||
"Input"
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_AVTransportID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_AVTransportID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RcsID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RcsID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -11,19 +12,10 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ControlHandler" />.
|
||||
/// </summary>
|
||||
public class ControlHandler : BaseControlHandler
|
||||
{
|
||||
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)
|
||||
: base(config, logger)
|
||||
{
|
||||
@@ -31,7 +23,7 @@ namespace Emby.Dlna.ConnectionManager
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
{
|
||||
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -42,10 +34,6 @@ namespace Emby.Dlna.ConnectionManager
|
||||
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)
|
||||
{
|
||||
xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
||||
/// </summary>
|
||||
public static class ServiceActionListBuilder
|
||||
public class ServiceActionListBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns an enumerable of the ConnectionManagar:1 DLNA actions.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
||||
public static IEnumerable<ServiceAction> GetActions()
|
||||
public IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
var list = new List<ServiceAction>
|
||||
{
|
||||
@@ -28,10 +22,6 @@ namespace Emby.Dlna.ConnectionManager
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "PrepareForConnection".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction PrepareForConnection()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -91,10 +81,6 @@ namespace Emby.Dlna.ConnectionManager
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetCurrentConnectionInfo".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetCurrentConnectionInfo()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -161,11 +147,7 @@ namespace Emby.Dlna.ConnectionManager
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetProtocolInfo".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetProtocolInfo()
|
||||
private ServiceAction GetProtocolInfo()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
@@ -189,11 +171,7 @@ namespace Emby.Dlna.ConnectionManager
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetCurrentConnectionIDs".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetCurrentConnectionIDs()
|
||||
private ServiceAction GetCurrentConnectionIDs()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
@@ -210,11 +188,7 @@ namespace Emby.Dlna.ConnectionManager
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "ConnectionComplete".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction ConnectionComplete()
|
||||
private ServiceAction ConnectionComplete()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Service;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.TV;
|
||||
@@ -19,10 +18,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ContentDirectoryService" />.
|
||||
/// </summary>
|
||||
public class ContentDirectoryService : BaseService, IContentDirectory
|
||||
public class ContentDirectory : BaseService, IContentDirectory
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
@@ -36,31 +32,14 @@ namespace Emby.Dlna.ContentDirectory
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ITVSeriesManager _tvSeriesManager;
|
||||
|
||||
/// <summary>
|
||||
/// 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,
|
||||
public ContentDirectory(IDlnaManager dlna,
|
||||
IUserDataManager userDataManager,
|
||||
IImageProcessor imageProcessor,
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager,
|
||||
ILogger<ContentDirectoryService> logger,
|
||||
IHttpClientFactory httpClient,
|
||||
ILogger logger,
|
||||
IHttpClient httpClient,
|
||||
ILocalizationManager localization,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IUserViewManager userViewManager,
|
||||
@@ -81,10 +60,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
_tvSeriesManager = tvSeriesManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system id. (A unique id which changes on when our definition changes.)
|
||||
/// </summary>
|
||||
private static int SystemUpdateId
|
||||
private int SystemUpdateId
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -97,18 +73,14 @@ namespace Emby.Dlna.ContentDirectory
|
||||
/// <inheritdoc />
|
||||
public string GetServiceXml()
|
||||
{
|
||||
return ContentDirectoryXmlBuilder.GetXml();
|
||||
return new ContentDirectoryXmlBuilder().GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile();
|
||||
var profile = _dlna.GetProfile(request.Headers) ??
|
||||
_dlna.GetDefaultProfile();
|
||||
|
||||
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@@ -133,12 +105,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
.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))
|
||||
{
|
||||
@@ -164,13 +131,18 @@ namespace Emby.Dlna.ContentDirectory
|
||||
|
||||
foreach (var user in _userManager.Users)
|
||||
{
|
||||
if (user.HasPermission(PermissionKind.IsAdministrator))
|
||||
if (user.Policy.IsAdministrator)
|
||||
{
|
||||
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 SA1600
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
@@ -6,154 +7,142 @@ using Emby.Dlna.Service;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ContentDirectoryXmlBuilder" />.
|
||||
/// </summary>
|
||||
public static class ContentDirectoryXmlBuilder
|
||||
public class ContentDirectoryXmlBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// 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()
|
||||
public 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()
|
||||
{
|
||||
var list = new List<StateVariable>
|
||||
var list = new List<StateVariable>();
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Filter",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
Name = "A_ARG_TYPE_Filter",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_SortCriteria",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_SortCriteria",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Index",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Index",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Count",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Count",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_UpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_UpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SearchCapabilities",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "SearchCapabilities",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SortCapabilities",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "SortCapabilities",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SystemUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "SystemUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_SearchCriteria",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_SearchCriteria",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ObjectID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ObjectID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_BrowseFlag",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_BrowseFlag",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new[]
|
||||
AllowedValues = new string[]
|
||||
{
|
||||
"BrowseMetadata",
|
||||
"BrowseDirectChildren"
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_BrowseLetter",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_BrowseLetter",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_CategoryType",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_CategoryType",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_PosSec",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_PosSec",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Featurelist",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Featurelist",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
||||
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>
|
||||
/// <param name="stubType">The stub type.</param>
|
||||
public ServerItem(BaseItem item, StubType? stubType)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
if (stubType.HasValue)
|
||||
{
|
||||
StubType = stubType;
|
||||
}
|
||||
else if (item is IItemByName and not Folder)
|
||||
{
|
||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying base item.
|
||||
/// </summary>
|
||||
public BaseItem Item { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DLNA item type.
|
||||
/// </summary>
|
||||
public StubType? StubType { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
||||
/// </summary>
|
||||
public static class ServiceActionListBuilder
|
||||
public class ServiceActionListBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a list of services that this instance provides.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
||||
public static IEnumerable<ServiceAction> GetActions()
|
||||
public IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
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()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -48,10 +40,6 @@ namespace Emby.Dlna.ContentDirectory
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetSearchCapabilities".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetSearchCapabilitiesAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -69,10 +57,6 @@ namespace Emby.Dlna.ContentDirectory
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetSortCapabilities".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetSortCapabilitiesAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -90,10 +74,6 @@ namespace Emby.Dlna.ContentDirectory
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "X_GetFeatureList".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetX_GetFeatureListAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -111,10 +91,6 @@ namespace Emby.Dlna.ContentDirectory
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "Search".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetSearchAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -195,11 +171,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "Browse".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetBrowseAction()
|
||||
private ServiceAction GetBrowseAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
@@ -279,11 +251,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "X_BrowseByLetter".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetBrowseByLetterAction()
|
||||
private ServiceAction GetBrowseByLetterAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
@@ -370,11 +338,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "X_SetBookmark".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetXSetBookmarkAction()
|
||||
private ServiceAction GetXSetBookmarkAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
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,6 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -9,17 +8,17 @@ namespace Emby.Dlna
|
||||
{
|
||||
public class ControlRequest
|
||||
{
|
||||
public ControlRequest(IHeaderDictionary headers)
|
||||
{
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
public IHeaderDictionary Headers { get; }
|
||||
public IHeaderDictionary Headers { get; set; }
|
||||
|
||||
public Stream InputXml { get; set; }
|
||||
|
||||
public string TargetServerUuId { get; set; }
|
||||
|
||||
public string RequestedUrl { get; set; }
|
||||
|
||||
public ControlRequest()
|
||||
{
|
||||
Headers = new HeaderDictionary();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -6,23 +7,15 @@ namespace Emby.Dlna
|
||||
{
|
||||
public class ControlResponse
|
||||
{
|
||||
public ControlResponse(string xml, bool isSuccessful)
|
||||
public ControlResponse()
|
||||
{
|
||||
Headers = new Dictionary<string, string>();
|
||||
Xml = xml;
|
||||
IsSuccessful = isSuccessful;
|
||||
}
|
||||
|
||||
public IDictionary<string, string> Headers { get; }
|
||||
public IDictionary<string, string> Headers { get; set; }
|
||||
|
||||
public string Xml { get; set; }
|
||||
|
||||
public bool IsSuccessful { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Xml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
|
||||
namespace Emby.Dlna.Didl
|
||||
{
|
||||
@@ -12,17 +14,21 @@ namespace Emby.Dlna.Didl
|
||||
public Filter()
|
||||
: this("*")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Filter(string filter)
|
||||
{
|
||||
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
||||
_fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
_fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
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 CA1305
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
@@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
|
||||
{
|
||||
public class StringWriterWithEncoding : StringWriter
|
||||
{
|
||||
private readonly Encoding? _encoding;
|
||||
private readonly Encoding _encoding;
|
||||
|
||||
public StringWriterWithEncoding()
|
||||
{
|
||||
@@ -30,6 +30,7 @@ namespace Emby.Dlna.Didl
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public StringWriterWithEncoding(Encoding encoding)
|
||||
{
|
||||
_encoding = encoding;
|
||||
@@ -53,6 +54,6 @@ namespace Emby.Dlna.Didl
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
||||
public override Encoding Encoding => _encoding ?? base.Encoding;
|
||||
public override Encoding Encoding => (null == _encoding) ? base.Encoding : _encoding;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#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,16 +1,17 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Profiles;
|
||||
using Emby.Dlna.Server;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
@@ -31,10 +32,10 @@ namespace Emby.Dlna
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<DlnaManager> _logger;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
|
||||
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
||||
|
||||
@@ -43,24 +44,22 @@ namespace Emby.Dlna
|
||||
IFileSystem fileSystem,
|
||||
IApplicationPaths appPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IServerApplicationHost appHost)
|
||||
{
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
_logger = loggerFactory.CreateLogger<DlnaManager>();
|
||||
_logger = loggerFactory.CreateLogger("Dlna");
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
|
||||
|
||||
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
|
||||
|
||||
public async Task InitProfilesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ExtractSystemProfilesAsync().ConfigureAwait(false);
|
||||
await ExtractSystemProfilesAsync();
|
||||
LoadProfiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -83,22 +82,22 @@ namespace Emby.Dlna
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
return _profiles.Values
|
||||
var list = _profiles.Values.ToList();
|
||||
return list
|
||||
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Item1.Info.Name)
|
||||
.Select(i => i.Item2)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile GetDefaultProfile()
|
||||
{
|
||||
return new DefaultProfile();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
|
||||
public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
|
||||
{
|
||||
if (deviceInfo == null)
|
||||
{
|
||||
@@ -108,57 +107,100 @@ namespace Emby.Dlna
|
||||
var profile = GetProfiles()
|
||||
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
|
||||
|
||||
if (profile == null)
|
||||
if (profile != null)
|
||||
{
|
||||
_logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
|
||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||
LogUnmatchedProfile(deviceInfo);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to match a device with a profile.
|
||||
/// Rules:
|
||||
/// - If the profile field has no value, the field matches irregardless of its contents.
|
||||
/// - the profile field can be an exact match, or a reg exp.
|
||||
/// </summary>
|
||||
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
|
||||
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
|
||||
/// <returns><b>True</b> if they match.</returns>
|
||||
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||
private void LogUnmatchedProfile(DeviceIdentification profile)
|
||||
{
|
||||
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
||||
builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
|
||||
builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
|
||||
builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
|
||||
builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
|
||||
|
||||
_logger.LogInformation(builder.ToString());
|
||||
}
|
||||
|
||||
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
||||
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
|
||||
{
|
||||
// In profile identification: An empty pattern matches anything.
|
||||
return true;
|
||||
if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(input))
|
||||
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
|
||||
{
|
||||
// The profile contains a value, and the device doesn't.
|
||||
return false;
|
||||
if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
|
||||
{
|
||||
if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
|
||||
{
|
||||
if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
|
||||
{
|
||||
if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelName))
|
||||
{
|
||||
if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
|
||||
{
|
||||
if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
|
||||
{
|
||||
if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
|
||||
{
|
||||
if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsRegexMatch(string input, string pattern)
|
||||
{
|
||||
try
|
||||
{
|
||||
return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|
||||
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
return Regex.IsMatch(input, pattern);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
@@ -167,8 +209,7 @@ namespace Emby.Dlna
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(IHeaderDictionary headers)
|
||||
public DeviceProfile GetProfile(IHeaderDictionary headers)
|
||||
{
|
||||
if (headers == null)
|
||||
{
|
||||
@@ -176,13 +217,15 @@ namespace Emby.Dlna
|
||||
}
|
||||
|
||||
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
|
||||
if (profile == null)
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
_logger.LogDebug("No matching device profile found. {@Headers}", headers);
|
||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||
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);
|
||||
}
|
||||
|
||||
return profile;
|
||||
@@ -209,7 +252,7 @@ namespace Emby.Dlna
|
||||
return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
|
||||
case HeaderMatchType.Substring:
|
||||
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;
|
||||
case HeaderMatchType.Regex:
|
||||
return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
|
||||
@@ -221,35 +264,45 @@ namespace Emby.Dlna
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _fileSystem.GetFilePaths(path)
|
||||
var xmlFies = _fileSystem.GetFilePaths(path)
|
||||
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
return xmlFies
|
||||
.Select(i => ParseProfileFile(i, type))
|
||||
.Where(i => i != null)
|
||||
.ToList()!; // We just filtered out all the nulls
|
||||
.ToList();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return Array.Empty<DeviceProfile>();
|
||||
return new List<DeviceProfile>();
|
||||
}
|
||||
}
|
||||
|
||||
private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
|
||||
private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
|
||||
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple))
|
||||
{
|
||||
return profileTuple.Item2;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DeviceProfile profile;
|
||||
|
||||
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
|
||||
var profile = ReserializeProfile(tempProfile);
|
||||
|
||||
profile = ReserializeProfile(tempProfile);
|
||||
|
||||
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -266,20 +319,14 @@ namespace Emby.Dlna
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(string id)
|
||||
public DeviceProfile GetProfile(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return ParseProfileFile(info.Path, info.Info.Type);
|
||||
}
|
||||
@@ -288,14 +335,14 @@ namespace Emby.Dlna
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
return _profiles.Values
|
||||
var list = _profiles.Values.ToList();
|
||||
return list
|
||||
.Select(i => i.Item1)
|
||||
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Info.Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
|
||||
{
|
||||
return GetProfileInfosInternal().Select(i => i.Info);
|
||||
@@ -303,14 +350,17 @@ namespace Emby.Dlna
|
||||
|
||||
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
|
||||
{
|
||||
return new InternalProfileInfo(
|
||||
new DeviceProfileInfo
|
||||
return new InternalProfileInfo
|
||||
{
|
||||
Path = file.FullName,
|
||||
|
||||
Info = new DeviceProfileInfo
|
||||
{
|
||||
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
|
||||
Name = _fileSystem.GetFileNameWithoutExtension(file),
|
||||
Type = type
|
||||
},
|
||||
file.FullName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ExtractSystemProfilesAsync()
|
||||
@@ -321,32 +371,26 @@ namespace Emby.Dlna
|
||||
|
||||
foreach (var name in _assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
|
||||
if (!name.StartsWith(namespaceName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = Path.Join(
|
||||
systemProfilesPath,
|
||||
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
|
||||
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
|
||||
|
||||
// The stream should exist as we just got its name from GetManifestResourceNames
|
||||
using (var stream = _assembly.GetManifestResourceStream(name)!)
|
||||
var path = Path.Combine(systemProfilesPath, filename);
|
||||
|
||||
using (var stream = _assembly.GetManifestResourceStream(name))
|
||||
{
|
||||
var length = stream.Length;
|
||||
var fileInfo = _fileSystem.GetFileInfo(path);
|
||||
|
||||
if (!fileInfo.Exists || fileInfo.Length != length)
|
||||
if (!fileInfo.Exists || fileInfo.Length != stream.Length)
|
||||
{
|
||||
Directory.CreateDirectory(systemProfilesPath);
|
||||
|
||||
var fileOptions = AsyncFile.WriteOptions;
|
||||
fileOptions.Mode = FileMode.Create;
|
||||
fileOptions.PreallocationSize = length;
|
||||
var fileStream = new FileStream(path, fileOptions);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||
{
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
await stream.CopyToAsync(fileStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,7 +400,6 @@ namespace Emby.Dlna
|
||||
Directory.CreateDirectory(UserProfilesPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteProfile(string id)
|
||||
{
|
||||
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
|
||||
@@ -374,7 +417,6 @@ namespace Emby.Dlna
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateProfile(DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
@@ -390,8 +432,7 @@ namespace Emby.Dlna
|
||||
SaveProfile(profile, path, DeviceProfileType.User);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateProfile(string profileId, DeviceProfile profile)
|
||||
public void UpdateProfile(DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
|
||||
@@ -399,13 +440,12 @@ namespace Emby.Dlna
|
||||
{
|
||||
throw new ArgumentException("Profile is missing Id");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(profile.Name))
|
||||
{
|
||||
throw new ArgumentException("Profile is missing Name");
|
||||
}
|
||||
|
||||
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
|
||||
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
|
||||
var path = Path.Combine(UserProfilesPath, newFilename);
|
||||
@@ -425,7 +465,6 @@ namespace Emby.Dlna
|
||||
{
|
||||
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
|
||||
}
|
||||
|
||||
SerializeToXml(profile, path);
|
||||
}
|
||||
|
||||
@@ -436,10 +475,10 @@ namespace Emby.Dlna
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <param name="profile">The device profile.</param>
|
||||
/// <returns>The re-serialized device profile.</returns>
|
||||
/// <param name="profile"></param>
|
||||
/// <returns></returns>
|
||||
private DeviceProfile ReserializeProfile(DeviceProfile profile)
|
||||
{
|
||||
if (profile.GetType() == typeof(DeviceProfile))
|
||||
@@ -447,56 +486,42 @@ namespace Emby.Dlna
|
||||
return profile;
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(profile, _jsonOptions);
|
||||
var json = _jsonSerializer.SerializeToString(profile);
|
||||
|
||||
// Output can't be null if the input isn't null
|
||||
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
|
||||
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
|
||||
}
|
||||
|
||||
class InternalProfileInfo
|
||||
{
|
||||
internal DeviceProfileInfo Info { get; set; }
|
||||
internal string Path { get; set; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
|
||||
{
|
||||
var profile = GetProfile(headers) ?? GetDefaultProfile();
|
||||
var profile = GetProfile(headers) ??
|
||||
GetDefaultProfile();
|
||||
|
||||
var serverId = _appHost.SystemId;
|
||||
|
||||
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageStream? GetIcon(string filename)
|
||||
public ImageStream GetIcon(string filename)
|
||||
{
|
||||
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
||||
? ImageFormat.Png
|
||||
: ImageFormat.Jpg;
|
||||
|
||||
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
|
||||
var stream = _assembly.GetManifestResourceStream(resource);
|
||||
if (stream == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ImageStream(stream)
|
||||
return new ImageStream
|
||||
{
|
||||
Format = format
|
||||
Format = format,
|
||||
Stream = _assembly.GetManifestResourceStream(resource)
|
||||
};
|
||||
}
|
||||
|
||||
private class InternalProfileInfo
|
||||
{
|
||||
internal InternalProfileInfo(DeviceProfileInfo info, string path)
|
||||
{
|
||||
Info = info;
|
||||
Path = path;
|
||||
}
|
||||
|
||||
internal DeviceProfileInfo Info { get; }
|
||||
|
||||
internal string Path { get; }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class DlnaProfileEntryPoint : IServerEntryPoint
|
||||
{
|
||||
@@ -518,7 +543,7 @@ namespace Emby.Dlna
|
||||
|
||||
private void DumpProfiles()
|
||||
{
|
||||
DeviceProfile[] list = new[]
|
||||
DeviceProfile[] list = new []
|
||||
{
|
||||
new SamsungSmartTvProfile(),
|
||||
new XboxOneProfile(),
|
||||
@@ -542,9 +567,9 @@ namespace Emby.Dlna
|
||||
new Foobar2000Profile(),
|
||||
new SharpSmartTvProfile(),
|
||||
new MediaMonkeyProfile(),
|
||||
// new Windows81Profile(),
|
||||
// new WindowsMediaCenterProfile(),
|
||||
// new WindowsPhoneProfile(),
|
||||
//new Windows81Profile(),
|
||||
//new WindowsMediaCenterProfile(),
|
||||
//new WindowsPhoneProfile(),
|
||||
new DirectTvProfile(),
|
||||
new DishHopperJoeyProfile(),
|
||||
new DefaultProfile(),
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<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>
|
||||
<Compile Include="..\SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
@@ -17,26 +12,24 @@
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Images\logo120.jpg" />
|
||||
<EmbeddedResource Include="Images\logo120.png" />
|
||||
@@ -80,7 +73,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -6,10 +7,8 @@ namespace Emby.Dlna
|
||||
{
|
||||
public class EventSubscriptionResponse
|
||||
{
|
||||
public EventSubscriptionResponse(string content, string contentType)
|
||||
public EventSubscriptionResponse()
|
||||
{
|
||||
Content = content;
|
||||
ContentType = contentType;
|
||||
Headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
@@ -17,6 +16,6 @@ namespace Emby.Dlna
|
||||
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public Dictionary<string, string> Headers { get; }
|
||||
public Dictionary<string, string> Headers { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -8,49 +7,43 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.Eventing
|
||||
{
|
||||
public class DlnaEventManager : IDlnaEventManager
|
||||
public class EventManager : IEventManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
|
||||
new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
|
||||
public EventManager(ILogger logger, IHttpClient httpClient)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
{
|
||||
var subscription = GetSubscription(subscriptionId, false);
|
||||
if (subscription != null)
|
||||
{
|
||||
subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
|
||||
int timeoutSeconds = subscription.TimeoutSeconds;
|
||||
subscription.SubscriptionTime = DateTime.UtcNow;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Renewing event subscription for {0} with timeout of {1} to {2}",
|
||||
subscription.NotificationType,
|
||||
timeoutSeconds,
|
||||
subscription.CallbackUrl);
|
||||
subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
|
||||
int timeoutSeconds = subscription.TimeoutSeconds;
|
||||
subscription.SubscriptionTime = DateTime.UtcNow;
|
||||
|
||||
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
||||
}
|
||||
_logger.LogDebug(
|
||||
"Renewing event subscription for {0} with timeout of {1} to {2}",
|
||||
subscription.NotificationType,
|
||||
timeoutSeconds,
|
||||
subscription.CallbackUrl);
|
||||
|
||||
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
@@ -58,8 +51,7 @@ namespace Emby.Dlna.Eventing
|
||||
var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
|
||||
var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Creating event subscription for {0} with timeout of {1} to {2}",
|
||||
_logger.LogDebug("Creating event subscription for {0} with timeout of {1} to {2}",
|
||||
notificationType,
|
||||
timeout,
|
||||
callbackUrl);
|
||||
@@ -69,8 +61,7 @@ namespace Emby.Dlna.Eventing
|
||||
Id = id,
|
||||
CallbackUrl = callbackUrl,
|
||||
SubscriptionTime = DateTime.UtcNow,
|
||||
TimeoutSeconds = timeout,
|
||||
NotificationType = notificationType
|
||||
TimeoutSeconds = timeout
|
||||
});
|
||||
|
||||
return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout);
|
||||
@@ -81,7 +72,9 @@ namespace Emby.Dlna.Eventing
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
// Starts with SECOND-
|
||||
if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
header = header.Split('-').Last();
|
||||
|
||||
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
@@ -94,17 +87,26 @@ namespace Emby.Dlna.Eventing
|
||||
{
|
||||
_logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
|
||||
|
||||
_subscriptions.TryRemove(subscriptionId, out _);
|
||||
_subscriptions.TryRemove(subscriptionId, out EventSubscription sub);
|
||||
|
||||
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
return new EventSubscriptionResponse
|
||||
{
|
||||
Content = string.Empty,
|
||||
ContentType = "text/plain"
|
||||
};
|
||||
}
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
|
||||
{
|
||||
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
var response = new EventSubscriptionResponse
|
||||
{
|
||||
Content = string.Empty,
|
||||
ContentType = "text/plain"
|
||||
};
|
||||
|
||||
response.Headers["SID"] = subscriptionId;
|
||||
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
|
||||
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -143,30 +145,33 @@ namespace Emby.Dlna.Eventing
|
||||
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
|
||||
foreach (var key in stateVariables.Keys)
|
||||
{
|
||||
builder.Append("<e:property>")
|
||||
.Append('<')
|
||||
.Append(key)
|
||||
.Append('>')
|
||||
.Append(stateVariables[key])
|
||||
.Append("</")
|
||||
.Append(key)
|
||||
.Append('>')
|
||||
.Append("</e:property>");
|
||||
builder.Append("<e:property>");
|
||||
builder.Append("<" + key + ">");
|
||||
builder.Append(stateVariables[key]);
|
||||
builder.Append("</" + key + ">");
|
||||
builder.Append("</e:property>");
|
||||
}
|
||||
|
||||
builder.Append("</e:propertyset>");
|
||||
|
||||
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
|
||||
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
|
||||
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
|
||||
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
|
||||
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
|
||||
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
RequestContent = builder.ToString(),
|
||||
RequestContentType = "text/xml",
|
||||
Url = subscription.CallbackUrl,
|
||||
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
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
||||
using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -1,6 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
|
||||
@@ -9,13 +8,10 @@ namespace Emby.Dlna.Eventing
|
||||
public class EventSubscription
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string CallbackUrl { get; set; }
|
||||
|
||||
public string NotificationType { get; set; }
|
||||
|
||||
public DateTime SubscriptionTime { get; set; }
|
||||
|
||||
public int TimeoutSeconds { get; set; }
|
||||
|
||||
public long TriggerCount { get; set; }
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IConnectionManager : IDlnaEventManager, IUpnpService
|
||||
public interface IConnectionManager : IEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IContentDirectory : IDlnaEventManager, IUpnpService
|
||||
public interface IContentDirectory : IEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IDlnaEventManager
|
||||
public interface IEventManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Cancels the event subscription.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The subscription identifier.</param>
|
||||
/// <returns>The response.</returns>
|
||||
EventSubscriptionResponse CancelEventSubscription(string subscriptionId);
|
||||
|
||||
/// <summary>
|
||||
/// Renews the event subscription.
|
||||
/// </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);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the event subscription.
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
|
||||
public interface IMediaReceiverRegistrar : IEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
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,17 +1,13 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.PlayTo;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using Jellyfin.Networking.Manager;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
@@ -27,19 +23,23 @@ using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Rssdp;
|
||||
using Rssdp.Infrastructure;
|
||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
||||
|
||||
namespace Emby.Dlna.Main
|
||||
{
|
||||
public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
|
||||
public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILogger<DlnaEntryPoint> _logger;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
private PlayToManager _manager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
@@ -48,24 +48,27 @@ namespace Emby.Dlna.Main
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
|
||||
private SsdpDevicePublisher _Publisher;
|
||||
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly object _syncLock = new object();
|
||||
private readonly bool _disabled;
|
||||
|
||||
private PlayToManager _manager;
|
||||
private SsdpDevicePublisher _publisher;
|
||||
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(
|
||||
IServerConfigurationManager config,
|
||||
public static DlnaEntryPoint Current;
|
||||
|
||||
public DlnaEntryPoint(IServerConfigurationManager config,
|
||||
ILoggerFactory loggerFactory,
|
||||
IServerApplicationHost appHost,
|
||||
ISessionManager sessionManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IHttpClient httpClient,
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager,
|
||||
IDlnaManager dlnaManager,
|
||||
@@ -83,7 +86,7 @@ namespace Emby.Dlna.Main
|
||||
_config = config;
|
||||
_appHost = appHost;
|
||||
_sessionManager = sessionManager;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_httpClient = httpClient;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_dlnaManager = dlnaManager;
|
||||
@@ -95,73 +98,39 @@ namespace Emby.Dlna.Main
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_socketFactory = socketFactory;
|
||||
_networkManager = networkManager;
|
||||
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
|
||||
_logger = loggerFactory.CreateLogger("Dlna");
|
||||
|
||||
ContentDirectory = new ContentDirectory.ContentDirectoryService(
|
||||
ContentDirectory = new ContentDirectory.ContentDirectory(
|
||||
dlnaManager,
|
||||
userDataManager,
|
||||
imageProcessor,
|
||||
libraryManager,
|
||||
config,
|
||||
userManager,
|
||||
loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
|
||||
httpClientFactory,
|
||||
_logger,
|
||||
httpClient,
|
||||
localizationManager,
|
||||
mediaSourceManager,
|
||||
userViewManager,
|
||||
mediaEncoder,
|
||||
tvSeriesManager);
|
||||
|
||||
ConnectionManager = new ConnectionManager.ConnectionManagerService(
|
||||
dlnaManager,
|
||||
config,
|
||||
loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
|
||||
httpClientFactory);
|
||||
ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient);
|
||||
|
||||
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
|
||||
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
|
||||
httpClientFactory,
|
||||
config);
|
||||
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config);
|
||||
Current = this;
|
||||
|
||||
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
|
||||
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
|
||||
|
||||
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
||||
{
|
||||
_logger.LogError("The DLNA specification does not support HTTPS.");
|
||||
}
|
||||
}
|
||||
|
||||
public static DlnaEntryPoint Current { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the dlna server is enabled.
|
||||
/// </summary>
|
||||
public static bool Enabled { get; private set; }
|
||||
|
||||
public IContentDirectory ContentDirectory { get; private set; }
|
||||
|
||||
public IConnectionManager ConnectionManager { get; private set; }
|
||||
|
||||
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
||||
|
||||
if (_disabled)
|
||||
{
|
||||
// No use starting as dlna won't work, as we're running purely on HTTPS.
|
||||
return;
|
||||
}
|
||||
|
||||
ReloadComponents();
|
||||
|
||||
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
||||
_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))
|
||||
{
|
||||
@@ -169,16 +138,15 @@ namespace Emby.Dlna.Main
|
||||
}
|
||||
}
|
||||
|
||||
private void ReloadComponents()
|
||||
private async void ReloadComponents()
|
||||
{
|
||||
var options = _config.GetDlnaConfiguration();
|
||||
Enabled = options.EnableServer;
|
||||
|
||||
StartSsdpHandler();
|
||||
|
||||
if (options.EnableServer)
|
||||
{
|
||||
StartDevicePublisher(options);
|
||||
await StartDevicePublisher(options).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -201,10 +169,10 @@ namespace Emby.Dlna.Main
|
||||
{
|
||||
if (_communicationsServer == null)
|
||||
{
|
||||
var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
|
||||
OperatingSystem.IsLinux();
|
||||
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
|
||||
OperatingSystem.Id == OperatingSystemId.Linux;
|
||||
|
||||
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||
_communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||
{
|
||||
IsShared = true
|
||||
};
|
||||
@@ -218,14 +186,16 @@ namespace Emby.Dlna.Main
|
||||
}
|
||||
}
|
||||
|
||||
private void LogMessage(string msg)
|
||||
{
|
||||
_logger.LogDebug(msg);
|
||||
}
|
||||
|
||||
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (communicationsServer != null)
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||
}
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -246,33 +216,27 @@ namespace Emby.Dlna.Main
|
||||
}
|
||||
}
|
||||
|
||||
public void StartDevicePublisher(Configuration.DlnaOptions options)
|
||||
public async Task StartDevicePublisher(Configuration.DlnaOptions options)
|
||||
{
|
||||
if (!options.BlastAliveMessages)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_publisher != null)
|
||||
if (_Publisher != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_publisher = new SsdpDevicePublisher(
|
||||
_communicationsServer,
|
||||
MediaBrowser.Common.System.OperatingSystem.Name,
|
||||
Environment.OSVersion.VersionString,
|
||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
{
|
||||
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
|
||||
SupportPnpRootDevice = false
|
||||
};
|
||||
_Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost);
|
||||
_Publisher.LogFunction = LogMessage;
|
||||
_Publisher.SupportPnpRootDevice = false;
|
||||
|
||||
RegisterServerEndpoints();
|
||||
await RegisterServerEndpoints().ConfigureAwait(false);
|
||||
|
||||
_publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
|
||||
_Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -280,47 +244,33 @@ 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 descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||
|
||||
var bindAddresses = NetworkManager.CreateCollection(
|
||||
_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)
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
// Not supporting IPv6 right now
|
||||
continue;
|
||||
}
|
||||
|
||||
// Limit to LAN addresses only
|
||||
if (!_networkManager.IsInLocalNetwork(address))
|
||||
{
|
||||
continue;
|
||||
// Not support IPv6 right now
|
||||
continue;
|
||||
}
|
||||
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
||||
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri);
|
||||
var descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
{
|
||||
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.
|
||||
Address = address.Address,
|
||||
PrefixLength = address.PrefixLength,
|
||||
CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
|
||||
Location = uri, // Must point to the URL that serves your devices UPnP description document.
|
||||
Address = address,
|
||||
SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
|
||||
FriendlyName = "Jellyfin",
|
||||
Manufacturer = "Jellyfin",
|
||||
ModelName = "Jellyfin Server",
|
||||
@@ -329,13 +279,13 @@ namespace Emby.Dlna.Main
|
||||
};
|
||||
|
||||
SetProperies(device, fullService);
|
||||
_publisher.AddDevice(device);
|
||||
_Publisher.AddDevice(device);
|
||||
|
||||
var embeddedDevices = new[]
|
||||
{
|
||||
"urn:schemas-upnp-org:service:ContentDirectory: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)
|
||||
@@ -361,13 +311,12 @@ namespace Emby.Dlna.Main
|
||||
{
|
||||
guid = text.GetMD5();
|
||||
}
|
||||
|
||||
return guid.ToString("D", CultureInfo.InvariantCulture);
|
||||
return guid.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
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(':');
|
||||
|
||||
@@ -378,6 +327,7 @@ namespace Emby.Dlna.Main
|
||||
device.DeviceType = serviceParts[2];
|
||||
}
|
||||
|
||||
private readonly object _syncLock = new object();
|
||||
private void StartPlayToManager()
|
||||
{
|
||||
lock (_syncLock)
|
||||
@@ -389,8 +339,7 @@ namespace Emby.Dlna.Main
|
||||
|
||||
try
|
||||
{
|
||||
_manager = new PlayToManager(
|
||||
_logger,
|
||||
_manager = new PlayToManager(_logger,
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_userManager,
|
||||
@@ -398,7 +347,8 @@ namespace Emby.Dlna.Main
|
||||
_appHost,
|
||||
_imageProcessor,
|
||||
_deviceDiscovery,
|
||||
_httpClientFactory,
|
||||
_httpClient,
|
||||
_config,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
@@ -428,30 +378,13 @@ namespace Emby.Dlna.Main
|
||||
{
|
||||
_logger.LogError(ex, "Error disposing PlayTo manager");
|
||||
}
|
||||
|
||||
_manager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DisposeDevicePublisher()
|
||||
{
|
||||
if (_publisher != null)
|
||||
{
|
||||
_logger.LogInformation("Disposing SsdpDevicePublisher");
|
||||
_publisher.Dispose();
|
||||
_publisher = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DisposeDevicePublisher();
|
||||
DisposePlayToManager();
|
||||
DisposeDeviceDiscovery();
|
||||
@@ -467,8 +400,16 @@ namespace Emby.Dlna.Main
|
||||
ConnectionManager = null;
|
||||
MediaReceiverRegistrar = 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.Collections.Generic;
|
||||
using System.Xml;
|
||||
@@ -8,23 +11,15 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ControlHandler" />.
|
||||
/// </summary>
|
||||
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)
|
||||
: base(config, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
{
|
||||
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -41,17 +36,9 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
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)
|
||||
=> 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)
|
||||
=> 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,88 +1,79 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Service;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
public class MediaReceiverRegistrarXmlBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
|
||||
/// </summary>
|
||||
/// <returns>An XML representation of this service.</returns>
|
||||
public static string GetXml()
|
||||
public 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()
|
||||
{
|
||||
var list = new List<StateVariable>
|
||||
var list = new List<StateVariable>();
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
Name = "AuthorizationGrantedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
Name = "AuthorizationGrantedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_DeviceID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_DeviceID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "AuthorizationDeniedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "AuthorizationDeniedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "ValidationSucceededUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "ValidationSucceededUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationRespMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationRespMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationReqMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationReqMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "ValidationRevokedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "ValidationRevokedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "int",
|
||||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "int",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
||||
/// </summary>
|
||||
public static class ServiceActionListBuilder
|
||||
public class ServiceActionListBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a list of services that this instance provides.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
||||
public static IEnumerable<ServiceAction> GetActions()
|
||||
public IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
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()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -52,10 +44,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "IsAuthorized".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetIsAuthorized()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -78,10 +66,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "RegisterDevice".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetRegisterDevice()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -104,10 +88,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetValidationSucceededUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetValidationSucceededUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
@@ -124,11 +104,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetAuthorizationDeniedUpdateID()
|
||||
private ServiceAction GetGetAuthorizationDeniedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
@@ -144,11 +120,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetValidationRevokedUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetValidationRevokedUpdateID()
|
||||
private ServiceAction GetGetValidationRevokedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
@@ -164,11 +136,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetAuthorizationGrantedUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetAuthorizationGrantedUpdateID()
|
||||
private ServiceAction GetGetAuthorizationGrantedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
@@ -10,9 +9,6 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class DeviceInfo
|
||||
{
|
||||
private readonly List<DeviceService> _services = new List<DeviceService>();
|
||||
private string _baseUrl = string.Empty;
|
||||
|
||||
public DeviceInfo()
|
||||
{
|
||||
Name = "Generic Device";
|
||||
@@ -38,6 +34,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
public string PresentationUrl { get; set; }
|
||||
|
||||
private string _baseUrl = string.Empty;
|
||||
public string BaseUrl
|
||||
{
|
||||
get => _baseUrl;
|
||||
@@ -46,6 +43,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
public DeviceIcon Icon { get; set; }
|
||||
|
||||
private readonly List<DeviceService> _services = new List<DeviceService>();
|
||||
public List<DeviceService> Services => _services;
|
||||
|
||||
public DeviceIdentification ToDeviceIdentification()
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class MediaChangedEventArgs : EventArgs
|
||||
{
|
||||
public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
|
||||
{
|
||||
OldMediaInfo = oldMediaInfo;
|
||||
NewMediaInfo = newMediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject OldMediaInfo { get; set; }
|
||||
|
||||
public UBaseObject NewMediaInfo { get; set; }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,30 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public sealed class PlayToManager : IDisposable
|
||||
public class PlayToManager : IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
@@ -33,7 +34,8 @@ namespace Emby.Dlna.PlayTo
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
@@ -45,7 +47,7 @@ namespace Emby.Dlna.PlayTo
|
||||
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||
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, 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;
|
||||
_sessionManager = sessionManager;
|
||||
@@ -55,7 +57,8 @@ namespace Emby.Dlna.PlayTo
|
||||
_appHost = appHost;
|
||||
_imageProcessor = imageProcessor;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_httpClient = httpClient;
|
||||
_config = config;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
@@ -76,20 +79,17 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var info = e.Argument;
|
||||
|
||||
if (!info.Headers.TryGetValue("USN", out string usn))
|
||||
{
|
||||
usn = string.Empty;
|
||||
}
|
||||
if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty;
|
||||
|
||||
if (!info.Headers.TryGetValue("NT", out string nt))
|
||||
{
|
||||
nt = string.Empty;
|
||||
}
|
||||
if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty;
|
||||
|
||||
string location = info.Location.ToString();
|
||||
|
||||
// It has to report that it's a media renderer
|
||||
if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)
|
||||
&& !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase))
|
||||
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
//_logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,10 +109,11 @@ namespace Emby.Dlna.PlayTo
|
||||
return;
|
||||
}
|
||||
|
||||
await AddDevice(info, cancellationToken).ConfigureAwait(false);
|
||||
await AddDevice(info, location, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -124,88 +125,81 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetUuid(string usn)
|
||||
private string GetUuid(string usn)
|
||||
{
|
||||
const string UuidStr = "uuid:";
|
||||
const string UuidColonStr = "::";
|
||||
|
||||
var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
|
||||
if (index == -1)
|
||||
{
|
||||
return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> tmp = usn.AsSpan()[(index + UuidStr.Length)..];
|
||||
|
||||
index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
|
||||
var found = false;
|
||||
var index = usn.IndexOf("uuid:", StringComparison.OrdinalIgnoreCase);
|
||||
if (index != -1)
|
||||
{
|
||||
tmp = tmp[..index];
|
||||
usn = usn.Substring(index);
|
||||
found = true;
|
||||
}
|
||||
|
||||
index = tmp.IndexOf('{');
|
||||
index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
|
||||
if (index != -1)
|
||||
{
|
||||
int endIndex = tmp.IndexOf('}');
|
||||
if (endIndex != -1)
|
||||
{
|
||||
tmp = tmp[(index + 1)..endIndex];
|
||||
}
|
||||
usn = usn.Substring(0, index);
|
||||
}
|
||||
|
||||
return tmp.ToString();
|
||||
if (found)
|
||||
{
|
||||
return usn;
|
||||
}
|
||||
|
||||
return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private async Task AddDevice(UpnpDeviceInfo info, CancellationToken cancellationToken)
|
||||
private async Task AddDevice(UpnpDeviceInfo info, string location, CancellationToken cancellationToken)
|
||||
{
|
||||
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))
|
||||
{
|
||||
uuid = GetUuid(uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
uuid = location.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var sessionInfo = await _sessionManager
|
||||
.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
|
||||
.ConfigureAwait(false);
|
||||
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
|
||||
|
||||
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (device == null)
|
||||
{
|
||||
_logger.LogError("Ignoring device as xml response is invalid.");
|
||||
return;
|
||||
}
|
||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string deviceName = device.Properties.Name;
|
||||
|
||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
|
||||
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(
|
||||
sessionInfo,
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_logger,
|
||||
_dlnaManager,
|
||||
_userManager,
|
||||
_imageProcessor,
|
||||
serverAddress,
|
||||
null,
|
||||
_deviceDiscovery,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_mediaEncoder);
|
||||
controller = new PlayToController(sessionInfo,
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_logger,
|
||||
_dlnaManager,
|
||||
_userManager,
|
||||
_imageProcessor,
|
||||
serverAddress,
|
||||
null,
|
||||
_deviceDiscovery,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_config,
|
||||
_mediaEncoder);
|
||||
|
||||
sessionInfo.AddController(controller);
|
||||
|
||||
@@ -218,17 +212,17 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
PlayableMediaTypes = profile.GetSupportedMediaTypes(),
|
||||
|
||||
SupportedCommands = new[]
|
||||
SupportedCommands = new string[]
|
||||
{
|
||||
GeneralCommandType.VolumeDown,
|
||||
GeneralCommandType.VolumeUp,
|
||||
GeneralCommandType.Mute,
|
||||
GeneralCommandType.Unmute,
|
||||
GeneralCommandType.ToggleMute,
|
||||
GeneralCommandType.SetVolume,
|
||||
GeneralCommandType.SetAudioStreamIndex,
|
||||
GeneralCommandType.SetSubtitleStreamIndex,
|
||||
GeneralCommandType.PlayMediaSource
|
||||
GeneralCommandType.VolumeDown.ToString(),
|
||||
GeneralCommandType.VolumeUp.ToString(),
|
||||
GeneralCommandType.Mute.ToString(),
|
||||
GeneralCommandType.Unmute.ToString(),
|
||||
GeneralCommandType.ToggleMute.ToString(),
|
||||
GeneralCommandType.SetVolume.ToString(),
|
||||
GeneralCommandType.SetAudioStreamIndex.ToString(),
|
||||
GeneralCommandType.SetSubtitleStreamIndex.ToString(),
|
||||
GeneralCommandType.PlayMediaSource.ToString()
|
||||
},
|
||||
|
||||
SupportsMediaControl = true
|
||||
@@ -238,7 +232,6 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
|
||||
@@ -247,13 +240,10 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
_disposeCancellationTokenSource.Cancel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
_logger.LogDebug(ex, "Error while disposing PlayToManager");
|
||||
}
|
||||
|
||||
_sessionLock.Dispose();
|
||||
_disposeCancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
|
||||
@@ -6,11 +7,6 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackProgressEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackProgressEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
public uBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
|
||||
@@ -6,11 +7,6 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackStartEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackStartEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
public uBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
|
||||
@@ -6,11 +7,12 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackStoppedEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
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,6 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -10,9 +9,9 @@ using MediaBrowser.Model.Session;
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1600
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -20,11 +19,13 @@ namespace Emby.Dlna.PlayTo
|
||||
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
|
||||
private const string FriendlyName = "Jellyfin";
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
public SsdpHttpClient(IHttpClientFactory httpClientFactory)
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public SsdpHttpClient(IHttpClient httpClient)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<XDocument> SendCommandAsync(
|
||||
@@ -32,24 +33,27 @@ namespace Emby.Dlna.PlayTo
|
||||
DeviceService service,
|
||||
string command,
|
||||
string postData,
|
||||
string header = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
bool logRequest = true,
|
||||
string header = null)
|
||||
{
|
||||
var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
|
||||
using var response = await PostSoapDataAsync(
|
||||
url,
|
||||
$"\"{service.ServiceType}#{command}\"",
|
||||
postData,
|
||||
header,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var cancellationToken = CancellationToken.None;
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
|
||||
using (var response = await PostSoapDataAsync(
|
||||
url,
|
||||
$"\"{service.ServiceType}#{command}\"",
|
||||
postData,
|
||||
header,
|
||||
logRequest,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
using (var stream = response.Content)
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
return XDocument.Parse(
|
||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
||||
LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||
@@ -60,7 +64,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return serviceUrl;
|
||||
}
|
||||
|
||||
if (!serviceUrl.StartsWith('/'))
|
||||
if (!serviceUrl.StartsWith("/"))
|
||||
{
|
||||
serviceUrl = "/" + serviceUrl;
|
||||
}
|
||||
@@ -76,45 +80,55 @@ namespace Emby.Dlna.PlayTo
|
||||
int eventport,
|
||||
int timeOut = 3600)
|
||||
{
|
||||
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
|
||||
options.Headers.UserAgent.ParseAdd(USERAGENT);
|
||||
options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
|
||||
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
|
||||
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
|
||||
options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
UserAgent = USERAGENT,
|
||||
LogErrorResponseBody = true,
|
||||
BufferContent = false,
|
||||
};
|
||||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture);
|
||||
options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">";
|
||||
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)
|
||||
{
|
||||
using var options = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
options.Headers.UserAgent.ParseAdd(USERAGENT);
|
||||
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
Url = url,
|
||||
UserAgent = USERAGENT,
|
||||
LogErrorResponseBody = true,
|
||||
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 null;
|
||||
return XDocument.Parse(
|
||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
||||
LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
||||
private Task<HttpResponseInfo> PostSoapDataAsync(
|
||||
string url,
|
||||
string soapAction,
|
||||
string postData,
|
||||
string header,
|
||||
bool logRequest,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (soapAction[0] != '\"')
|
||||
@@ -122,20 +136,29 @@ namespace Emby.Dlna.PlayTo
|
||||
soapAction = $"\"{soapAction}\"";
|
||||
}
|
||||
|
||||
using var options = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
options.Headers.UserAgent.ParseAdd(USERAGENT);
|
||||
options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction);
|
||||
options.Headers.TryAddWithoutValidation("Pragma", "no-cache");
|
||||
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
UserAgent = USERAGENT,
|
||||
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))
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user