Compare commits

..

41 Commits

Author SHA1 Message Date
Joshua M. Boniface dbbf97e588 Fix version in spec 2020-04-13 00:24:43 -04:00
Joshua M. Boniface 4e9df69ffd Merge pull request #2847 from mark-monteiro/fix-build
Fix compilation error in HttpListenerHost
2020-04-12 21:35:43 -04:00
Mark Monteiro 7f38ef4c3c Fix compilation error in HttpListenerHost 2020-04-12 20:49:54 -04:00
Joshua M. Boniface 16549dead9 Bump version to 10.5.4 2020-04-12 19:24:56 -04:00
Joshua M. Boniface 9bd1a9d19c Merge pull request #2715 from nyanmisaka/libfdk-aac
Prefer to use libfdk_aac encoder for better audio quality when it is available

(cherry picked from commit bf92694f8b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 19:19:00 -04:00
Joshua M. Boniface 67194994f9 Merge pull request #2783 from JustAMan/better-cancel-msg
Add logging of URL being processed when logging an error

(cherry picked from commit 2be6550db4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 19:18:22 -04:00
Vasily c249e15f48 Merge pull request #2782 from JustAMan/fix-ssa-delivery
Fix support for attachments with baseURL set

(cherry picked from commit 6386b9b1b9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 16:51:11 -04:00
Anthony Lavado 48ba5a9a30 Merge pull request #2779 from KristupasSavickas/fix-docker-arm-ffmpeg-path
Fix ffmpeg path on ARM docker image

(cherry picked from commit 6d98c0b62a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 16:50:27 -04:00
Bond-009 dd13f8d16a Merge pull request #2821 from nyanmisaka/mpeg4
Fix MPEG4 broken on VAAPI

(cherry picked from commit 84dba64644)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 15:53:12 -04:00
dkanada b43a8a56dc Merge pull request #2796 from JustAMan/fix-transcode-reasons
Make codec check in profile examine profile type first

(cherry picked from commit aeedd06f51)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 15:52:39 -04:00
Bond-009 3ec18f085e Merge pull request #2785 from nyanmisaka/mpge4-profile15
Fix MPEG4 packback error regression on vaapi

(cherry picked from commit b16b58bc57)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 15:51:47 -04:00
dkanada f2728b5a92 Merge pull request #2758 from Bond-009/plugininstalled
Remove PluginInstalled

(cherry picked from commit 0cd7cd611e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 15:50:09 -04:00
Bond-009 ee47a75f9f Merge pull request #2721 from PrplHaz4/patch-2
Separate Channels permissions from All Libraries

(cherry picked from commit 3a98ad8255)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 15:49:32 -04:00
Joshua M. Boniface 66e7e8bcd2 Make the portable packages build the right tag
Will change for 10.5.4, but quickfix for now
2020-04-05 15:20:08 -04:00
Joshua M. Boniface 0de3a9465e Fix version in RPM spec 2020-04-05 14:58:16 -04:00
Vasily b2f7417365 Merge pull request #2559 from whooo/295-fix
Add descriptive TV episode titles for DLNA browsing

(cherry picked from commit a37b69a493)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:51:29 -04:00
Vasily bf0c07abfe Merge pull request #2503 from nyanmisaka/vaapi
Fix various bugs in HWA subtitle burn-in

(cherry picked from commit 9aefb41512)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:51:08 -04:00
Bond-009 3a4cd01b13 Merge pull request #2740 from JustAMan/fix-livetv
Fix GetLocalApiUrl for cases with https enabled

(cherry picked from commit b3283e37f2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:44:41 -04:00
Anthony Lavado 7059761806 Merge pull request #2730 from Bond-009/plugincrash
Try to not crash on unsupported plugin load

(cherry picked from commit c9919f4633)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:44:12 -04:00
dkanada 2cde59af44 Merge pull request #2723 from jairbubbles/updateSkiaForArm
Update Jellyfin.SkiaSharp.NativeAssets.LinuxArm to version 1.68.1

(cherry picked from commit 58900bb57e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:43:51 -04:00
Anthony Lavado 52a850cc9b Merge pull request #2720 from dkanada/music
Fix custom musicbrainz servers

(cherry picked from commit 8c8a396cd6)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:43:32 -04:00
dkanada 54435a1243 Merge pull request #2712 from joshuaboniface/fix-ldap-issues
Revert #2146 ordering change

(cherry picked from commit 9e82e7c847)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:42:56 -04:00
Bond-009 899be44388 Merge pull request #2674 from JustAMan/fix-attachments-scan
Make variables binding correspond with column names

(cherry picked from commit 10050a55a5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:42:30 -04:00
dkanada 354079862e Merge pull request #2668 from mark-monteiro/fix-application-host-dispose
Fix ApplicationHost Dispose() method

(cherry picked from commit 622106467e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:41:53 -04:00
Bond-009 01db9af821 Merge pull request #2655 from lfoust/tvdbruntimefix
Fix FormatException when mapping TVDB series

(cherry picked from commit 632323969f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:41:28 -04:00
Bond-009 9da2635d86 Merge pull request #2653 from iwalton3/fix-embedded-subtitles
Fix embedded mkv subtitles.

(cherry picked from commit 1345e699fa)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:40:59 -04:00
Joshua M. Boniface 4caa597cde Bump version to 10.5.3
Include quick robustness fix in Docker container clone too.
2020-04-05 12:40:29 -04:00
dkanada da34bd940e Merge pull request #2478 from JustAMan/fix-search-order
Fix ordering of search results

(cherry picked from commit 0d9787dfb4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 15:25:11 -04:00
Joshua M. Boniface 54efde4073 Merge pull request #2642 from mark-monteiro/fix-extras
Add missing null check when retrieving extras

(cherry picked from commit 425bd2b01b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 15:24:54 -04:00
Joshua M. Boniface 02cfa15582 Quickly fix failing curl in ARM Dockerfile 2020-03-22 15:02:03 -04:00
Joshua M. Boniface 0e171d794c Bump version to 10.5.2 2020-03-22 12:13:59 -04:00
dkanada d5f2384375 Merge pull request #2617 from Shawmon/wasm-mimetype
add wasm mimetype

(cherry picked from commit af5d3e8eae)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 11:48:18 -04:00
dkanada 816b3f5c2e Merge pull request #2622 from Artiume/patch-5
Fix Release 10.5.z
2020-03-21 16:26:16 +09:00
artiume a234388552 Fix arm64 2020-03-17 10:34:25 -04:00
artiume 30d38bd5c6 Update armhf 2020-03-17 10:33:06 -04:00
artiume 9f49fe2e99 Fix Release 10.5.z 2020-03-17 09:38:22 -04:00
Joshua M. Boniface f720a0fca2 Fix mangled Dockerfiles 2020-03-16 09:30:01 -04:00
Joshua M. Boniface aadff77531 Merge pull request #2607 from joshuaboniface/fix-fedora
Correct BuildRequires and NodeJS for Fedora/CentOS

(cherry picked from commit ef4dfd4461)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-15 23:06:22 -04:00
dkanada 89fc5aa11a Merge pull request #2582 from Bond-009/subs
Fix subtitles

(cherry picked from commit 6960f0af67)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-15 23:05:41 -04:00
dkanada cb91595a24 Merge pull request #2541 from joshuaboniface/fix-docker-arm
Fix curl for Jellyfin GPG key

(cherry picked from commit bf34105af3)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-15 23:05:13 -04:00
Joshua M. Boniface 6233bae7f0 Bump version to 10.5.1 2020-03-15 23:04:07 -04:00
2544 changed files with 87987 additions and 162338 deletions
@@ -1,23 +1,21 @@
parameters: parameters:
- name: Packages - name: Packages
type: object type: object
default: {} default: {}
- name: LinuxImage - name: LinuxImage
type: string type: string
default: "ubuntu-latest" default: "ubuntu-latest"
- name: DotNetSdkVersion - name: DotNetSdkVersion
type: string type: string
default: 6.0.x default: 3.1.100
jobs: jobs:
- job: CompatibilityCheck - job: CompatibilityCheck
displayName: Compatibility Check displayName: Compatibility Check
dependsOn: Build
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
pool: pool:
vmImage: "${{ parameters.LinuxImage }}" vmImage: "${{ parameters.LinuxImage }}"
# only execute for pull requests
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
strategy: strategy:
matrix: matrix:
${{ each Package in parameters.Packages }}: ${{ each Package in parameters.Packages }}:
@@ -25,7 +23,7 @@ jobs:
NugetPackageName: ${{ Package.value.NugetPackageName }} NugetPackageName: ${{ Package.value.NugetPackageName }}
AssemblyFileName: ${{ Package.value.AssemblyFileName }} AssemblyFileName: ${{ Package.value.AssemblyFileName }}
maxParallel: 2 maxParallel: 2
dependsOn: MainBuild
steps: steps:
- checkout: none - checkout: none
@@ -35,34 +33,26 @@ jobs:
packageType: sdk packageType: sdk
version: ${{ parameters.DotNetSdkVersion }} version: ${{ parameters.DotNetSdkVersion }}
- task: DotNetCoreCLI@2
displayName: 'Install ABI CompatibilityChecker Tool'
inputs:
command: custom
custom: tool
arguments: 'update compatibilitychecker -g'
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: 'Download New Assembly Build Artifact' displayName: "Download New Assembly Build Artifact"
inputs: inputs:
source: 'current' source: "current"
artifact: "$(NugetPackageName)" artifact: "$(NugetPackageName)"
path: "$(System.ArtifactsDirectory)/new-artifacts" path: "$(System.ArtifactsDirectory)/new-artifacts"
runVersion: "latest" runVersion: "latest"
- task: CopyFiles@2 - task: CopyFiles@2
displayName: 'Copy New Assembly Build Artifact' displayName: "Copy New Assembly Build Artifact"
inputs: inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
contents: '**/*.dll' contents: "**/*.dll"
targetFolder: $(System.ArtifactsDirectory)/new-release targetFolder: $(System.ArtifactsDirectory)/new-release
cleanTargetFolder: true cleanTargetFolder: true
overWrite: true overWrite: true
flattenFolders: true flattenFolders: true
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: 'Download Reference Assembly Build Artifact' displayName: "Download Reference Assembly Build Artifact"
enabled: false
inputs: inputs:
source: "specific" source: "specific"
artifact: "$(NugetPackageName)" artifact: "$(NugetPackageName)"
@@ -73,21 +63,34 @@ jobs:
runBranch: "refs/heads/$(System.PullRequest.TargetBranch)" runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
- task: CopyFiles@2 - task: CopyFiles@2
displayName: 'Copy Reference Assembly Build Artifact' displayName: "Copy Reference Assembly Build Artifact"
enabled: false
inputs: inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: '**/*.dll' contents: "**/*.dll"
targetFolder: $(System.ArtifactsDirectory)/current-release targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true cleanTargetFolder: true
overWrite: true overWrite: true
flattenFolders: true flattenFolders: true
- task: DotNetCoreCLI@2 - task: DownloadGitHubRelease@0
displayName: 'Execute ABI Compatibility Check Tool' displayName: "Download ABI Compatibility Check Tool"
enabled: false
inputs: inputs:
command: custom connection: Jellyfin Release Download
custom: compat userRepository: EraYaN/dotnet-compatibility
arguments: 'current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only' defaultVersionType: "latest"
itemPattern: "**-ci.zip"
downloadPath: "$(System.ArtifactsDirectory)"
- task: ExtractFiles@1
displayName: "Extract ABI Compatibility Check Tool"
inputs:
archiveFilePatterns: "$(System.ArtifactsDirectory)/*-ci.zip"
destinationFolder: $(System.ArtifactsDirectory)/tools
cleanDestinationFolder: true
# The `--warnings-only` switch will swallow the return code and not emit any errors.
- task: CmdLine@2
displayName: "Execute ABI Compatibility Check Tool"
inputs:
script: "dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only"
workingDirectory: $(System.ArtifactsDirectory) workingDirectory: $(System.ArtifactsDirectory)
+57 -56
View File
@@ -1,100 +1,101 @@
parameters: parameters:
LinuxImage: 'ubuntu-latest' LinuxImage: "ubuntu-latest"
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj' RestoreBuildProjects: "Jellyfin.Server/Jellyfin.Server.csproj"
DotNetSdkVersion: 6.0.x DotNetSdkVersion: 3.1.100
jobs: jobs:
- job: Build - job: MainBuild
displayName: Build displayName: Main Build
strategy: strategy:
matrix: matrix:
Release: Release:
BuildConfiguration: Release BuildConfiguration: Release
Debug: Debug:
BuildConfiguration: Debug BuildConfiguration: Debug
maxParallel: 2
pool: pool:
vmImage: '${{ parameters.LinuxImage }}' vmImage: "${{ parameters.LinuxImage }}"
steps: steps:
- checkout: self - checkout: self
clean: true clean: true
submodules: true submodules: true
persistCredentials: true persistCredentials: true
- task: DownloadPipelineArtifact@2 - task: CmdLine@2
displayName: 'Download Web Branch' displayName: "Clone Web Client (Master, Release, or Tag)"
condition: in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion') condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs: inputs:
path: '$(Agent.TempDirectory)' script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
artifact: 'jellyfin-web-production'
source: 'specific'
project: 'jellyfin'
pipeline: 'Jellyfin Web'
runBranch: variables['Build.SourceBranch']
- task: DownloadPipelineArtifact@2 - task: CmdLine@2
displayName: 'Download Web Target' displayName: "Clone Web Client (PR)"
condition: eq(variables['Build.Reason'], 'PullRequest') condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
inputs: inputs:
path: '$(Agent.TempDirectory)' script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
artifact: 'jellyfin-web-production'
source: 'specific'
project: 'jellyfin'
pipeline: 'Jellyfin Web'
runBranch: variables['System.PullRequest.TargetBranch']
- task: ExtractFiles@1 - task: NodeTool@0
displayName: 'Extract Web Client' displayName: "Install Node"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs: inputs:
archiveFilePatterns: '$(Agent.TempDirectory)/*.zip' versionSpec: "10.x"
destinationFolder: '$(Build.SourcesDirectory)/MediaBrowser.WebDashboard'
cleanDestinationFolder: false - task: CmdLine@2
displayName: "Build Web Client"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
script: yarn install
workingDirectory: $(Agent.TempDirectory)/jellyfin-web
- task: CopyFiles@2
displayName: "Copy Web Client"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
contents: "**"
targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
cleanTargetFolder: true
overWrite: true
flattenFolders: false
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Update DotNet' displayName: "Update DotNet"
inputs: inputs:
packageType: sdk packageType: sdk
version: ${{ parameters.DotNetSdkVersion }} version: ${{ parameters.DotNetSdkVersion }}
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Publish Server' displayName: "Publish Server"
inputs: inputs:
command: publish command: publish
publishWebProjects: false publishWebProjects: false
projects: '${{ parameters.RestoreBuildProjects }}' projects: "${{ parameters.RestoreBuildProjects }}"
arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)' arguments: "--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)"
zipAfterPublish: false zipAfterPublish: false
- task: PublishPipelineArtifact@1 - task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Naming' displayName: "Publish Artifact Naming"
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs: inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll' targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll"
artifactName: 'Jellyfin.Naming' artifactName: "Jellyfin.Naming"
- task: PublishPipelineArtifact@1 - task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Controller' displayName: "Publish Artifact Controller"
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs: inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll' targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll"
artifactName: 'Jellyfin.Controller' artifactName: "Jellyfin.Controller"
- task: PublishPipelineArtifact@1 - task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Model' displayName: "Publish Artifact Model"
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs: inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll' targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll"
artifactName: 'Jellyfin.Model' artifactName: "Jellyfin.Model"
- task: PublishPipelineArtifact@1 - task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Common' displayName: "Publish Artifact Common"
condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
inputs: inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll' targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll"
artifactName: 'Jellyfin.Common' artifactName: "Jellyfin.Common"
- 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'
-269
View File
@@ -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
+19 -52
View File
@@ -1,25 +1,26 @@
parameters: parameters:
- name: ImageNames - name: ImageNames
type: object type: object
default: default:
Linux: "ubuntu-latest" Linux: "ubuntu-latest"
Windows: "windows-latest" Windows: "windows-latest"
macOS: "macos-latest" macOS: "macos-latest"
- name: TestProjects - name: TestProjects
type: string type: string
default: "tests/**/*Tests.csproj" default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion - name: DotNetSdkVersion
type: string type: string
default: 6.0.x default: 3.1.100
jobs: jobs:
- job: Test - job: MainTest
displayName: Test displayName: Main Test
strategy: strategy:
matrix: matrix:
${{ each imageName in parameters.ImageNames }}: ${{ each imageName in parameters.ImageNames }}:
${{ imageName.key }}: ${{ imageName.key }}:
ImageName: ${{ imageName.value }} ImageName: ${{ imageName.value }}
maxParallel: 3
pool: pool:
vmImage: "$(ImageName)" vmImage: "$(ImageName)"
steps: steps:
@@ -28,52 +29,25 @@ jobs:
submodules: true submodules: true
persistCredentials: false persistCredentials: false
# This is required for the SonarCloud analyzer
- task: UseDotNet@2
displayName: "Install .NET SDK 5.x"
condition: eq(variables['ImageName'], 'ubuntu-latest')
inputs:
packageType: sdk
version: '5.x'
- task: UseDotNet@2 - task: UseDotNet@2
displayName: "Update DotNet" displayName: "Update DotNet"
inputs: inputs:
packageType: sdk packageType: sdk
version: ${{ parameters.DotNetSdkVersion }} version: ${{ parameters.DotNetSdkVersion }}
- task: SonarCloudPrepare@1
displayName: 'Prepare analysis on SonarCloud'
condition: eq(variables['ImageName'], 'ubuntu-latest')
enabled: false
inputs:
SonarCloud: 'Sonarcloud for Jellyfin'
organization: 'jellyfin'
projectKey: 'jellyfin_jellyfin'
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Run CLI Tests' displayName: Run .NET Core CLI tests
inputs: inputs:
command: "test" command: "test"
projects: ${{ parameters.TestProjects }} projects: ${{ parameters.TestProjects }}
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal' arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
publishTestResults: true publishTestResults: true
testRunTitle: $(Agent.JobName) testRunTitle: $(Agent.JobName)
workingDirectory: "$(Build.SourcesDirectory)" workingDirectory: "$(Build.SourcesDirectory)"
- task: SonarCloudAnalyze@1
displayName: 'Run Code Analysis'
condition: eq(variables['ImageName'], 'ubuntu-latest')
enabled: false
- task: SonarCloudPublish@1
displayName: 'Publish Quality Gate Result'
condition: eq(variables['ImageName'], 'ubuntu-latest')
enabled: false
- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
displayName: 'Run ReportGenerator' displayName: ReportGenerator (merge)
inputs: inputs:
reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml" reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
targetdir: "$(Agent.TempDirectory)/merged/" targetdir: "$(Agent.TempDirectory)/merged/"
@@ -82,17 +56,10 @@ jobs:
## V2 is already in the repository but it does not work "wrong number of segments" YAML error. ## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
- task: PublishCodeCoverageResults@1 - task: PublishCodeCoverageResults@1
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
displayName: 'Publish Code Coverage' displayName: Publish Code Coverage
inputs: inputs:
codeCoverageTool: "cobertura" codeCoverageTool: "cobertura"
#summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2 #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml" summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
pathToSources: $(Build.SourcesDirectory) pathToSources: $(Build.SourcesDirectory)
failIfCoverageEmpty: true failIfCoverageEmpty: true
- task: PublishPipelineArtifact@1
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json"
artifactName: 'OpenAPI Spec'
+82
View 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"
+17 -31
View File
@@ -1,46 +1,38 @@
name: $(Date:yyyyMMdd)$(Rev:.r) name: $(Date:yyyyMMdd)$(Rev:.r)
variables: variables:
- name: TestProjects - name: TestProjects
value: 'tests/**/*Tests.csproj' value: "tests/**/*Tests.csproj"
- name: RestoreBuildProjects - name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj' value: "Jellyfin.Server/Jellyfin.Server.csproj"
- name: DotNetSdkVersion
value: 3.1.100
pr: pr:
autoCancel: true autoCancel: true
trigger: trigger:
batch: true batch: true
branches:
include:
- '*'
tags:
include:
- 'v*'
jobs: jobs:
- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}:
- template: azure-pipelines-main.yml - template: azure-pipelines-main.yml
parameters: parameters:
LinuxImage: 'ubuntu-latest' LinuxImage: "ubuntu-latest"
RestoreBuildProjects: $(RestoreBuildProjects) RestoreBuildProjects: $(RestoreBuildProjects)
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-test.yml - template: azure-pipelines-test.yml
parameters: parameters:
ImageNames: ImageNames:
Linux: 'ubuntu-latest' Linux: "ubuntu-latest"
Windows: 'windows-latest' Windows: "windows-latest"
macOS: 'macos-latest' macOS: "macos-latest"
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-test.yml
parameters:
ImageNames:
Linux: 'ubuntu-latest'
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: - template: azure-pipelines-windows.yml
- template: azure-pipelines-abi.yml parameters:
WindowsImage: "windows-latest"
TestProjects: $(TestProjects)
- template: azure-pipelines-compat.yml
parameters: parameters:
Packages: Packages:
Naming: Naming:
@@ -55,10 +47,4 @@ jobs:
Common: Common:
NugetPackageName: Jellyfin.Common NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll AssemblyFileName: MediaBrowser.Common.dll
Extensions: LinuxImage: "ubuntu-latest"
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
-1
View File
@@ -1 +0,0 @@
fedora
+59
View 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
View 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"
+10 -7
View File
@@ -13,7 +13,7 @@ charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
end_of_line = lf end_of_line = lf
max_line_length = off max_line_length = null
# YAML indentation # YAML indentation
[*.{yml,yaml}] [*.{yml,yaml}]
@@ -22,7 +22,6 @@ indent_size = 2
# XML indentation # XML indentation
[*.{csproj,xml}] [*.{csproj,xml}]
indent_size = 2 indent_size = 2
############################### ###############################
# .NET Coding Conventions # # .NET Coding Conventions #
############################### ###############################
@@ -52,12 +51,11 @@ dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = true:suggestion dotnet_style_coalesce_expression = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_auto_properties = true:silent dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_conditional_expression_over_assignment = true:silent dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent dotnet_style_prefer_conditional_expression_over_return = true:silent
############################### ###############################
# Naming Conventions # # Naming Conventions #
############################### ###############################
@@ -69,7 +67,7 @@ dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
@@ -161,7 +159,6 @@ csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion csharp_prefer_simple_default_expression = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion
############################### ###############################
# C# Formatting Rules # # C# Formatting Rules #
############################### ###############################
@@ -192,3 +189,9 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences # Wrapping preferences
csharp_preserve_single_line_statements = true csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true csharp_preserve_single_line_blocks = true
###############################
# VB Coding Conventions #
###############################
[*.vb]
# Modifier preferences
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion
-4
View File
@@ -1,4 +0,0 @@
# Joshua must review all changes to deployment and build.sh
.ci/* @joshuaboniface
deployment/* @joshuaboniface
build.sh @joshuaboniface
+43
View 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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).
+7 -17
View File
@@ -244,14 +244,14 @@ pip-log.txt
######################### #########################
# Artifacts for debian-x64 # Artifacts for debian-x64
debian/.debhelper/ deployment/debian-package-x64/pkg-src/.debhelper/
debian/*.debhelper deployment/debian-package-x64/pkg-src/*.debhelper
debian/debhelper-build-stamp deployment/debian-package-x64/pkg-src/debhelper-build-stamp
debian/files deployment/debian-package-x64/pkg-src/files
debian/jellyfin.substvars deployment/debian-package-x64/pkg-src/jellyfin.substvars
debian/jellyfin/ deployment/debian-package-x64/pkg-src/jellyfin/
# Don't ignore the debian/bin folder # Don't ignore the debian/bin folder
!debian/bin/ !deployment/debian-package-x64/pkg-src/bin/
deployment/**/dist/ deployment/**/dist/
deployment/**/pkg-dist/ deployment/**/pkg-dist/
@@ -268,16 +268,6 @@ doc/
# Deployment artifacts # Deployment artifacts
dist dist
*.exe *.exe
*.dll
# BenchmarkDotNet artifacts # BenchmarkDotNet artifacts
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
View File
@@ -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
View File
@@ -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": [
]
}
+9 -23
View File
@@ -1,30 +1,19 @@
{ {
"version": "0.2.0", // Use IntelliSense to find out which attributes exist for C# debugging
"configurations": [ // 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)", "name": ".NET Core Launch (console)",
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "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": [], "args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart",
"serverReadyAction": {
"action": "openExternally",
"pattern": "Overriding address\\(es\\) \\'(https?:\\S+)\\'",
}
},
{
"name": ".NET Core Launch (nowebclient)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",
"stopAtEntry": false, "stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart" "internalConsoleOptions": "openOnSessionStart"
@@ -35,8 +24,5 @@
"request": "attach", "request": "attach",
"processId": "${command:pickProcess}" "processId": "${command:pickProcess}"
} }
], ,]
"env": {
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
}
} }
+2 -17
View File
@@ -10,21 +10,6 @@
"${workspaceFolder}/Jellyfin.Server/Jellyfin.Server.csproj" "${workspaceFolder}/Jellyfin.Server/Jellyfin.Server.csproj"
], ],
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "api tests",
"command": "dotnet",
"type": "process",
"args": [
"test",
"${workspaceFolder}/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj"
],
"problemMatcher": "$msCompile"
} }
], ]
"options": { }
"env": {
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
}
}
}
-4
View File
@@ -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)
-36
View File
@@ -1,6 +1,5 @@
# Jellyfin Contributors # Jellyfin Contributors
- [1337joe](https://github.com/1337joe)
- [97carmine](https://github.com/97carmine) - [97carmine](https://github.com/97carmine)
- [Abbe98](https://github.com/Abbe98) - [Abbe98](https://github.com/Abbe98)
- [agrenott](https://github.com/agrenott) - [agrenott](https://github.com/agrenott)
@@ -8,8 +7,6 @@
- [anthonylavado](https://github.com/anthonylavado) - [anthonylavado](https://github.com/anthonylavado)
- [Artiume](https://github.com/Artiume) - [Artiume](https://github.com/Artiume)
- [AThomsen](https://github.com/AThomsen) - [AThomsen](https://github.com/AThomsen)
- [barongreenback](https://github.com/BaronGreenback)
- [barronpm](https://github.com/barronpm)
- [bilde2910](https://github.com/bilde2910) - [bilde2910](https://github.com/bilde2910)
- [bfayers](https://github.com/bfayers) - [bfayers](https://github.com/bfayers)
- [BnMcG](https://github.com/BnMcG) - [BnMcG](https://github.com/BnMcG)
@@ -18,8 +15,6 @@
- [bugfixin](https://github.com/bugfixin) - [bugfixin](https://github.com/bugfixin)
- [chaosinnovator](https://github.com/chaosinnovator) - [chaosinnovator](https://github.com/chaosinnovator)
- [ckcr4lyf](https://github.com/ckcr4lyf) - [ckcr4lyf](https://github.com/ckcr4lyf)
- [cocool97](https://github.com/cocool97)
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
- [crankdoofus](https://github.com/crankdoofus) - [crankdoofus](https://github.com/crankdoofus)
- [crobibero](https://github.com/crobibero) - [crobibero](https://github.com/crobibero)
- [cromefire](https://github.com/cromefire) - [cromefire](https://github.com/cromefire)
@@ -27,7 +22,6 @@
- [cvium](https://github.com/cvium) - [cvium](https://github.com/cvium)
- [dannymichel](https://github.com/dannymichel) - [dannymichel](https://github.com/dannymichel)
- [DaveChild](https://github.com/DaveChild) - [DaveChild](https://github.com/DaveChild)
- [Delgan](https://github.com/Delgan)
- [dcrdev](https://github.com/dcrdev) - [dcrdev](https://github.com/dcrdev)
- [dhartung](https://github.com/dhartung) - [dhartung](https://github.com/dhartung)
- [dinki](https://github.com/dinki) - [dinki](https://github.com/dinki)
@@ -36,7 +30,6 @@
- [dmitrylyzo](https://github.com/dmitrylyzo) - [dmitrylyzo](https://github.com/dmitrylyzo)
- [DMouse10462](https://github.com/DMouse10462) - [DMouse10462](https://github.com/DMouse10462)
- [DrPandemic](https://github.com/DrPandemic) - [DrPandemic](https://github.com/DrPandemic)
- [eglia](https://github.com/eglia)
- [EraYaN](https://github.com/EraYaN) - [EraYaN](https://github.com/EraYaN)
- [escabe](https://github.com/escabe) - [escabe](https://github.com/escabe)
- [excelite](https://github.com/excelite) - [excelite](https://github.com/excelite)
@@ -47,14 +40,11 @@
- [Froghut](https://github.com/Froghut) - [Froghut](https://github.com/Froghut)
- [fruhnow](https://github.com/fruhnow) - [fruhnow](https://github.com/fruhnow)
- [geilername](https://github.com/geilername) - [geilername](https://github.com/geilername)
- [GermanCoding](https://github.com/GermanCoding)
- [gnattu](https://github.com/gnattu) - [gnattu](https://github.com/gnattu)
- [GodTamIt](https://github.com/GodTamIt)
- [grafixeyehero](https://github.com/grafixeyehero) - [grafixeyehero](https://github.com/grafixeyehero)
- [h1nk](https://github.com/h1nk) - [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93) - [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017) - [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga) - [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h) - [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface) - [joshuaboniface](https://github.com/joshuaboniface)
@@ -64,7 +54,6 @@
- [Larvitar](https://github.com/Larvitar) - [Larvitar](https://github.com/Larvitar)
- [LeoVerto](https://github.com/LeoVerto) - [LeoVerto](https://github.com/LeoVerto)
- [Liggy](https://github.com/Liggy) - [Liggy](https://github.com/Liggy)
- [lmaonator](https://github.com/lmaonator)
- [LogicalPhallacy](https://github.com/LogicalPhallacy) - [LogicalPhallacy](https://github.com/LogicalPhallacy)
- [loli10K](https://github.com/loli10K) - [loli10K](https://github.com/loli10K)
- [lostmypillow](https://github.com/lostmypillow) - [lostmypillow](https://github.com/lostmypillow)
@@ -74,12 +63,10 @@
- [marius-luca-87](https://github.com/marius-luca-87) - [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro) - [mark-monteiro](https://github.com/mark-monteiro)
- [Matt07211](https://github.com/Matt07211) - [Matt07211](https://github.com/Matt07211)
- [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00) - [mcarlton00](https://github.com/mcarlton00)
- [mitchfizz05](https://github.com/mitchfizz05) - [mitchfizz05](https://github.com/mitchfizz05)
- [MrTimscampi](https://github.com/MrTimscampi) - [MrTimscampi](https://github.com/MrTimscampi)
- [n8225](https://github.com/n8225) - [n8225](https://github.com/n8225)
- [Nalsai](https://github.com/Nalsai)
- [Narfinger](https://github.com/Narfinger) - [Narfinger](https://github.com/Narfinger)
- [NathanPickard](https://github.com/NathanPickard) - [NathanPickard](https://github.com/NathanPickard)
- [neilsb](https://github.com/neilsb) - [neilsb](https://github.com/neilsb)
@@ -87,10 +74,7 @@
- [Nickbert7](https://github.com/Nickbert7) - [Nickbert7](https://github.com/Nickbert7)
- [nvllsvm](https://github.com/nvllsvm) - [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka) - [nyanmisaka](https://github.com/nyanmisaka)
- [OancaAndrei](https://github.com/OancaAndrei)
- [obradovichv](https://github.com/obradovichv)
- [oddstr13](https://github.com/oddstr13) - [oddstr13](https://github.com/oddstr13)
- [orryverducci](https://github.com/orryverducci)
- [petermcneil](https://github.com/petermcneil) - [petermcneil](https://github.com/petermcneil)
- [Phlogi](https://github.com/Phlogi) - [Phlogi](https://github.com/Phlogi)
- [pjeanjean](https://github.com/pjeanjean) - [pjeanjean](https://github.com/pjeanjean)
@@ -107,19 +91,14 @@
- [samuel9554](https://github.com/samuel9554) - [samuel9554](https://github.com/samuel9554)
- [scheidleon](https://github.com/scheidleon) - [scheidleon](https://github.com/scheidleon)
- [sebPomme](https://github.com/sebPomme) - [sebPomme](https://github.com/sebPomme)
- [SegiH](https://github.com/SegiH)
- [SenorSmartyPants](https://github.com/SenorSmartyPants) - [SenorSmartyPants](https://github.com/SenorSmartyPants)
- [shemanaev](https://github.com/shemanaev) - [shemanaev](https://github.com/shemanaev)
- [skaro13](https://github.com/skaro13) - [skaro13](https://github.com/skaro13)
- [sl1288](https://github.com/sl1288) - [sl1288](https://github.com/sl1288)
- [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004) - [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251) - [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
- [ssenart](https://github.com/ssenart)
- [stanionascu](https://github.com/stanionascu) - [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles) - [stevehayles](https://github.com/stevehayles)
- [StollD](https://github.com/StollD)
- [SuperSandro2000](https://github.com/SuperSandro2000) - [SuperSandro2000](https://github.com/SuperSandro2000)
- [tbraeutigam](https://github.com/tbraeutigam) - [tbraeutigam](https://github.com/tbraeutigam)
- [teacupx](https://github.com/teacupx) - [teacupx](https://github.com/teacupx)
@@ -149,16 +128,6 @@
- [XVicarious](https://github.com/XVicarious) - [XVicarious](https://github.com/XVicarious)
- [YouKnowBlom](https://github.com/YouKnowBlom) - [YouKnowBlom](https://github.com/YouKnowBlom)
- [KristupasSavickas](https://github.com/KristupasSavickas) - [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
- [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)
- [RealGreenDragon](https://github.com/RealGreenDragon)
# Emby Contributors # Emby Contributors
@@ -222,8 +191,3 @@
- [tikuf](https://github.com/tikuf/) - [tikuf](https://github.com/tikuf/)
- [Tim Hobbs](https://github.com/timhobbs) - [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande) - [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [olsh](https://github.com/olsh)
- [lbenini](https://github.com/lbenini)
- [gnuyent](https://github.com/gnuyent)
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [Jakob Kukla](https://github.com/jakobkukla)
-21
View File
@@ -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>
+40 -67
View File
@@ -1,18 +1,26 @@
# DESIGNED FOR BUILDING ON AMD64 ONLY ARG DOTNET_VERSION=3.1
##################################### ARG FFMPEG_VERSION=latest
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder FROM node:alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=10.5.4
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ RUN apk add curl git \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && git clone --branch release-10.5.z --single-branch https://github.com/jellyfin/jellyfin-web.git \
&& cd jellyfin-web-* \ && cd jellyfin-web \
&& npm ci --no-audit --unsafe-perm \ && git checkout tags/v${JELLYFIN_WEB_VERSION} \
&& yarn install \
&& yarn build \
&& mv dist /dist && 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 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
@@ -21,72 +29,37 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
# https://github.com/intel/compute-runtime/releases COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg
ARG GMMLIB_VERSION=22.0.2 COPY --from=builder /jellyfin /jellyfin
ARG IGC_VERSION=1.0.10395 COPY --from=web-builder /dist /jellyfin/jellyfin-web
ARG NEO_VERSION=22.08.22549
ARG LEVEL_ZERO_VERSION=1.3.22549
# Install dependencies: # Install dependencies:
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding. # libfontconfig1: needed for Skia
# curl: healthcheck # libgomp1: needed for ffmpeg
# libva-drm2: needed for ffmpeg
# mesa-va-drivers: needed for VAAPI
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https 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 \ && apt-get install --no-install-recommends --no-install-suggests -y \
libfontconfig1 \
libgomp1 \
libva-drm2 \
mesa-va-drivers \ mesa-va-drivers \
jellyfin-ffmpeg \
openssl \ openssl \
locales \ ca-certificates \
# Intel VAAPI Tone mapping dependencies: vainfo \
# Prefer NEO to Beignet since the latter one doesn't support Comet Lake or newer for now. i965-va-driver \
# Do not use the intel-opencl-icd package from repo since they will not build with RELEASE_WITH_REGKEYS enabled. && apt-get clean autoclean -y\
&& mkdir intel-compute-runtime \ && apt-get autoremove -y\
&& 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 \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \ && mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media \ && chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen && 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 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 -p:DebugType=none
FROM app
ENV HEALTHCHECK_URL=http://localhost:8096/health
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096 EXPOSE 8096
VOLUME /cache /config VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \ "--datadir", "/config", \
"--cachedir", "/cache", \ "--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] "--ffmpeg", "/usr/local/bin/ffmpeg"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
+27 -38
View File
@@ -1,20 +1,33 @@
# DESIGNED FOR BUILDING ON ARM ONLY # DESIGNED FOR BUILDING ON AMD64 ONLY
##################################### #####################################
# Requires binfm_misc registration # Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=6.0 ARG DOTNET_VERSION=3.1
FROM node:lts-alpine as web-builder FROM node:alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=10.5.4
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ RUN apk add curl git \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && git clone --branch release-10.5.z --single-branch https://github.com/jellyfin/jellyfin-web.git \
&& cd jellyfin-web-* \ && cd jellyfin-web \
&& npm ci --no-audit --unsafe-perm \ && git checkout tags/v${JELLYFIN_WEB_VERSION} \
&& yarn install \
&& yarn build \
&& mv dist /dist && 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 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 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
@@ -24,8 +37,6 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
# curl: setup & healthcheck
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \ && 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 - && \ curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
@@ -43,42 +54,20 @@ RUN apt-get update \
libraspberrypi0 \ libraspberrypi0 \
vainfo \ vainfo \
libva2 \ libva2 \
locales \ && apt-get remove curl gnupg -y \
&& apt-get remove gnupg -y \
&& apt-get clean autoclean -y \ && apt-get clean autoclean -y \
&& apt-get autoremove -y \ && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \ && mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media \ && chmod 777 /cache /config /media
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
# 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 -p:DebugType=none
FROM app
ENV HEALTHCHECK_URL=http://localhost:8096/health
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web COPY --from=web-builder /dist /jellyfin/jellyfin-web
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
EXPOSE 8096 EXPOSE 8096
VOLUME /cache /config VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \ "--datadir", "/config", \
"--cachedir", "/cache", \ "--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
+26 -39
View File
@@ -1,20 +1,32 @@
# DESIGNED FOR BUILDING ON ARM64 ONLY # DESIGNED FOR BUILDING ON AMD64 ONLY
##################################### #####################################
# Requires binfm_misc registration # Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=6.0 ARG DOTNET_VERSION=3.1
FROM node:lts-alpine as web-builder FROM node:alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=10.5.4
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ RUN apk add curl git \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && git clone --branch release-10.5.z --single-branch https://github.com/jellyfin/jellyfin-web.git \
&& cd jellyfin-web-* \ && cd jellyfin-web \
&& npm ci --no-audit --unsafe-perm \ && git checkout tags/v${JELLYFIN_WEB_VERSION} \
&& yarn install \
&& yarn build \
&& mv dist /dist && 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 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 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
@@ -24,9 +36,7 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
# curl: healcheck
RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
ffmpeg \ ffmpeg \
libssl-dev \ libssl-dev \
ca-certificates \ ca-certificates \
@@ -34,42 +44,19 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
libfreetype6 \ libfreetype6 \
libomxil-bellagio0 \ libomxil-bellagio0 \
libomxil-bellagio-bin \ libomxil-bellagio-bin \
locales \
curl \
&& apt-get clean autoclean -y \ && apt-get clean autoclean -y \
&& apt-get autoremove -y \ && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \ && mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media \ && chmod 777 /cache /config /media
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
# 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 -p:DebugType=none
FROM app
ENV HEALTHCHECK_URL=http://localhost:8096/health
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web COPY --from=web-builder /dist /jellyfin/jellyfin-web
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
EXPOSE 8096 EXPOSE 8096
VOLUME /cache /config VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \ "--datadir", "/config", \
"--cachedir", "/cache", \ "--cachedir", "/cache", \
"--ffmpeg", "/usr/bin/ffmpeg"] "--ffmpeg", "/usr/bin/ffmpeg"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
+10 -5
View File
@@ -1,6 +1,4 @@
#pragma warning disable CS1591 using System;
using System.Buffers.Binary;
using System.IO; using System.IO;
namespace DvdLib namespace DvdLib
@@ -14,12 +12,19 @@ namespace DvdLib
public override ushort ReadUInt16() public override ushort ReadUInt16()
{ {
return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2)); return BitConverter.ToUInt16(ReadAndReverseBytes(2), 0);
} }
public override uint ReadUInt32() public override uint ReadUInt32()
{ {
return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4)); return BitConverter.ToUInt32(ReadAndReverseBytes(4), 0);
}
private byte[] ReadAndReverseBytes(int count)
{
byte[] val = base.ReadBytes(count);
Array.Reverse(val, 0, count);
return val;
} }
} }
} }
+5 -8
View File
@@ -1,20 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<ProjectGuid>{713F42B5-878E-499D-A878-E4C652B1D5E8}</ProjectGuid>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\SharedVersion.cs" /> <Compile Include="..\SharedVersion.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
<Nullable>disable</Nullable>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
-3
View File
@@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System.IO; using System.IO;
namespace DvdLib.Ifo namespace DvdLib.Ifo
@@ -7,7 +5,6 @@ namespace DvdLib.Ifo
public class Cell public class Cell
{ {
public CellPlaybackInfo PlaybackInfo { get; private set; } public CellPlaybackInfo PlaybackInfo { get; private set; }
public CellPositionInfo PositionInfo { get; private set; } public CellPositionInfo PositionInfo { get; private set; }
internal void ParsePlayback(BinaryReader br) internal void ParsePlayback(BinaryReader br)
-2
View File
@@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System.IO; using System.IO;
namespace DvdLib.Ifo namespace DvdLib.Ifo
-2
View File
@@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System.IO; using System.IO;
namespace DvdLib.Ifo namespace DvdLib.Ifo
-4
View File
@@ -1,13 +1,9 @@
#pragma warning disable CS1591
namespace DvdLib.Ifo namespace DvdLib.Ifo
{ {
public class Chapter public class Chapter
{ {
public ushort ProgramChainNumber { get; private set; } public ushort ProgramChainNumber { get; private set; }
public ushort ProgramNumber { get; private set; } public ushort ProgramNumber { get; private set; }
public uint ChapterNumber { get; private set; } public uint ChapterNumber { get; private set; }
public Chapter(ushort pgcNum, ushort programNum, uint chapterNum) public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)
+12 -21
View File
@@ -1,10 +1,8 @@
#pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using MediaBrowser.Model.IO;
namespace DvdLib.Ifo namespace DvdLib.Ifo
{ {
@@ -15,10 +13,13 @@ namespace DvdLib.Ifo
private ushort _titleCount; private ushort _titleCount;
public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>(); public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>();
public Dvd(string path) private readonly IFileSystem _fileSystem;
public Dvd(string path, IFileSystem fileSystem)
{ {
_fileSystem = fileSystem;
Titles = new List<Title>(); Titles = new List<Title>();
var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories); var allFiles = _fileSystem.GetFiles(path, true).ToList();
var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ?? var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ??
allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase)); allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase));
@@ -32,7 +33,7 @@ namespace DvdLib.Ifo
continue; continue;
} }
var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries); var nums = ifo.Name.Split(new [] { '_' }, StringSplitOptions.RemoveEmptyEntries);
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber)) if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
{ {
ReadVTS(ifoNumber, ifo.FullName); ReadVTS(ifoNumber, ifo.FullName);
@@ -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)) ?? 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)); allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
@@ -118,19 +119,12 @@ namespace DvdLib.Ifo
uint chapNum = 1; uint chapNum = 1;
vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin); vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1)); var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
if (t == null) if (t == null) continue;
{
continue;
}
do do
{ {
t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum)); t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum));
if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1])) if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1])) break;
{
break;
}
chapNum++; chapNum++;
} }
while (vtsFs.Position < (baseAddr + endaddr)); while (vtsFs.Position < (baseAddr + endaddr));
@@ -155,10 +149,7 @@ namespace DvdLib.Ifo
uint vtsPgcOffset = vtsRead.ReadUInt32(); uint vtsPgcOffset = vtsRead.ReadUInt32();
var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum)); var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum));
if (t != null) if (t != null) t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
{
t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
}
} }
} }
} }
+2 -10
View File
@@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System; using System;
namespace DvdLib.Ifo namespace DvdLib.Ifo
@@ -15,14 +13,8 @@ namespace DvdLib.Ifo
Second = GetBCDValue(data[2]); Second = GetBCDValue(data[2]);
Frames = GetBCDValue((byte)(data[3] & 0x3F)); Frames = GetBCDValue((byte)(data[3] & 0x3F));
if ((data[3] & 0x80) != 0) if ((data[3] & 0x80) != 0) FrameRate = 30;
{ else if ((data[3] & 0x40) != 0) FrameRate = 25;
FrameRate = 30;
}
else if ((data[3] & 0x40) != 0)
{
FrameRate = 25;
}
} }
private static byte GetBCDValue(byte data) private static byte GetBCDValue(byte data)
+1 -3
View File
@@ -1,12 +1,10 @@
#pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
namespace DvdLib.Ifo namespace DvdLib.Ifo
{ {
public class Program public class Program
{ {
public IReadOnlyList<Cell> Cells { get; } public readonly List<Cell> Cells;
public Program(List<Cell> cells) public Program(List<Cell> cells)
{ {
+2 -15
View File
@@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -22,9 +20,7 @@ namespace DvdLib.Ifo
public readonly List<Cell> Cells; public readonly List<Cell> Cells;
public DvdTime PlaybackTime { get; private set; } public DvdTime PlaybackTime { get; private set; }
public UserOperation ProhibitedUserOperations { get; private set; } public UserOperation ProhibitedUserOperations { get; private set; }
public byte[] AudioStreamControl { get; private set; } // 8*2 entries public byte[] AudioStreamControl { get; private set; } // 8*2 entries
public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
@@ -35,11 +31,9 @@ namespace DvdLib.Ifo
private ushort _goupProgramNumber; private ushort _goupProgramNumber;
public ProgramPlaybackMode PlaybackMode { get; private set; } public ProgramPlaybackMode PlaybackMode { get; private set; }
public uint ProgramCount { get; private set; } public uint ProgramCount { get; private set; }
public byte StillTime { get; private set; } public byte StillTime { get; private set; }
public byte[] Palette { get; private set; } // 16*4 entries public byte[] Palette { get; private set; } // 16*4 entries
private ushort _commandTableOffset; private ushort _commandTableOffset;
@@ -75,15 +69,8 @@ namespace DvdLib.Ifo
StillTime = br.ReadByte(); StillTime = br.ReadByte();
byte pbMode = br.ReadByte(); byte pbMode = br.ReadByte();
if (pbMode == 0) if (pbMode == 0) PlaybackMode = ProgramPlaybackMode.Sequential;
{ else PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
PlaybackMode = ProgramPlaybackMode.Sequential;
}
else
{
PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
}
ProgramCount = (uint)(pbMode & 0x7F); ProgramCount = (uint)(pbMode & 0x7F);
Palette = br.ReadBytes(64); Palette = br.ReadBytes(64);
+1 -10
View File
@@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -8,11 +6,8 @@ namespace DvdLib.Ifo
public class Title public class Title
{ {
public uint TitleNumber { get; private set; } public uint TitleNumber { get; private set; }
public uint AngleCount { get; private set; } public uint AngleCount { get; private set; }
public ushort ChapterCount { get; private set; } public ushort ChapterCount { get; private set; }
public byte VideoTitleSetNumber { get; private set; } public byte VideoTitleSetNumber { get; private set; }
private ushort _parentalManagementMask; private ushort _parentalManagementMask;
@@ -20,7 +15,6 @@ namespace DvdLib.Ifo
private uint _vtsStartSector; // relative to start of entire disk private uint _vtsStartSector; // relative to start of entire disk
public ProgramChain EntryProgramChain { get; private set; } public ProgramChain EntryProgramChain { get; private set; }
public readonly List<ProgramChain> ProgramChains; public readonly List<ProgramChain> ProgramChains;
public readonly List<Chapter> Chapters; public readonly List<Chapter> Chapters;
@@ -59,10 +53,7 @@ namespace DvdLib.Ifo
var pgc = new ProgramChain(pgcNum); var pgc = new ProgramChain(pgcNum);
pgc.ParseHeader(br); pgc.ParseHeader(br);
ProgramChains.Add(pgc); ProgramChains.Add(pgc);
if (entryPgc) if (entryPgc) EntryProgramChain = pgc;
{
EntryProgramChain = pgc;
}
br.BaseStream.Seek(curPos, SeekOrigin.Begin); br.BaseStream.Seek(curPos, SeekOrigin.Begin);
} }
-2
View File
@@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System; using System;
namespace DvdLib.Ifo namespace DvdLib.Ifo
+374
View 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
View 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);
}
}
}
+6 -15
View File
@@ -1,23 +1,14 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
namespace Emby.Dlna.Common namespace Emby.Dlna.Common
{ {
/// <summary>
/// DLNA Query parameter type, used when querying DLNA devices via SOAP.
/// </summary>
public class Argument public class Argument
{ {
/// <summary> public string Name { get; set; }
/// Gets or sets name of the DLNA argument.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary> public string Direction { get; set; }
/// Gets or sets the direction of the parameter.
/// </summary>
public string Direction { get; set; } = string.Empty;
/// <summary> public string RelatedStateVariable { get; set; }
/// Gets or sets the related DLNA state variable for this argument.
/// </summary>
public string RelatedStateVariable { get; set; } = string.Empty;
} }
} }
+11 -22
View File
@@ -1,41 +1,30 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System.Globalization; using System.Globalization;
namespace Emby.Dlna.Common namespace Emby.Dlna.Common
{ {
/// <summary>
/// Defines the <see cref="DeviceIcon" />.
/// </summary>
public class DeviceIcon public class DeviceIcon
{ {
/// <summary> public string Url { get; set; }
/// Gets or sets the Url.
/// </summary>
public string Url { get; set; } = string.Empty;
/// <summary> public string MimeType { get; set; }
/// Gets or sets the MimeType.
/// </summary>
public string MimeType { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the Width.
/// </summary>
public int Width { get; set; } public int Width { get; set; }
/// <summary>
/// Gets or sets the Height.
/// </summary>
public int Height { get; set; } public int Height { get; set; }
/// <summary> public string Depth { get; set; }
/// Gets or sets the Depth.
/// </summary>
public string Depth { get; set; } = string.Empty;
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return string.Format(CultureInfo.InvariantCulture, "{0}x{1}", Height, Width); return string.Format(
CultureInfo.InvariantCulture,
"{0}x{1}",
Height,
Width);
} }
} }
} }
+10 -24
View File
@@ -1,36 +1,22 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
namespace Emby.Dlna.Common namespace Emby.Dlna.Common
{ {
/// <summary>
/// Defines the <see cref="DeviceService" />.
/// </summary>
public class DeviceService public class DeviceService
{ {
/// <summary> public string ServiceType { get; set; }
/// Gets or sets the Service Type.
/// </summary>
public string ServiceType { get; set; } = string.Empty;
/// <summary> public string ServiceId { get; set; }
/// Gets or sets the Service Id.
/// </summary>
public string ServiceId { get; set; } = string.Empty;
/// <summary> public string ScpdUrl { get; set; }
/// Gets or sets the Scpd Url.
/// </summary>
public string ScpdUrl { get; set; } = string.Empty;
/// <summary> public string ControlUrl { get; set; }
/// Gets or sets the Control Url.
/// </summary>
public string ControlUrl { get; set; } = string.Empty;
/// <summary> public string EventSubUrl { get; set; }
/// Gets or sets the EventSubUrl.
/// </summary>
public string EventSubUrl { get; set; } = string.Empty;
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() => ServiceId; public override string ToString()
=> ServiceId;
} }
} }
+9 -15
View File
@@ -1,31 +1,25 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic; using System.Collections.Generic;
namespace Emby.Dlna.Common namespace Emby.Dlna.Common
{ {
/// <summary>
/// Defines the <see cref="ServiceAction" />.
/// </summary>
public class ServiceAction public class ServiceAction
{ {
/// <summary>
/// Initializes a new instance of the <see cref="ServiceAction"/> class.
/// </summary>
public ServiceAction() public ServiceAction()
{ {
ArgumentList = new List<Argument>(); ArgumentList = new List<Argument>();
} }
/// <summary> public string Name { get; set; }
/// Gets or sets the name of the action.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary> public List<Argument> ArgumentList { get; set; }
/// Gets the ArgumentList.
/// </summary>
public List<Argument> ArgumentList { get; }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() => Name; public override string ToString()
{
return Name;
}
} }
} }
+13 -20
View File
@@ -1,34 +1,27 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
using System.Collections.Generic;
namespace Emby.Dlna.Common namespace Emby.Dlna.Common
{ {
/// <summary>
/// Defines the <see cref="StateVariable" />.
/// </summary>
public class StateVariable public class StateVariable
{ {
/// <summary> public StateVariable()
/// Gets or sets the name of the state variable. {
/// </summary> AllowedValues = Array.Empty<string>();
public string Name { get; set; } = string.Empty; }
/// <summary> public string Name { get; set; }
/// Gets or sets the data type of the state variable.
/// </summary> public string DataType { get; set; }
public string DataType { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether it sends events.
/// </summary>
public bool SendsEvents { get; set; } public bool SendsEvents { get; set; }
/// <summary> public string[] AllowedValues { get; set; }
/// Gets or sets the allowed values range.
/// </summary>
public IReadOnlyList<string> AllowedValues { get; set; } = Array.Empty<string>();
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() => Name; public override string ToString()
=> Name;
} }
} }
+8 -66
View File
@@ -1,92 +1,34 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
namespace Emby.Dlna.Configuration namespace Emby.Dlna.Configuration
{ {
/// <summary>
/// The DlnaOptions class contains the user definable parameters for the dlna subsystems.
/// </summary>
public class DlnaOptions public class DlnaOptions
{ {
/// <summary>
/// Initializes a new instance of the <see cref="DlnaOptions"/> class.
/// </summary>
public DlnaOptions() public DlnaOptions()
{ {
EnablePlayTo = true; EnablePlayTo = true;
EnableServer = false; EnableServer = true;
BlastAliveMessages = true; BlastAliveMessages = true;
SendOnlyMatchedHost = true; SendOnlyMatchedHost = true;
ClientDiscoveryIntervalSeconds = 60; ClientDiscoveryIntervalSeconds = 60;
AliveMessageIntervalSeconds = 1800; BlastAliveMessageIntervalSeconds = 1800;
} }
/// <summary>
/// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna playTo subsystem.
/// </summary>
public bool EnablePlayTo { get; set; } public bool EnablePlayTo { get; set; }
/// <summary>
/// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna server subsystem.
/// </summary>
public bool EnableServer { get; set; } public bool EnableServer { get; set; }
/// <summary>
/// Gets or sets a value indicating whether detailed dlna server logs are sent to the console/log.
/// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
/// </summary>
public bool EnableDebugLog { get; set; } public bool EnableDebugLog { get; set; }
/// <summary> public bool BlastAliveMessages { get; set; }
/// Gets or sets a value indicating whether whether detailed playTo debug logs are sent to the console/log.
/// If the setting "Emby.Dlna.PlayTo": "Debug" msut be set in logging.default.json for this property to work. public bool SendOnlyMatchedHost { get; set; }
/// </summary>
public bool EnablePlayToTracing { get; set; }
/// <summary>
/// Gets or sets the ssdp client discovery interval time (in seconds).
/// This is the time after which the server will send a ssdp search request.
/// </summary>
public int ClientDiscoveryIntervalSeconds { get; set; } public int ClientDiscoveryIntervalSeconds { get; set; }
/// <summary> public int BlastAliveMessageIntervalSeconds { get; set; }
/// Gets or sets the frequency at which ssdp alive notifications are transmitted.
/// </summary>
public int AliveMessageIntervalSeconds { get; set; }
/// <summary> public string DefaultUserId { get; set; }
/// 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;
} }
} }
+17
View File
@@ -1,5 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic;
using Emby.Dlna.Configuration; using Emby.Dlna.Configuration;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@@ -12,4 +14,19 @@ namespace Emby.Dlna
return manager.GetConfiguration<DlnaOptions>("dlna"); return manager.GetConfiguration<DlnaOptions>("dlna");
} }
} }
public class DlnaConfigurationFactory : IConfigurationFactory
{
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new ConfigurationStore[]
{
new ConfigurationStore
{
Key = "dlna",
ConfigurationType = typeof (DlnaOptions)
}
};
}
}
} }
@@ -0,0 +1,42 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System.Threading.Tasks;
using Emby.Dlna.Service;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager
{
public class ConnectionManager : BaseService, IConnectionManager
{
private readonly IDlnaManager _dlna;
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient)
: base(logger, httpClient)
{
_dlna = dlna;
_config = config;
_logger = logger;
}
/// <inheritdoc />
public string GetServiceXml()
{
return new ConnectionManagerXmlBuilder().GetXml();
}
/// <inheritdoc />
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{
var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile();
return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request);
}
}
}
@@ -1,53 +0,0 @@
#pragma warning disable CS1591
using System.Net.Http;
using System.Threading.Tasks;
using Emby.Dlna.Service;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager
{
/// <summary>
/// Defines the <see cref="ConnectionManagerService" />.
/// </summary>
public class ConnectionManagerService : BaseService, IConnectionManager
{
private readonly IDlnaManager _dlna;
private readonly IServerConfigurationManager _config;
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionManagerService"/> class.
/// </summary>
/// <param name="dlna">The <see cref="IDlnaManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
/// <param name="logger">The <see cref="ILogger{ConnectionManagerService}"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
public ConnectionManagerService(
IDlnaManager dlna,
IServerConfigurationManager config,
ILogger<ConnectionManagerService> logger,
IHttpClientFactory httpClientFactory)
: base(logger, httpClientFactory)
{
_dlna = dlna;
_config = config;
}
/// <inheritdoc />
public string GetServiceXml()
{
return ConnectionManagerXmlBuilder.GetXml();
}
/// <inheritdoc />
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{
var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile();
return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
}
}
}
@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
@@ -6,57 +7,45 @@ using Emby.Dlna.Service;
namespace Emby.Dlna.ConnectionManager namespace Emby.Dlna.ConnectionManager
{ {
/// <summary> public class ConnectionManagerXmlBuilder
/// Defines the <see cref="ConnectionManagerXmlBuilder" />.
/// </summary>
public static class ConnectionManagerXmlBuilder
{ {
/// <summary> public string GetXml()
/// Gets the ConnectionManager:1 service template.
/// See http://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf.
/// </summary>
/// <returns>An XML description of this service.</returns>
public static string GetXml()
{ {
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables()); return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), GetStateVariables());
} }
/// <summary>
/// Get the list of state variables for this invocation.
/// </summary>
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables() private static IEnumerable<StateVariable> GetStateVariables()
{ {
var list = new List<StateVariable> var list = new List<StateVariable>();
list.Add(new StateVariable
{ {
new StateVariable Name = "SourceProtocolInfo",
{ DataType = "string",
Name = "SourceProtocolInfo", SendsEvents = true
DataType = "string", });
SendsEvents = true
},
new StateVariable list.Add(new StateVariable
{ {
Name = "SinkProtocolInfo", Name = "SinkProtocolInfo",
DataType = "string", DataType = "string",
SendsEvents = true SendsEvents = true
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "CurrentConnectionIDs", Name = "CurrentConnectionIDs",
DataType = "string", DataType = "string",
SendsEvents = true SendsEvents = true
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_ConnectionStatus", Name = "A_ARG_TYPE_ConnectionStatus",
DataType = "string", DataType = "string",
SendsEvents = false, SendsEvents = false,
AllowedValues = new[] AllowedValues = new string[]
{ {
"OK", "OK",
"ContentFormatMismatch", "ContentFormatMismatch",
@@ -64,56 +53,55 @@ namespace Emby.Dlna.ConnectionManager
"UnreliableChannel", "UnreliableChannel",
"Unknown" "Unknown"
} }
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_ConnectionManager", Name = "A_ARG_TYPE_ConnectionManager",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_Direction", Name = "A_ARG_TYPE_Direction",
DataType = "string", DataType = "string",
SendsEvents = false, SendsEvents = false,
AllowedValues = new[] AllowedValues = new string[]
{ {
"Output", "Output",
"Input" "Input"
} }
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_ProtocolInfo", Name = "A_ARG_TYPE_ProtocolInfo",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_ConnectionID", Name = "A_ARG_TYPE_ConnectionID",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_AVTransportID", Name = "A_ARG_TYPE_AVTransportID",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_RcsID", Name = "A_ARG_TYPE_RcsID",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
} });
};
return list; return list;
} }
+2 -14
View File
@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -11,19 +12,10 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager namespace Emby.Dlna.ConnectionManager
{ {
/// <summary>
/// Defines the <see cref="ControlHandler" />.
/// </summary>
public class ControlHandler : BaseControlHandler public class ControlHandler : BaseControlHandler
{ {
private readonly DeviceProfile _profile; private readonly DeviceProfile _profile;
/// <summary>
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
/// </summary>
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
/// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param>
public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile) public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile)
: base(config, logger) : base(config, logger)
{ {
@@ -31,7 +23,7 @@ namespace Emby.Dlna.ConnectionManager
} }
/// <inheritdoc /> /// <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)) if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
{ {
@@ -42,10 +34,6 @@ namespace Emby.Dlna.ConnectionManager
throw new ResourceNotFoundException("Unexpected control request name: " + methodName); throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
} }
/// <summary>
/// Builds the response to the GetProtocolInfo request.
/// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private void HandleGetProtocolInfo(XmlWriter xmlWriter) private void HandleGetProtocolInfo(XmlWriter xmlWriter)
{ {
xmlWriter.WriteElementString("Source", _profile.ProtocolInfo); xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);
@@ -1,20 +1,14 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
namespace Emby.Dlna.ConnectionManager namespace Emby.Dlna.ConnectionManager
{ {
/// <summary> public class ServiceActionListBuilder
/// Defines the <see cref="ServiceActionListBuilder" />.
/// </summary>
public static class ServiceActionListBuilder
{ {
/// <summary> public IEnumerable<ServiceAction> GetActions()
/// Returns an enumerable of the ConnectionManagar:1 DLNA actions.
/// </summary>
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
public static IEnumerable<ServiceAction> GetActions()
{ {
var list = new List<ServiceAction> var list = new List<ServiceAction>
{ {
@@ -28,10 +22,6 @@ namespace Emby.Dlna.ConnectionManager
return list; return list;
} }
/// <summary>
/// Returns the action details for "PrepareForConnection".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction PrepareForConnection() private static ServiceAction PrepareForConnection()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -91,10 +81,6 @@ namespace Emby.Dlna.ConnectionManager
return action; return action;
} }
/// <summary>
/// Returns the action details for "GetCurrentConnectionInfo".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetCurrentConnectionInfo() private static ServiceAction GetCurrentConnectionInfo()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -161,11 +147,7 @@ namespace Emby.Dlna.ConnectionManager
return action; return action;
} }
/// <summary> private ServiceAction GetProtocolInfo()
/// Returns the action details for "GetProtocolInfo".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetProtocolInfo()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@@ -189,11 +171,7 @@ namespace Emby.Dlna.ConnectionManager
return action; return action;
} }
/// <summary> private ServiceAction GetCurrentConnectionIDs()
/// Returns the action details for "GetCurrentConnectionIDs".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetCurrentConnectionIDs()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@@ -210,11 +188,7 @@ namespace Emby.Dlna.ConnectionManager
return action; return action;
} }
/// <summary> private ServiceAction ConnectionComplete()
/// Returns the action details for "ConnectionComplete".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction ConnectionComplete()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@@ -1,15 +1,14 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using Jellyfin.Data.Entities; using MediaBrowser.Common.Net;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.TV; using MediaBrowser.Controller.TV;
@@ -19,10 +18,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ContentDirectory namespace Emby.Dlna.ContentDirectory
{ {
/// <summary> public class ContentDirectory : BaseService, IContentDirectory
/// Defines the <see cref="ContentDirectoryService" />.
/// </summary>
public class ContentDirectoryService : BaseService, IContentDirectory
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
@@ -36,31 +32,14 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ITVSeriesManager _tvSeriesManager; private readonly ITVSeriesManager _tvSeriesManager;
/// <summary> public ContentDirectory(IDlnaManager dlna,
/// Initializes a new instance of the <see cref="ContentDirectoryService"/> class.
/// </summary>
/// <param name="dlna">The <see cref="IDlnaManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="userDataManager">The <see cref="IUserDataManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="imageProcessor">The <see cref="IImageProcessor"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="libraryManager">The <see cref="ILibraryManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="config">The <see cref="IServerConfigurationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="userManager">The <see cref="IUserManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="logger">The <see cref="ILogger{ContentDirectoryService}"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="httpClient">The <see cref="IHttpClientFactory"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="localization">The <see cref="ILocalizationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="userViewManager">The <see cref="IUserViewManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
public ContentDirectoryService(
IDlnaManager dlna,
IUserDataManager userDataManager, IUserDataManager userDataManager,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IServerConfigurationManager config, IServerConfigurationManager config,
IUserManager userManager, IUserManager userManager,
ILogger<ContentDirectoryService> logger, ILogger logger,
IHttpClientFactory httpClient, IHttpClient httpClient,
ILocalizationManager localization, ILocalizationManager localization,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IUserViewManager userViewManager, IUserViewManager userViewManager,
@@ -81,10 +60,7 @@ namespace Emby.Dlna.ContentDirectory
_tvSeriesManager = tvSeriesManager; _tvSeriesManager = tvSeriesManager;
} }
/// <summary> private int SystemUpdateId
/// Gets the system id. (A unique id which changes on when our definition changes.)
/// </summary>
private static int SystemUpdateId
{ {
get get
{ {
@@ -97,18 +73,14 @@ namespace Emby.Dlna.ContentDirectory
/// <inheritdoc /> /// <inheritdoc />
public string GetServiceXml() public string GetServiceXml()
{ {
return ContentDirectoryXmlBuilder.GetXml(); return new ContentDirectoryXmlBuilder().GetXml();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request) public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{ {
if (request == null) var profile = _dlna.GetProfile(request.Headers) ??
{ _dlna.GetDefaultProfile();
throw new ArgumentNullException(nameof(request));
}
var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile();
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase)); var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
@@ -133,12 +105,7 @@ namespace Emby.Dlna.ContentDirectory
.ProcessControlRequestAsync(request); .ProcessControlRequestAsync(request);
} }
/// <summary> private User GetUser(DeviceProfile profile)
/// 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)
{ {
if (!string.IsNullOrEmpty(profile.UserId)) if (!string.IsNullOrEmpty(profile.UserId))
{ {
@@ -164,13 +131,18 @@ namespace Emby.Dlna.ContentDirectory
foreach (var user in _userManager.Users) foreach (var user in _userManager.Users)
{ {
if (user.HasPermission(PermissionKind.IsAdministrator)) if (user.Policy.IsAdministrator)
{ {
return user; return user;
} }
} }
return _userManager.Users.FirstOrDefault(); foreach (var user in _userManager.Users)
{
return user;
}
return null;
} }
} }
} }
@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
@@ -6,154 +7,142 @@ using Emby.Dlna.Service;
namespace Emby.Dlna.ContentDirectory namespace Emby.Dlna.ContentDirectory
{ {
/// <summary> public class ContentDirectoryXmlBuilder
/// Defines the <see cref="ContentDirectoryXmlBuilder" />.
/// </summary>
public static class ContentDirectoryXmlBuilder
{ {
/// <summary> public string GetXml()
/// Gets the ContentDirectory:1 service template.
/// See http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf.
/// </summary>
/// <returns>An XML description of this service.</returns>
public static string GetXml()
{ {
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables()); return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
GetStateVariables());
} }
/// <summary>
/// Get the list of state variables for this invocation.
/// </summary>
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables() private static IEnumerable<StateVariable> GetStateVariables()
{ {
var list = new List<StateVariable> var list = new List<StateVariable>();
list.Add(new StateVariable
{ {
new StateVariable Name = "A_ARG_TYPE_Filter",
{ DataType = "string",
Name = "A_ARG_TYPE_Filter", SendsEvents = false
DataType = "string", });
SendsEvents = false
},
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_SortCriteria", Name = "A_ARG_TYPE_SortCriteria",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_Index", Name = "A_ARG_TYPE_Index",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_Count", Name = "A_ARG_TYPE_Count",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_UpdateID", Name = "A_ARG_TYPE_UpdateID",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "SearchCapabilities", Name = "SearchCapabilities",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "SortCapabilities", Name = "SortCapabilities",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "SystemUpdateID", Name = "SystemUpdateID",
DataType = "ui4", DataType = "ui4",
SendsEvents = true SendsEvents = true
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_SearchCriteria", Name = "A_ARG_TYPE_SearchCriteria",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_Result", Name = "A_ARG_TYPE_Result",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_ObjectID", Name = "A_ARG_TYPE_ObjectID",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_BrowseFlag", Name = "A_ARG_TYPE_BrowseFlag",
DataType = "string", DataType = "string",
SendsEvents = false, SendsEvents = false,
AllowedValues = new[] AllowedValues = new string[]
{ {
"BrowseMetadata", "BrowseMetadata",
"BrowseDirectChildren" "BrowseDirectChildren"
} }
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_BrowseLetter", Name = "A_ARG_TYPE_BrowseLetter",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_CategoryType", Name = "A_ARG_TYPE_CategoryType",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_RID", Name = "A_ARG_TYPE_RID",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_PosSec", Name = "A_ARG_TYPE_PosSec",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_Featurelist", Name = "A_ARG_TYPE_Featurelist",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
} });
};
return list; return list;
} }
File diff suppressed because it is too large Load Diff
-39
View File
@@ -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 System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
namespace Emby.Dlna.ContentDirectory namespace Emby.Dlna.ContentDirectory
{ {
/// <summary> public class ServiceActionListBuilder
/// Defines the <see cref="ServiceActionListBuilder" />.
/// </summary>
public static class ServiceActionListBuilder
{ {
/// <summary> public IEnumerable<ServiceAction> GetActions()
/// Returns a list of services that this instance provides.
/// </summary>
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
public static IEnumerable<ServiceAction> GetActions()
{ {
return new[] return new[]
{ {
@@ -27,10 +23,6 @@ namespace Emby.Dlna.ContentDirectory
}; };
} }
/// <summary>
/// Returns the action details for "GetSystemUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetSystemUpdateIDAction() private static ServiceAction GetGetSystemUpdateIDAction()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -48,10 +40,6 @@ namespace Emby.Dlna.ContentDirectory
return action; return action;
} }
/// <summary>
/// Returns the action details for "GetSearchCapabilities".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetSearchCapabilitiesAction() private static ServiceAction GetSearchCapabilitiesAction()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -69,10 +57,6 @@ namespace Emby.Dlna.ContentDirectory
return action; return action;
} }
/// <summary>
/// Returns the action details for "GetSortCapabilities".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetSortCapabilitiesAction() private static ServiceAction GetSortCapabilitiesAction()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -90,10 +74,6 @@ namespace Emby.Dlna.ContentDirectory
return action; return action;
} }
/// <summary>
/// Returns the action details for "X_GetFeatureList".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetX_GetFeatureListAction() private static ServiceAction GetX_GetFeatureListAction()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -111,10 +91,6 @@ namespace Emby.Dlna.ContentDirectory
return action; return action;
} }
/// <summary>
/// Returns the action details for "Search".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetSearchAction() private static ServiceAction GetSearchAction()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -195,11 +171,7 @@ namespace Emby.Dlna.ContentDirectory
return action; return action;
} }
/// <summary> private ServiceAction GetBrowseAction()
/// Returns the action details for "Browse".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetBrowseAction()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@@ -279,11 +251,7 @@ namespace Emby.Dlna.ContentDirectory
return action; return action;
} }
/// <summary> private ServiceAction GetBrowseByLetterAction()
/// Returns the action details for "X_BrowseByLetter".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetBrowseByLetterAction()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@@ -370,11 +338,7 @@ namespace Emby.Dlna.ContentDirectory
return action; return action;
} }
/// <summary> private ServiceAction GetXSetBookmarkAction()
/// Returns the action details for "X_SetBookmark".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetXSetBookmarkAction()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
-30
View File
@@ -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
}
}
+7 -8
View File
@@ -1,6 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System.IO; using System.IO;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@@ -9,17 +8,17 @@ namespace Emby.Dlna
{ {
public class ControlRequest public class ControlRequest
{ {
public ControlRequest(IHeaderDictionary headers) public IHeaderDictionary Headers { get; set; }
{
Headers = headers;
}
public IHeaderDictionary Headers { get; }
public Stream InputXml { get; set; } public Stream InputXml { get; set; }
public string TargetServerUuId { get; set; } public string TargetServerUuId { get; set; }
public string RequestedUrl { get; set; } public string RequestedUrl { get; set; }
public ControlRequest()
{
Headers = new HeaderDictionary();
}
} }
} }
+3 -10
View File
@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic; using System.Collections.Generic;
@@ -6,23 +7,15 @@ namespace Emby.Dlna
{ {
public class ControlResponse public class ControlResponse
{ {
public ControlResponse(string xml, bool isSuccessful) public ControlResponse()
{ {
Headers = new Dictionary<string, string>(); 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 string Xml { get; set; }
public bool IsSuccessful { get; set; } public bool IsSuccessful { get; set; }
/// <inheritdoc />
public override string ToString()
{
return Xml;
}
} }
} }
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -1,6 +1,8 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
using MediaBrowser.Model.Extensions;
namespace Emby.Dlna.Didl namespace Emby.Dlna.Didl
{ {
@@ -12,17 +14,21 @@ namespace Emby.Dlna.Didl
public Filter() public Filter()
: this("*") : this("*")
{ {
} }
public Filter(string filter) public Filter(string filter)
{ {
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase); _all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
_fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
_fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
} }
public bool Contains(string field) public bool Contains(string field)
{ {
return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase)); // Don't bother with this. Some clients (media monkey) use the filter and then don't display very well when very little data comes back.
return true;
//return _all || ListHelper.ContainsIgnoreCase(_fields, field);
} }
} }
} }
+4 -3
View File
@@ -1,5 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable CA1305 #pragma warning disable SA1600
using System; using System;
using System.IO; using System.IO;
@@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
{ {
public class StringWriterWithEncoding : StringWriter public class StringWriterWithEncoding : StringWriter
{ {
private readonly Encoding? _encoding; private readonly Encoding _encoding;
public StringWriterWithEncoding() public StringWriterWithEncoding()
{ {
@@ -30,6 +30,7 @@ namespace Emby.Dlna.Didl
{ {
} }
public StringWriterWithEncoding(Encoding encoding) public StringWriterWithEncoding(Encoding encoding)
{ {
_encoding = encoding; _encoding = encoding;
@@ -53,6 +54,6 @@ namespace Emby.Dlna.Didl
_encoding = encoding; _encoding = encoding;
} }
public override Encoding Encoding => _encoding ?? base.Encoding; public override Encoding Encoding => (null == _encoding) ? base.Encoding : _encoding;
} }
} }
-23
View File
@@ -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)
}
};
}
}
}
+151 -126
View File
@@ -1,16 +1,17 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Profiles; using Emby.Dlna.Profiles;
using Emby.Dlna.Server; using Emby.Dlna.Server;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@@ -31,10 +32,10 @@ namespace Emby.Dlna
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILogger<DlnaManager> _logger; private readonly ILogger _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal); private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
@@ -43,24 +44,22 @@ namespace Emby.Dlna
IFileSystem fileSystem, IFileSystem fileSystem,
IApplicationPaths appPaths, IApplicationPaths appPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IJsonSerializer jsonSerializer,
IServerApplicationHost appHost) IServerApplicationHost appHost)
{ {
_xmlSerializer = xmlSerializer; _xmlSerializer = xmlSerializer;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_appPaths = appPaths; _appPaths = appPaths;
_logger = loggerFactory.CreateLogger<DlnaManager>(); _logger = loggerFactory.CreateLogger("Dlna");
_jsonSerializer = jsonSerializer;
_appHost = appHost; _appHost = appHost;
} }
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
public async Task InitProfilesAsync() public async Task InitProfilesAsync()
{ {
try try
{ {
await ExtractSystemProfilesAsync().ConfigureAwait(false); await ExtractSystemProfilesAsync();
LoadProfiles(); LoadProfiles();
} }
catch (Exception ex) catch (Exception ex)
@@ -83,22 +82,22 @@ namespace Emby.Dlna
{ {
lock (_profiles) lock (_profiles)
{ {
return _profiles.Values var list = _profiles.Values.ToList();
return list
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1) .OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
.ThenBy(i => i.Item1.Info.Name) .ThenBy(i => i.Item1.Info.Name)
.Select(i => i.Item2) .Select(i => i.Item2)
.ToList(); .ToList();
} }
} }
/// <inheritdoc />
public DeviceProfile GetDefaultProfile() public DeviceProfile GetDefaultProfile()
{ {
return new DefaultProfile(); return new DefaultProfile();
} }
/// <inheritdoc /> public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
{ {
if (deviceInfo == null) if (deviceInfo == null)
{ {
@@ -108,57 +107,100 @@ namespace Emby.Dlna
var profile = GetProfiles() var profile = GetProfiles()
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification)); .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 else
{ {
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name); LogUnmatchedProfile(deviceInfo);
} }
return profile; return profile;
} }
/// <summary> private void LogUnmatchedProfile(DeviceIdentification profile)
/// 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)
{ {
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName) var builder = new StringBuilder();
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl) builder.AppendLine("No matching device profile found. The default will need to be used.");
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription) builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName) builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber) builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl) builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber); 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. if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
return true; return false;
} }
if (string.IsNullOrEmpty(input)) if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
{ {
// The profile contains a value, and the device doesn't. if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
return false; 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 try
{ {
return input.Equals(pattern, StringComparison.OrdinalIgnoreCase) return Regex.IsMatch(input, pattern);
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
@@ -167,8 +209,7 @@ namespace Emby.Dlna
} }
} }
/// <inheritdoc /> public DeviceProfile GetProfile(IHeaderDictionary headers)
public DeviceProfile? GetProfile(IHeaderDictionary headers)
{ {
if (headers == null) if (headers == null)
{ {
@@ -176,13 +217,15 @@ namespace Emby.Dlna
} }
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification)); 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 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; return profile;
@@ -209,7 +252,7 @@ namespace Emby.Dlna
return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase); return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
case HeaderMatchType.Substring: case HeaderMatchType.Substring:
var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1; var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
// _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch); //_logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
return isMatch; return isMatch;
case HeaderMatchType.Regex: case HeaderMatchType.Regex:
return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase); return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
@@ -221,35 +264,45 @@ namespace Emby.Dlna
return false; return false;
} }
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type) private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
{ {
try try
{ {
return _fileSystem.GetFilePaths(path) var xmlFies = _fileSystem.GetFilePaths(path)
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase)) .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
.ToList();
return xmlFies
.Select(i => ParseProfileFile(i, type)) .Select(i => ParseProfileFile(i, type))
.Where(i => i != null) .Where(i => i != null)
.ToList()!; // We just filtered out all the nulls .ToList();
} }
catch (IOException) 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) lock (_profiles)
{ {
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple)) if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple))
{ {
return profileTuple.Item2; return profileTuple.Item2;
} }
try try
{ {
DeviceProfile profile;
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path); var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
var profile = ReserializeProfile(tempProfile);
profile = ReserializeProfile(tempProfile);
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); 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)) if (string.IsNullOrEmpty(id))
{ {
throw new ArgumentNullException(nameof(id)); throw new ArgumentNullException(nameof(id));
} }
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
if (info == null)
{
return null;
}
return ParseProfileFile(info.Path, info.Info.Type); return ParseProfileFile(info.Path, info.Info.Type);
} }
@@ -288,14 +335,14 @@ namespace Emby.Dlna
{ {
lock (_profiles) lock (_profiles)
{ {
return _profiles.Values var list = _profiles.Values.ToList();
return list
.Select(i => i.Item1) .Select(i => i.Item1)
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1) .OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
.ThenBy(i => i.Info.Name); .ThenBy(i => i.Info.Name);
} }
} }
/// <inheritdoc />
public IEnumerable<DeviceProfileInfo> GetProfileInfos() public IEnumerable<DeviceProfileInfo> GetProfileInfos()
{ {
return GetProfileInfosInternal().Select(i => i.Info); return GetProfileInfosInternal().Select(i => i.Info);
@@ -303,14 +350,17 @@ namespace Emby.Dlna
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type) private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
{ {
return new InternalProfileInfo( return new InternalProfileInfo
new DeviceProfileInfo {
Path = file.FullName,
Info = new DeviceProfileInfo
{ {
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture), Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
Name = _fileSystem.GetFileNameWithoutExtension(file), Name = _fileSystem.GetFileNameWithoutExtension(file),
Type = type Type = type
}, }
file.FullName); };
} }
private async Task ExtractSystemProfilesAsync() private async Task ExtractSystemProfilesAsync()
@@ -321,32 +371,26 @@ namespace Emby.Dlna
foreach (var name in _assembly.GetManifestResourceNames()) foreach (var name in _assembly.GetManifestResourceNames())
{ {
if (!name.StartsWith(namespaceName, StringComparison.Ordinal)) if (!name.StartsWith(namespaceName))
{ {
continue; continue;
} }
var path = Path.Join( var filename = Path.GetFileName(name).Substring(namespaceName.Length);
systemProfilesPath,
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
// The stream should exist as we just got its name from GetManifestResourceNames var path = Path.Combine(systemProfilesPath, filename);
using (var stream = _assembly.GetManifestResourceStream(name)!)
using (var stream = _assembly.GetManifestResourceStream(name))
{ {
var length = stream.Length;
var fileInfo = _fileSystem.GetFileInfo(path); var fileInfo = _fileSystem.GetFileInfo(path);
if (!fileInfo.Exists || fileInfo.Length != length) if (!fileInfo.Exists || fileInfo.Length != stream.Length)
{ {
Directory.CreateDirectory(systemProfilesPath); Directory.CreateDirectory(systemProfilesPath);
var fileOptions = AsyncFile.WriteOptions; using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
fileOptions.Mode = FileMode.Create;
fileOptions.PreallocationSize = length;
var fileStream = new FileStream(path, fileOptions);
await using (fileStream.ConfigureAwait(false))
{ {
await stream.CopyToAsync(fileStream).ConfigureAwait(false); await stream.CopyToAsync(fileStream);
} }
} }
} }
@@ -356,7 +400,6 @@ namespace Emby.Dlna
Directory.CreateDirectory(UserProfilesPath); Directory.CreateDirectory(UserProfilesPath);
} }
/// <inheritdoc />
public void DeleteProfile(string id) public void DeleteProfile(string id)
{ {
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase)); 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) public void CreateProfile(DeviceProfile profile)
{ {
profile = ReserializeProfile(profile); profile = ReserializeProfile(profile);
@@ -390,8 +432,7 @@ namespace Emby.Dlna
SaveProfile(profile, path, DeviceProfileType.User); SaveProfile(profile, path, DeviceProfileType.User);
} }
/// <inheritdoc /> public void UpdateProfile(DeviceProfile profile)
public void UpdateProfile(string profileId, DeviceProfile profile)
{ {
profile = ReserializeProfile(profile); profile = ReserializeProfile(profile);
@@ -399,13 +440,12 @@ namespace Emby.Dlna
{ {
throw new ArgumentException("Profile is missing Id"); throw new ArgumentException("Profile is missing Id");
} }
if (string.IsNullOrEmpty(profile.Name)) if (string.IsNullOrEmpty(profile.Name))
{ {
throw new ArgumentException("Profile is missing Name"); throw new ArgumentException("Profile is missing Name");
} }
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 newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
var path = Path.Combine(UserProfilesPath, newFilename); 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); _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
} }
SerializeToXml(profile, path); SerializeToXml(profile, path);
} }
@@ -436,10 +475,10 @@ namespace Emby.Dlna
/// <summary> /// <summary>
/// Recreates the object using serialization, to ensure it's not a subclass. /// Recreates the object using serialization, to ensure it's not a subclass.
/// If it's a subclass it may not serialize properly to xml (different root element tag name). /// If it's a subclass it may not serlialize properly to xml (different root element tag name)
/// </summary> /// </summary>
/// <param name="profile">The device profile.</param> /// <param name="profile"></param>
/// <returns>The re-serialized device profile.</returns> /// <returns></returns>
private DeviceProfile ReserializeProfile(DeviceProfile profile) private DeviceProfile ReserializeProfile(DeviceProfile profile)
{ {
if (profile.GetType() == typeof(DeviceProfile)) if (profile.GetType() == typeof(DeviceProfile))
@@ -447,56 +486,42 @@ namespace Emby.Dlna
return profile; 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.DeserializeFromString<DeviceProfile>(json);
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!; }
class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
internal string Path { get; set; }
} }
/// <inheritdoc />
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{ {
var profile = GetProfile(headers) ?? GetDefaultProfile(); var profile = GetProfile(headers) ??
GetDefaultProfile();
var serverId = _appHost.SystemId; var serverId = _appHost.SystemId;
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml(); 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) var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? ImageFormat.Png ? ImageFormat.Png
: ImageFormat.Jpg; : ImageFormat.Jpg;
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant(); 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 class DlnaProfileEntryPoint : IServerEntryPoint
{ {
@@ -518,7 +543,7 @@ namespace Emby.Dlna
private void DumpProfiles() private void DumpProfiles()
{ {
DeviceProfile[] list = new[] DeviceProfile[] list = new []
{ {
new SamsungSmartTvProfile(), new SamsungSmartTvProfile(),
new XboxOneProfile(), new XboxOneProfile(),
@@ -542,9 +567,9 @@ namespace Emby.Dlna
new Foobar2000Profile(), new Foobar2000Profile(),
new SharpSmartTvProfile(), new SharpSmartTvProfile(),
new MediaMonkeyProfile(), new MediaMonkeyProfile(),
// new Windows81Profile(), //new Windows81Profile(),
// new WindowsMediaCenterProfile(), //new WindowsMediaCenterProfile(),
// new WindowsPhoneProfile(), //new WindowsPhoneProfile(),
new DirectTvProfile(), new DirectTvProfile(),
new DishHopperJoeyProfile(), new DishHopperJoeyProfile(),
new DefaultProfile(), new DefaultProfile(),
+10 -16
View File
@@ -1,10 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<ProjectGuid>{805844AB-E92F-45E6-9D99-4F6D48D129A5}</ProjectGuid>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\SharedVersion.cs" /> <Compile Include="..\SharedVersion.cs" />
</ItemGroup> </ItemGroup>
@@ -17,26 +12,24 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" 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" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Images\logo120.jpg" /> <EmbeddedResource Include="Images\logo120.jpg" />
<EmbeddedResource Include="Images\logo120.png" /> <EmbeddedResource Include="Images\logo120.png" />
@@ -80,7 +73,8 @@
</ItemGroup> </ItemGroup>
<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> </ItemGroup>
</Project> </Project>
+3 -4
View File
@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic; using System.Collections.Generic;
@@ -6,10 +7,8 @@ namespace Emby.Dlna
{ {
public class EventSubscriptionResponse public class EventSubscriptionResponse
{ {
public EventSubscriptionResponse(string content, string contentType) public EventSubscriptionResponse()
{ {
Content = content;
ContentType = contentType;
Headers = new Dictionary<string, string>(); Headers = new Dictionary<string, string>();
} }
@@ -17,6 +16,6 @@ namespace Emby.Dlna
public string ContentType { get; set; } public string ContentType { get; set; }
public Dictionary<string, string> Headers { get; } public Dictionary<string, string> Headers { get; set; }
} }
} }
@@ -1,6 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@@ -8,49 +7,43 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Eventing namespace Emby.Dlna.Eventing
{ {
public class DlnaEventManager : IDlnaEventManager public class EventManager : IEventManager
{ {
private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions = private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase); new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClient _httpClient;
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory) public EventManager(ILogger logger, IHttpClient httpClient)
{ {
_httpClientFactory = httpClientFactory; _httpClient = httpClient;
_logger = logger; _logger = logger;
} }
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl) public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
{ {
var subscription = GetSubscription(subscriptionId, false); var subscription = GetSubscription(subscriptionId, false);
if (subscription != null)
{
subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
int timeoutSeconds = subscription.TimeoutSeconds;
subscription.SubscriptionTime = DateTime.UtcNow;
_logger.LogDebug( subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
"Renewing event subscription for {0} with timeout of {1} to {2}", int timeoutSeconds = subscription.TimeoutSeconds;
subscription.NotificationType, subscription.SubscriptionTime = DateTime.UtcNow;
timeoutSeconds,
subscription.CallbackUrl);
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) public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
@@ -58,8 +51,7 @@ namespace Emby.Dlna.Eventing
var timeout = ParseTimeout(requestedTimeoutString) ?? 300; var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
_logger.LogDebug( _logger.LogDebug("Creating event subscription for {0} with timeout of {1} to {2}",
"Creating event subscription for {0} with timeout of {1} to {2}",
notificationType, notificationType,
timeout, timeout,
callbackUrl); callbackUrl);
@@ -69,8 +61,7 @@ namespace Emby.Dlna.Eventing
Id = id, Id = id,
CallbackUrl = callbackUrl, CallbackUrl = callbackUrl,
SubscriptionTime = DateTime.UtcNow, SubscriptionTime = DateTime.UtcNow,
TimeoutSeconds = timeout, TimeoutSeconds = timeout
NotificationType = notificationType
}); });
return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout); return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout);
@@ -81,7 +72,9 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header)) if (!string.IsNullOrEmpty(header))
{ {
// Starts with SECOND- // 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; return val;
} }
@@ -94,17 +87,26 @@ namespace Emby.Dlna.Eventing
{ {
_logger.LogDebug("Cancelling event subscription {0}", subscriptionId); _logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
_subscriptions.TryRemove(subscriptionId, out _); _subscriptions.TryRemove(subscriptionId, out EventSubscription sub);
return new EventSubscriptionResponse(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) 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["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; return response;
} }
@@ -143,30 +145,33 @@ namespace Emby.Dlna.Eventing
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">"); builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
foreach (var key in stateVariables.Keys) foreach (var key in stateVariables.Keys)
{ {
builder.Append("<e:property>") builder.Append("<e:property>");
.Append('<') builder.Append("<" + key + ">");
.Append(key) builder.Append(stateVariables[key]);
.Append('>') builder.Append("</" + key + ">");
.Append(stateVariables[key]) builder.Append("</e:property>");
.Append("</")
.Append(key)
.Append('>')
.Append("</e:property>");
} }
builder.Append("</e:propertyset>"); builder.Append("</e:propertyset>");
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl); var options = new HttpRequestOptions
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml); {
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType); RequestContent = builder.ToString(),
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange"); RequestContentType = "text/xml",
options.Headers.TryAddWithoutValidation("SID", subscription.Id); Url = subscription.CallbackUrl,
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture)); BufferContent = false
};
options.RequestHeaders.Add("NT", subscription.NotificationType);
options.RequestHeaders.Add("NTS", "upnp:propchange");
options.RequestHeaders.Add("SID", subscription.Id);
options.RequestHeaders.Add("SEQ", subscription.TriggerCount.ToString(_usCulture));
try try
{ {
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); {
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
+1 -5
View File
@@ -1,6 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
@@ -9,13 +8,10 @@ namespace Emby.Dlna.Eventing
public class EventSubscription public class EventSubscription
{ {
public string Id { get; set; } public string Id { get; set; }
public string CallbackUrl { get; set; } public string CallbackUrl { get; set; }
public string NotificationType { get; set; } public string NotificationType { get; set; }
public DateTime SubscriptionTime { get; set; } public DateTime SubscriptionTime { get; set; }
public int TimeoutSeconds { get; set; } public int TimeoutSeconds { get; set; }
public long TriggerCount { get; set; } public long TriggerCount { get; set; }
+2 -1
View File
@@ -1,8 +1,9 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
namespace Emby.Dlna namespace Emby.Dlna
{ {
public interface IConnectionManager : IDlnaEventManager, IUpnpService public interface IConnectionManager : IEventManager, IUpnpService
{ {
} }
} }
+2 -1
View File
@@ -1,8 +1,9 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
namespace Emby.Dlna namespace Emby.Dlna
{ {
public interface IContentDirectory : IDlnaEventManager, IUpnpService public interface IContentDirectory : IEventManager, IUpnpService
{ {
} }
} }
@@ -1,33 +1,24 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
namespace Emby.Dlna namespace Emby.Dlna
{ {
public interface IDlnaEventManager public interface IEventManager
{ {
/// <summary> /// <summary>
/// Cancels the event subscription. /// Cancels the event subscription.
/// </summary> /// </summary>
/// <param name="subscriptionId">The subscription identifier.</param> /// <param name="subscriptionId">The subscription identifier.</param>
/// <returns>The response.</returns>
EventSubscriptionResponse CancelEventSubscription(string subscriptionId); EventSubscriptionResponse CancelEventSubscription(string subscriptionId);
/// <summary> /// <summary>
/// Renews the event subscription. /// Renews the event subscription.
/// </summary> /// </summary>
/// <param name="subscriptionId">The subscription identifier.</param>
/// <param name="notificationType">The notification type.</param>
/// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
/// <param name="callbackUrl">The callback url.</param>
/// <returns>The response.</returns>
EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl); EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
/// <summary> /// <summary>
/// Creates the event subscription. /// Creates the event subscription.
/// </summary> /// </summary>
/// <param name="notificationType">The notification type.</param>
/// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
/// <param name="callbackUrl">The callback url.</param>
/// <returns>The response.</returns>
EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl); EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
} }
} }
+2 -1
View File
@@ -1,8 +1,9 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
namespace Emby.Dlna namespace Emby.Dlna
{ {
public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService public interface IMediaReceiverRegistrar : IEventManager, IUpnpService
{ {
} }
} }
+1
View File
@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System.Threading.Tasks; using System.Threading.Tasks;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 286 B

+78 -137
View File
@@ -1,17 +1,13 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.PlayTo; using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
@@ -27,19 +23,23 @@ using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Rssdp; using Rssdp;
using Rssdp.Infrastructure; using Rssdp.Infrastructure;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Dlna.Main namespace Emby.Dlna.Main
{ {
public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
{ {
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly ILogger<DlnaEntryPoint> _logger; private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private PlayToManager _manager;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClient _httpClient;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager; private readonly IDlnaManager _dlnaManager;
@@ -48,24 +48,27 @@ namespace Emby.Dlna.Main
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery; private readonly IDeviceDiscovery _deviceDiscovery;
private SsdpDevicePublisher _Publisher;
private readonly ISocketFactory _socketFactory; private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
private readonly bool _disabled;
private PlayToManager _manager;
private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer; private ISsdpCommunicationsServer _communicationsServer;
private bool _disposed; internal IContentDirectory ContentDirectory { get; private set; }
internal IConnectionManager ConnectionManager { get; private set; }
internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public DlnaEntryPoint( public static DlnaEntryPoint Current;
IServerConfigurationManager config,
public DlnaEntryPoint(IServerConfigurationManager config,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IServerApplicationHost appHost, IServerApplicationHost appHost,
ISessionManager sessionManager, ISessionManager sessionManager,
IHttpClientFactory httpClientFactory, IHttpClient httpClient,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IUserManager userManager, IUserManager userManager,
IDlnaManager dlnaManager, IDlnaManager dlnaManager,
@@ -83,7 +86,7 @@ namespace Emby.Dlna.Main
_config = config; _config = config;
_appHost = appHost; _appHost = appHost;
_sessionManager = sessionManager; _sessionManager = sessionManager;
_httpClientFactory = httpClientFactory; _httpClient = httpClient;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userManager = userManager; _userManager = userManager;
_dlnaManager = dlnaManager; _dlnaManager = dlnaManager;
@@ -95,73 +98,39 @@ namespace Emby.Dlna.Main
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_socketFactory = socketFactory; _socketFactory = socketFactory;
_networkManager = networkManager; _networkManager = networkManager;
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); _logger = loggerFactory.CreateLogger("Dlna");
ContentDirectory = new ContentDirectory.ContentDirectoryService( ContentDirectory = new ContentDirectory.ContentDirectory(
dlnaManager, dlnaManager,
userDataManager, userDataManager,
imageProcessor, imageProcessor,
libraryManager, libraryManager,
config, config,
userManager, userManager,
loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(), _logger,
httpClientFactory, httpClient,
localizationManager, localizationManager,
mediaSourceManager, mediaSourceManager,
userViewManager, userViewManager,
mediaEncoder, mediaEncoder,
tvSeriesManager); tvSeriesManager);
ConnectionManager = new ConnectionManager.ConnectionManagerService( ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient);
dlnaManager,
config,
loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
httpClientFactory);
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService( MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config);
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
httpClientFactory,
config);
Current = this; Current = this;
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() public async Task RunAsync()
{ {
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
if (_disabled)
{
// No use starting as dlna won't work, as we're running purely on HTTPS.
return;
}
ReloadComponents(); ReloadComponents();
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
} }
private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{ {
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
{ {
@@ -169,16 +138,15 @@ namespace Emby.Dlna.Main
} }
} }
private void ReloadComponents() private async void ReloadComponents()
{ {
var options = _config.GetDlnaConfiguration(); var options = _config.GetDlnaConfiguration();
Enabled = options.EnableServer;
StartSsdpHandler(); StartSsdpHandler();
if (options.EnableServer) if (options.EnableServer)
{ {
StartDevicePublisher(options); await StartDevicePublisher(options).ConfigureAwait(false);
} }
else else
{ {
@@ -201,10 +169,10 @@ namespace Emby.Dlna.Main
{ {
if (_communicationsServer == null) if (_communicationsServer == null)
{ {
var enableMultiSocketBinding = OperatingSystem.IsWindows() || var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
OperatingSystem.IsLinux(); OperatingSystem.Id == OperatingSystemId.Linux;
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{ {
IsShared = true IsShared = true
}; };
@@ -218,14 +186,16 @@ namespace Emby.Dlna.Main
} }
} }
private void LogMessage(string msg)
{
_logger.LogDebug(msg);
}
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer) private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
{ {
try try
{ {
if (communicationsServer != null) ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
{
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
}
} }
catch (Exception ex) 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) if (!options.BlastAliveMessages)
{ {
return; return;
} }
if (_publisher != null) if (_Publisher != null)
{ {
return; return;
} }
try try
{ {
_publisher = new SsdpDevicePublisher( _Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost);
_communicationsServer, _Publisher.LogFunction = LogMessage;
MediaBrowser.Common.System.OperatingSystem.Name, _Publisher.SupportPnpRootDevice = false;
Environment.OSVersion.VersionString,
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
SupportPnpRootDevice = false
};
RegisterServerEndpoints(); await RegisterServerEndpoints().ConfigureAwait(false);
_publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); _Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -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 udn = CreateUuid(_appHost.SystemId);
var descriptorUri = "/dlna/" + udn + "/description.xml";
var bindAddresses = NetworkManager.CreateCollection( foreach (var address in addresses)
_networkManager.GetInternalBindAddresses()
.Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)));
if (bindAddresses.Count == 0)
{
// No interfaces returned, so use loopback.
bindAddresses = _networkManager.GetLoopbacks();
}
foreach (IPNetAddress address in bindAddresses)
{ {
if (address.AddressFamily == AddressFamily.InterNetworkV6) if (address.AddressFamily == AddressFamily.InterNetworkV6)
{ {
// Not supporting IPv6 right now // Not support IPv6 right now
continue; continue;
}
// Limit to LAN addresses only
if (!_networkManager.IsInLocalNetwork(address))
{
continue;
} }
var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; 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 var device = new SsdpRootDevice
{ {
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. Location = uri, // Must point to the URL that serves your devices UPnP description document.
Address = address.Address, Address = address,
PrefixLength = address.PrefixLength, SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
FriendlyName = "Jellyfin", FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin", Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server", ModelName = "Jellyfin Server",
@@ -329,13 +279,13 @@ namespace Emby.Dlna.Main
}; };
SetProperies(device, fullService); SetProperies(device, fullService);
_publisher.AddDevice(device); _Publisher.AddDevice(device);
var embeddedDevices = new[] var embeddedDevices = new[]
{ {
"urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ContentDirectory:1",
"urn:schemas-upnp-org:service:ConnectionManager:1", "urn:schemas-upnp-org:service:ConnectionManager:1",
// "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" //"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
}; };
foreach (var subDevice in embeddedDevices) foreach (var subDevice in embeddedDevices)
@@ -361,13 +311,12 @@ namespace Emby.Dlna.Main
{ {
guid = text.GetMD5(); guid = text.GetMD5();
} }
return guid.ToString("N", CultureInfo.InvariantCulture);
return guid.ToString("D", CultureInfo.InvariantCulture);
} }
private void SetProperies(SsdpDevice device, string fullDeviceType) private void SetProperies(SsdpDevice device, string fullDeviceType)
{ {
var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase); var service = fullDeviceType.Replace("urn:", string.Empty).Replace(":1", string.Empty);
var serviceParts = service.Split(':'); var serviceParts = service.Split(':');
@@ -378,6 +327,7 @@ namespace Emby.Dlna.Main
device.DeviceType = serviceParts[2]; device.DeviceType = serviceParts[2];
} }
private readonly object _syncLock = new object();
private void StartPlayToManager() private void StartPlayToManager()
{ {
lock (_syncLock) lock (_syncLock)
@@ -389,8 +339,7 @@ namespace Emby.Dlna.Main
try try
{ {
_manager = new PlayToManager( _manager = new PlayToManager(_logger,
_logger,
_sessionManager, _sessionManager,
_libraryManager, _libraryManager,
_userManager, _userManager,
@@ -398,7 +347,8 @@ namespace Emby.Dlna.Main
_appHost, _appHost,
_imageProcessor, _imageProcessor,
_deviceDiscovery, _deviceDiscovery,
_httpClientFactory, _httpClient,
_config,
_userDataManager, _userDataManager,
_localization, _localization,
_mediaSourceManager, _mediaSourceManager,
@@ -428,30 +378,13 @@ namespace Emby.Dlna.Main
{ {
_logger.LogError(ex, "Error disposing PlayTo manager"); _logger.LogError(ex, "Error disposing PlayTo manager");
} }
_manager = null; _manager = null;
} }
} }
} }
public void DisposeDevicePublisher()
{
if (_publisher != null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_publisher.Dispose();
_publisher = null;
}
}
/// <inheritdoc />
public void Dispose() public void Dispose()
{ {
if (_disposed)
{
return;
}
DisposeDevicePublisher(); DisposeDevicePublisher();
DisposePlayToManager(); DisposePlayToManager();
DisposeDeviceDiscovery(); DisposeDeviceDiscovery();
@@ -467,8 +400,16 @@ namespace Emby.Dlna.Main
ConnectionManager = null; ConnectionManager = null;
MediaReceiverRegistrar = null; MediaReceiverRegistrar = null;
Current = null; Current = null;
}
_disposed = true; public void DisposeDevicePublisher()
{
if (_Publisher != null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_Publisher.Dispose();
_Publisher = null;
}
} }
} }
} }
@@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Xml; using System.Xml;
@@ -8,23 +11,15 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
{ {
/// <summary>
/// Defines the <see cref="ControlHandler" />.
/// </summary>
public class ControlHandler : BaseControlHandler public class ControlHandler : BaseControlHandler
{ {
/// <summary>
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
/// </summary>
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
public ControlHandler(IServerConfigurationManager config, ILogger logger) public ControlHandler(IServerConfigurationManager config, ILogger logger)
: base(config, logger) : base(config, logger)
{ {
} }
/// <inheritdoc /> /// <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)) if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
{ {
@@ -41,17 +36,9 @@ namespace Emby.Dlna.MediaReceiverRegistrar
throw new ResourceNotFoundException("Unexpected control request name: " + methodName); throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
} }
/// <summary>
/// Records that the handle is authorized in the xml stream.
/// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private static void HandleIsAuthorized(XmlWriter xmlWriter) private static void HandleIsAuthorized(XmlWriter xmlWriter)
=> xmlWriter.WriteElementString("Result", "1"); => xmlWriter.WriteElementString("Result", "1");
/// <summary>
/// Records that the handle is validated in the xml stream.
/// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private static void HandleIsValidated(XmlWriter xmlWriter) private static void HandleIsValidated(XmlWriter xmlWriter)
=> xmlWriter.WriteElementString("Result", "1"); => xmlWriter.WriteElementString("Result", "1");
} }
@@ -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 System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Service; using Emby.Dlna.Service;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
{ {
/// <summary> public class MediaReceiverRegistrarXmlBuilder
/// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />.
/// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
/// </summary>
public static class MediaReceiverRegistrarXmlBuilder
{ {
/// <summary> public string GetXml()
/// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
/// </summary>
/// <returns>An XML representation of this service.</returns>
public static string GetXml()
{ {
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables()); return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
GetStateVariables());
} }
/// <summary>
/// The a list of all the state variables for this invocation.
/// </summary>
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables() private static IEnumerable<StateVariable> GetStateVariables()
{ {
var list = new List<StateVariable> var list = new List<StateVariable>();
list.Add(new StateVariable
{ {
new StateVariable Name = "AuthorizationGrantedUpdateID",
{ DataType = "ui4",
Name = "AuthorizationGrantedUpdateID", SendsEvents = true
DataType = "ui4", });
SendsEvents = true
},
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_DeviceID", Name = "A_ARG_TYPE_DeviceID",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "AuthorizationDeniedUpdateID", Name = "AuthorizationDeniedUpdateID",
DataType = "ui4", DataType = "ui4",
SendsEvents = true SendsEvents = true
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "ValidationSucceededUpdateID", Name = "ValidationSucceededUpdateID",
DataType = "ui4", DataType = "ui4",
SendsEvents = true SendsEvents = true
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_RegistrationRespMsg", Name = "A_ARG_TYPE_RegistrationRespMsg",
DataType = "bin.base64", DataType = "bin.base64",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_RegistrationReqMsg", Name = "A_ARG_TYPE_RegistrationReqMsg",
DataType = "bin.base64", DataType = "bin.base64",
SendsEvents = false SendsEvents = false
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "ValidationRevokedUpdateID", Name = "ValidationRevokedUpdateID",
DataType = "ui4", DataType = "ui4",
SendsEvents = true SendsEvents = true
}, });
new StateVariable list.Add(new StateVariable
{ {
Name = "A_ARG_TYPE_Result", Name = "A_ARG_TYPE_Result",
DataType = "int", DataType = "int",
SendsEvents = false SendsEvents = false
} });
};
return list; return list;
} }
@@ -1,18 +1,14 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
{ {
/// <summary> public class ServiceActionListBuilder
/// Defines the <see cref="ServiceActionListBuilder" />.
/// </summary>
public static class ServiceActionListBuilder
{ {
/// <summary> public IEnumerable<ServiceAction> GetActions()
/// Returns a list of services that this instance provides.
/// </summary>
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
public static IEnumerable<ServiceAction> GetActions()
{ {
return new[] return new[]
{ {
@@ -26,10 +22,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
}; };
} }
/// <summary>
/// Returns the action details for "IsValidated".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetIsValidated() private static ServiceAction GetIsValidated()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -52,10 +44,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
/// <summary>
/// Returns the action details for "IsAuthorized".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetIsAuthorized() private static ServiceAction GetIsAuthorized()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -78,10 +66,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
/// <summary>
/// Returns the action details for "RegisterDevice".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetRegisterDevice() private static ServiceAction GetRegisterDevice()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -104,10 +88,6 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
/// <summary>
/// Returns the action details for "GetValidationSucceededUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetValidationSucceededUpdateID() private static ServiceAction GetGetValidationSucceededUpdateID()
{ {
var action = new ServiceAction var action = new ServiceAction
@@ -124,11 +104,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
/// <summary> private ServiceAction GetGetAuthorizationDeniedUpdateID()
/// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetAuthorizationDeniedUpdateID()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@@ -144,11 +120,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
/// <summary> private ServiceAction GetGetValidationRevokedUpdateID()
/// Returns the action details for "GetValidationRevokedUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetValidationRevokedUpdateID()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@@ -164,11 +136,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
/// <summary> private ServiceAction GetGetAuthorizationGrantedUpdateID()
/// Returns the action details for "GetAuthorizationGrantedUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetAuthorizationGrantedUpdateID()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
+234 -338
View File
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -1,6 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
@@ -10,9 +9,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class DeviceInfo public class DeviceInfo
{ {
private readonly List<DeviceService> _services = new List<DeviceService>();
private string _baseUrl = string.Empty;
public DeviceInfo() public DeviceInfo()
{ {
Name = "Generic Device"; Name = "Generic Device";
@@ -38,6 +34,7 @@ namespace Emby.Dlna.PlayTo
public string PresentationUrl { get; set; } public string PresentationUrl { get; set; }
private string _baseUrl = string.Empty;
public string BaseUrl public string BaseUrl
{ {
get => _baseUrl; get => _baseUrl;
@@ -46,6 +43,7 @@ namespace Emby.Dlna.PlayTo
public DeviceIcon Icon { get; set; } public DeviceIcon Icon { get; set; }
private readonly List<DeviceService> _services = new List<DeviceService>();
public List<DeviceService> Services => _services; public List<DeviceService> Services => _services;
public DeviceIdentification ToDeviceIdentification() public DeviceIdentification ToDeviceIdentification()
-19
View File
@@ -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
+75 -85
View File
@@ -1,29 +1,30 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
{ {
public sealed class PlayToManager : IDisposable public class PlayToManager : IDisposable
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
@@ -33,7 +34,8 @@ namespace Emby.Dlna.PlayTo
private readonly IDlnaManager _dlnaManager; private readonly IDlnaManager _dlnaManager;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClient _httpClient;
private readonly IServerConfigurationManager _config;
private readonly IUserDataManager _userDataManager; private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
@@ -45,7 +47,7 @@ namespace Emby.Dlna.PlayTo
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{ {
_logger = logger; _logger = logger;
_sessionManager = sessionManager; _sessionManager = sessionManager;
@@ -55,7 +57,8 @@ namespace Emby.Dlna.PlayTo
_appHost = appHost; _appHost = appHost;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_deviceDiscovery = deviceDiscovery; _deviceDiscovery = deviceDiscovery;
_httpClientFactory = httpClientFactory; _httpClient = httpClient;
_config = config;
_userDataManager = userDataManager; _userDataManager = userDataManager;
_localization = localization; _localization = localization;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
@@ -76,20 +79,17 @@ namespace Emby.Dlna.PlayTo
var info = e.Argument; var info = e.Argument;
if (!info.Headers.TryGetValue("USN", out string usn)) if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty;
{
usn = string.Empty;
}
if (!info.Headers.TryGetValue("NT", out string nt)) if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty;
{
nt = string.Empty; string location = info.Location.ToString();
}
// It has to report that it's a media renderer // It has to report that it's a media renderer
if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase) if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
&& !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)) nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
{ {
//_logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
return; return;
} }
@@ -109,10 +109,11 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
await AddDevice(info, cancellationToken).ConfigureAwait(false); await AddDevice(info, location, cancellationToken).ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -124,88 +125,81 @@ namespace Emby.Dlna.PlayTo
} }
} }
internal static string GetUuid(string usn) private string GetUuid(string usn)
{ {
const string UuidStr = "uuid:"; var found = false;
const string UuidColonStr = "::"; var index = usn.IndexOf("uuid:", StringComparison.OrdinalIgnoreCase);
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);
if (index != -1) if (index != -1)
{ {
tmp = tmp[..index]; usn = usn.Substring(index);
found = true;
} }
index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
index = tmp.IndexOf('{');
if (index != -1) if (index != -1)
{ {
int endIndex = tmp.IndexOf('}'); usn = usn.Substring(0, index);
if (endIndex != -1)
{
tmp = tmp[(index + 1)..endIndex];
}
} }
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; var uri = info.Location;
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri); _logger.LogDebug("Attempting to create PlayToController from location {0}", location);
_logger.LogDebug("Logging session activity from location {0}", location);
if (info.Headers.TryGetValue("USN", out string uuid)) if (info.Headers.TryGetValue("USN", out string uuid))
{ {
uuid = GetUuid(uuid); uuid = GetUuid(uuid);
} }
else else
{ {
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture); uuid = location.GetMD5().ToString("N", CultureInfo.InvariantCulture);
} }
var sessionInfo = await _sessionManager var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
.ConfigureAwait(false);
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault(); var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
if (controller == null) if (controller == null)
{ {
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false); var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false);
if (device == null)
{
_logger.LogError("Ignoring device as xml response is invalid.");
return;
}
string deviceName = device.Properties.Name; string deviceName = device.Properties.Name;
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
string serverAddress = _appHost.GetSmartApiUrl(info.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( controller = new PlayToController(sessionInfo,
sessionInfo, _sessionManager,
_sessionManager, _libraryManager,
_libraryManager, _logger,
_logger, _dlnaManager,
_dlnaManager, _userManager,
_userManager, _imageProcessor,
_imageProcessor, serverAddress,
serverAddress, null,
null, _deviceDiscovery,
_deviceDiscovery, _userDataManager,
_userDataManager, _localization,
_localization, _mediaSourceManager,
_mediaSourceManager, _config,
_mediaEncoder); _mediaEncoder);
sessionInfo.AddController(controller); sessionInfo.AddController(controller);
@@ -218,17 +212,17 @@ namespace Emby.Dlna.PlayTo
{ {
PlayableMediaTypes = profile.GetSupportedMediaTypes(), PlayableMediaTypes = profile.GetSupportedMediaTypes(),
SupportedCommands = new[] SupportedCommands = new string[]
{ {
GeneralCommandType.VolumeDown, GeneralCommandType.VolumeDown.ToString(),
GeneralCommandType.VolumeUp, GeneralCommandType.VolumeUp.ToString(),
GeneralCommandType.Mute, GeneralCommandType.Mute.ToString(),
GeneralCommandType.Unmute, GeneralCommandType.Unmute.ToString(),
GeneralCommandType.ToggleMute, GeneralCommandType.ToggleMute.ToString(),
GeneralCommandType.SetVolume, GeneralCommandType.SetVolume.ToString(),
GeneralCommandType.SetAudioStreamIndex, GeneralCommandType.SetAudioStreamIndex.ToString(),
GeneralCommandType.SetSubtitleStreamIndex, GeneralCommandType.SetSubtitleStreamIndex.ToString(),
GeneralCommandType.PlayMediaSource GeneralCommandType.PlayMediaSource.ToString()
}, },
SupportsMediaControl = true SupportsMediaControl = true
@@ -238,7 +232,6 @@ namespace Emby.Dlna.PlayTo
} }
} }
/// <inheritdoc />
public void Dispose() public void Dispose()
{ {
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
@@ -247,13 +240,10 @@ namespace Emby.Dlna.PlayTo
{ {
_disposeCancellationTokenSource.Cancel(); _disposeCancellationTokenSource.Cancel();
} }
catch (Exception ex) catch
{ {
_logger.LogDebug(ex, "Error while disposing PlayToManager");
}
_sessionLock.Dispose(); }
_disposeCancellationTokenSource.Dispose();
_disposed = true; _disposed = true;
} }
@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
@@ -6,11 +7,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class PlaybackProgressEventArgs : EventArgs public class PlaybackProgressEventArgs : EventArgs
{ {
public PlaybackProgressEventArgs(UBaseObject mediaInfo) public uBaseObject MediaInfo { get; set; }
{
MediaInfo = mediaInfo;
}
public UBaseObject MediaInfo { get; set; }
} }
} }
+2 -6
View File
@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
@@ -6,11 +7,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class PlaybackStartEventArgs : EventArgs public class PlaybackStartEventArgs : EventArgs
{ {
public PlaybackStartEventArgs(UBaseObject mediaInfo) public uBaseObject MediaInfo { get; set; }
{
MediaInfo = mediaInfo;
}
public UBaseObject MediaInfo { get; set; }
} }
} }
+7 -5
View File
@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
@@ -6,11 +7,12 @@ namespace Emby.Dlna.PlayTo
{ {
public class PlaybackStoppedEventArgs : EventArgs public class PlaybackStoppedEventArgs : EventArgs
{ {
public PlaybackStoppedEventArgs(UBaseObject mediaInfo) public uBaseObject MediaInfo { get; set; }
{ }
MediaInfo = mediaInfo;
}
public UBaseObject MediaInfo { get; set; } public class MediaChangedEventArgs : EventArgs
{
public uBaseObject OldMediaInfo { get; set; }
public uBaseObject NewMediaInfo { get; set; }
} }
} }
+1 -2
View File
@@ -1,6 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
+3 -4
View File
@@ -1,6 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -10,9 +9,9 @@ using MediaBrowser.Model.Session;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
{ {
public static class PlaylistItemFactory public class PlaylistItemFactory
{ {
public static PlaylistItem Create(Photo item, DeviceProfile profile) public PlaylistItem Create(Photo item, DeviceProfile profile)
{ {
var playlistItem = new PlaylistItem var playlistItem = new PlaylistItem
{ {
+79 -56
View File
@@ -1,11 +1,10 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
#pragma warning disable SA1600
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -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 USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
private const string FriendlyName = "Jellyfin"; 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( public async Task<XDocument> SendCommandAsync(
@@ -32,24 +33,27 @@ namespace Emby.Dlna.PlayTo
DeviceService service, DeviceService service,
string command, string command,
string postData, string postData,
string header = null, bool logRequest = true,
CancellationToken cancellationToken = default) string header = null)
{ {
var url = NormalizeServiceUrl(baseUrl, service.ControlUrl); var cancellationToken = CancellationToken.None;
using var response = await PostSoapDataAsync(
url,
$"\"{service.ServiceType}#{command}\"",
postData,
header,
cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
return await XDocument.LoadAsync( using (var response = await PostSoapDataAsync(
stream, url,
LoadOptions.None, $"\"{service.ServiceType}#{command}\"",
cancellationToken).ConfigureAwait(false); 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) private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@@ -60,7 +64,7 @@ namespace Emby.Dlna.PlayTo
return serviceUrl; return serviceUrl;
} }
if (!serviceUrl.StartsWith('/')) if (!serviceUrl.StartsWith("/"))
{ {
serviceUrl = "/" + serviceUrl; serviceUrl = "/" + serviceUrl;
} }
@@ -76,45 +80,55 @@ namespace Emby.Dlna.PlayTo
int eventport, int eventport,
int timeOut = 3600) int timeOut = 3600)
{ {
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url); var options = new HttpRequestOptions
options.Headers.UserAgent.ParseAdd(USERAGENT); {
options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture)); Url = url,
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">"); UserAgent = USERAGENT,
options.Headers.TryAddWithoutValidation("NT", "upnp:event"); LogErrorResponseBody = true,
options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture)); BufferContent = false,
};
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture);
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead) options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">";
.ConfigureAwait(false); options.RequestHeaders["NT"] = "upnp:event";
response.EnsureSuccessStatusCode(); options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture);
using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false))
{
}
} }
public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken) public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
{ {
using var options = new HttpRequestMessage(HttpMethod.Get, url); var options = new HttpRequestOptions
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
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
{ {
return await XDocument.LoadAsync( Url = url,
stream, UserAgent = USERAGENT,
LoadOptions.None, LogErrorResponseBody = true,
cancellationToken).ConfigureAwait(false); BufferContent = false,
}
catch 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 url,
string soapAction, string soapAction,
string postData, string postData,
string header, string header,
bool logRequest,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (soapAction[0] != '\"') if (soapAction[0] != '\"')
@@ -122,20 +136,29 @@ namespace Emby.Dlna.PlayTo
soapAction = $"\"{soapAction}\""; soapAction = $"\"{soapAction}\"";
} }
using var options = new HttpRequestMessage(HttpMethod.Post, url); var options = new HttpRequestOptions
options.Headers.UserAgent.ParseAdd(USERAGENT); {
options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction); Url = url,
options.Headers.TryAddWithoutValidation("Pragma", "no-cache"); UserAgent = USERAGENT,
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); LogErrorResponseBody = true,
BufferContent = false,
CancellationToken = cancellationToken
};
options.RequestHeaders["SOAPAction"] = soapAction;
options.RequestHeaders["Pragma"] = "no-cache";
options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
if (!string.IsNullOrEmpty(header)) if (!string.IsNullOrEmpty(header))
{ {
options.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header); options.RequestHeaders["contentFeatures.dlna.org"] = header;
} }
options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml); options.RequestContentType = "text/xml";
options.RequestContent = postData;
return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); return _httpClient.Post(options);
} }
} }
} }
+14
View 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