Merge branch 'master' into more-track-titles

pull/2788/head
Pika 4 years ago committed by GitHub
commit 3b21abd879
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,13 +1,13 @@
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: 3.1.100 default: 3.1.100
jobs: jobs:
- job: CompatibilityCheck - job: CompatibilityCheck
@ -23,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 dependsOn: Build
steps: steps:
- checkout: none - checkout: none
@ -33,6 +33,13 @@ 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:
@ -72,25 +79,11 @@ jobs:
overWrite: true overWrite: true
flattenFolders: true flattenFolders: true
- task: DownloadGitHubRelease@0
displayName: "Download ABI Compatibility Check Tool"
inputs:
connection: Jellyfin Release Download
userRepository: EraYaN/dotnet-compatibility
defaultVersionType: "latest"
itemPattern: "**-ci.zip"
downloadPath: "$(System.ArtifactsDirectory)"
- task: ExtractFiles@1
displayName: "Extract ABI Compatibility Check Tool"
inputs:
archiveFilePatterns: "$(System.ArtifactsDirectory)/*-ci.zip"
destinationFolder: $(System.ArtifactsDirectory)/tools
cleanDestinationFolder: true
# The `--warnings-only` switch will swallow the return code and not emit any errors. # The `--warnings-only` switch will swallow the return code and not emit any errors.
- task: CmdLine@2 - task: DotNetCoreCLI@2
displayName: "Execute ABI Compatibility Check Tool" displayName: 'Execute ABI Compatibility Check Tool'
inputs: inputs:
script: "dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only" command: custom
custom: compat
arguments: 'current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
workingDirectory: $(System.ArtifactsDirectory) workingDirectory: $(System.ArtifactsDirectory)

@ -1,101 +1,93 @@
parameters: parameters:
LinuxImage: "ubuntu-latest" LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: "Jellyfin.Server/Jellyfin.Server.csproj" RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
DotNetSdkVersion: 3.1.100 DotNetSdkVersion: 3.1.100
jobs: jobs:
- job: MainBuild - job: Build
displayName: Main Build displayName: 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: CmdLine@2 - task: DownloadPipelineArtifact@2
displayName: "Clone Web Client (Master, Release, or Tag)" displayName: 'Download Web Branch'
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')) condition: in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')
inputs: inputs:
script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" path: '$(Agent.TempDirectory)'
artifact: 'jellyfin-web-production'
source: 'specific'
project: 'jellyfin'
pipeline: 'Jellyfin Web'
runBranch: variables['Build.SourceBranch']
- task: CmdLine@2 - task: DownloadPipelineArtifact@2
displayName: "Clone Web Client (PR)" displayName: 'Download Web Target'
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')) condition: eq(variables['Build.Reason'], 'PullRequest')
inputs: inputs:
script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" path: '$(Agent.TempDirectory)'
artifact: 'jellyfin-web-production'
source: 'specific'
project: 'jellyfin'
pipeline: 'Jellyfin Web'
runBranch: variables['System.PullRequest.TargetBranch']
- task: NodeTool@0 - task: ExtractFiles@1
displayName: "Install Node" displayName: 'Extract 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: inputs:
versionSpec: "10.x" archiveFilePatterns: '$(Agent.TempDirectory)/*.zip'
destinationFolder: '$(Build.SourcesDirectory)/MediaBrowser.WebDashboard'
- task: CmdLine@2 cleanDestinationFolder: false
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@0 - 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@0 - 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@0 - 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@0 - 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'

@ -0,0 +1,163 @@
jobs:
- job: BuildPackage
displayName: 'Build Packages'
strategy:
matrix:
CentOS.amd64:
BuildConfiguration: centos.amd64
Fedora.amd64:
BuildConfiguration: fedora.amd64
Debian.amd64:
BuildConfiguration: debian.amd64
Debian.arm64:
BuildConfiguration: debian.arm64
Debian.armhf:
BuildConfiguration: debian.armhf
Ubuntu.amd64:
BuildConfiguration: ubuntu.amd64
Ubuntu.arm64:
BuildConfiguration: ubuntu.arm64
Ubuntu.armhf:
BuildConfiguration: ubuntu.armhf
Linux.amd64:
BuildConfiguration: linux.amd64
Windows.amd64:
BuildConfiguration: windows.amd64
MacOS:
BuildConfiguration: macos
Portable:
BuildConfiguration: portable
pool:
vmImage: 'ubuntu-latest'
steps:
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
displayName: 'Build Dockerfile'
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
displayName: 'Run Dockerfile (unstable)'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
displayName: 'Run Dockerfile (stable)'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
- task: PublishPipelineArtifact@1
displayName: 'Publish Release'
inputs:
targetPath: '$(Build.SourcesDirectory)/deployment/dist'
artifactName: 'jellyfin-server-$(BuildConfiguration)'
- task: SSH@0
displayName: 'Create target directory on repository server'
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- task: CopyFilesOverSSH@0
displayName: 'Upload artifacts to repository server'
inputs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: BuildDocker
displayName: 'Build Docker'
strategy:
matrix:
amd64:
BuildConfiguration: amd64
arm64:
BuildConfiguration: arm64
armhf:
BuildConfiguration: armhf
pool:
vmImage: 'ubuntu-latest'
steps:
- 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')
inputs:
repository: 'jellyfin/jellyfin-server'
command: buildAndPush
buildContext: '.'
Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
containerRegistry: Docker Hub
tags: |
stable-$(Build.BuildNumber)-$(BuildConfiguration)
stable-$(BuildConfiguration)
- job: CollectArtifacts
displayName: 'Collect Artifacts'
dependsOn:
- BuildPackage
- BuildDocker
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
steps:
- task: SSH@0
displayName: 'Update Unstable Repository'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: |
sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
rm $0
exit
- task: SSH@0
displayName: 'Update Stable Repository'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: |
sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
rm $0
exit
- job: PublishNuget
displayName: 'Publish NuGet packages'
dependsOn:
- BuildPackage
condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NuGetCommand@2
inputs:
command: 'pack'
packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
packDestination: '$(Build.ArtifactStagingDirectory)'
- task: NuGetCommand@2
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
includeNugetOrg: 'true'

@ -1,26 +1,25 @@
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: 3.1.100 default: 3.1.100
jobs: jobs:
- job: MainTest - job: Test
displayName: Main Test displayName: 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:
@ -29,14 +28,31 @@ jobs:
submodules: true submodules: true
persistCredentials: false persistCredentials: false
# This is required for the SonarCloud analyzer
- task: UseDotNet@2
displayName: "Install .NET Core SDK 2.1"
condition: eq(variables['ImageName'], 'ubuntu-latest')
inputs:
packageType: sdk
version: '2.1.805'
- 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 .NET Core CLI tests displayName: 'Run CLI Tests'
inputs: inputs:
command: "test" command: "test"
projects: ${{ parameters.TestProjects }} projects: ${{ parameters.TestProjects }}
@ -45,9 +61,20 @@ jobs:
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: ReportGenerator (merge) displayName: 'Run ReportGenerator'
enabled: false
inputs: inputs:
reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml" reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
targetdir: "$(Agent.TempDirectory)/merged/" targetdir: "$(Agent.TempDirectory)/merged/"
@ -56,7 +83,8 @@ 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'
enabled: false
inputs: inputs:
codeCoverageTool: "cobertura" codeCoverageTool: "cobertura"
#summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2 #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2

@ -1,82 +0,0 @@
parameters:
WindowsImage: "windows-latest"
TestProjects: "tests/**/*Tests.csproj"
DotNetSdkVersion: 3.1.100
jobs:
- job: PublishWindows
displayName: Publish Windows
pool:
vmImage: ${{ parameters.WindowsImage }}
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: true
- task: CmdLine@2
displayName: "Clone Web Client (Master, Release, or Tag)"
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
- task: CmdLine@2
displayName: "Clone Web Client (PR)"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest'))
inputs:
script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web"
- task: NodeTool@0
displayName: "Install Node"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
versionSpec: "10.x"
- task: CmdLine@2
displayName: "Build Web Client"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
script: yarn install
workingDirectory: $(Agent.TempDirectory)/jellyfin-web
- task: CopyFiles@2
displayName: "Copy Web Client"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist
contents: "**"
targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
cleanTargetFolder: true
overWrite: true
flattenFolders: false
- task: CmdLine@2
displayName: "Clone UX Repository"
inputs:
script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
- task: PowerShell@2
displayName: "Build NSIS Installer"
inputs:
targetType: "filePath"
filePath: ./deployment/windows/build-jellyfin.ps1
arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
errorActionPreference: "stop"
workingDirectory: $(Build.SourcesDirectory)
- task: CopyFiles@2
displayName: "Copy NSIS Installer"
inputs:
sourceFolder: $(Build.SourcesDirectory)/deployment/windows/
contents: "jellyfin*.exe"
targetFolder: $(System.ArtifactsDirectory)/setup
cleanTargetFolder: true
overWrite: true
flattenFolders: true
- task: PublishPipelineArtifact@0
displayName: "Publish Artifact Setup"
condition: succeeded()
inputs:
targetPath: "$(build.artifactstagingdirectory)/setup"
artifactName: "Jellyfin Server Setup"

@ -1,12 +1,12 @@
name: $(Date:yyyyMMdd)$(Rev:.r) name: $(Date:yyyyMMdd)$(Rev:.r)
variables: variables:
- name: TestProjects - name: TestProjects
value: "tests/**/*Tests.csproj" value: 'tests/**/*Tests.csproj'
- name: RestoreBuildProjects - name: RestoreBuildProjects
value: "Jellyfin.Server/Jellyfin.Server.csproj" value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion - name: DotNetSdkVersion
value: 3.1.100 value: 3.1.100
pr: pr:
autoCancel: true autoCancel: true
@ -15,24 +15,22 @@ trigger:
batch: true batch: true
jobs: jobs:
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- 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'), 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'
- template: azure-pipelines-windows.yml - ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
parameters: - template: azure-pipelines-abi.yml
WindowsImage: "windows-latest"
TestProjects: $(TestProjects)
- template: azure-pipelines-compat.yml
parameters: parameters:
Packages: Packages:
Naming: Naming:
@ -47,4 +45,7 @@ jobs:
Common: Common:
NugetPackageName: Jellyfin.Common NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll AssemblyFileName: MediaBrowser.Common.dll
LinuxImage: "ubuntu-latest" LinuxImage: 'ubuntu-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml

@ -1,59 +0,0 @@
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)"

@ -0,0 +1 @@
../fedora/Makefile

@ -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 = null max_line_length = off
# YAML indentation # YAML indentation
[*.{yml,yaml}] [*.{yml,yaml}]
@ -22,6 +22,7 @@ indent_size = 2
# XML indentation # XML indentation
[*.{csproj,xml}] [*.{csproj,xml}]
indent_size = 2 indent_size = 2
############################### ###############################
# .NET Coding Conventions # # .NET Coding Conventions #
############################### ###############################
@ -51,11 +52,12 @@ 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_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_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 #
############################### ###############################
@ -67,7 +69,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
@ -159,6 +161,7 @@ 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 #
############################### ###############################
@ -189,9 +192,3 @@ 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

@ -0,0 +1,3 @@
# Joshua must review all changes to deployment and build.sh
deployment/* @joshuaboniface
build.sh @joshuaboniface

@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: nuget
directory: "/"
schedule:
interval: weekly
time: '12:00'
open-pull-requests-limit: 10

19
.gitignore vendored

@ -244,14 +244,14 @@ pip-log.txt
######################### #########################
# Artifacts for debian-x64 # Artifacts for debian-x64
deployment/debian-package-x64/pkg-src/.debhelper/ debian/.debhelper/
deployment/debian-package-x64/pkg-src/*.debhelper debian/*.debhelper
deployment/debian-package-x64/pkg-src/debhelper-build-stamp debian/debhelper-build-stamp
deployment/debian-package-x64/pkg-src/files debian/files
deployment/debian-package-x64/pkg-src/jellyfin.substvars debian/jellyfin.substvars
deployment/debian-package-x64/pkg-src/jellyfin/ debian/jellyfin/
# Don't ignore the debian/bin folder # Don't ignore the debian/bin folder
!deployment/debian-package-x64/pkg-src/bin/ !debian/bin/
deployment/**/dist/ deployment/**/dist/
deployment/**/pkg-dist/ deployment/**/pkg-dist/
@ -271,3 +271,8 @@ dist
# BenchmarkDotNet artifacts # BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts BenchmarkDotNet.Artifacts
# Ignore web artifacts from native builds
web/
web-src.*
MediaBrowser.WebDashboard/jellyfin-web

@ -0,0 +1,14 @@
{
// 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": [
]
}

@ -1,9 +1,6 @@
{ {
// Use IntelliSense to find out which attributes exist for C# debugging "version": "0.2.0",
// Use hover for the description of the existing attributes "configurations": [
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{ {
"name": ".NET Core Launch (console)", "name": ".NET Core Launch (console)",
"type": "coreclr", "type": "coreclr",
@ -24,5 +21,8 @@
"request": "attach", "request": "attach",
"processId": "${command:pickProcess}" "processId": "${command:pickProcess}"
} }
,] ],
"env": {
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
}
} }

19
.vscode/tasks.json vendored

@ -10,6 +10,21 @@
"${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/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj"
],
"problemMatcher": "$msCompile"
}
],
"options": {
"env": {
"DOTNET_CLI_TELEMETRY_OPTOUT": "1"
} }
] }
} }

@ -7,6 +7,7 @@
- [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)
- [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)
@ -22,6 +23,7 @@
- [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)
@ -128,6 +130,8 @@
- [xosdy](https://github.com/xosdy) - [xosdy](https://github.com/xosdy)
- [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)
- [Pusta](https://github.com/pusta)
# Emby Contributors # Emby Contributors

@ -1,9 +1,8 @@
ARG DOTNET_VERSION=3.1 ARG DOTNET_VERSION=3.1
ARG FFMPEG_VERSION=latest
FROM node:alpine as web-builder FROM node:alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm \ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& yarn install \ && yarn install \
@ -17,7 +16,6 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg
FROM debian:buster-slim FROM debian:buster-slim
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
@ -27,32 +25,26 @@ 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"
COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg
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
# Install dependencies: # Install dependencies:
# libfontconfig1: needed for Skia # mesa-va-drivers: needed for AMD VAAPI
# 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 \
&& 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 \
ca-certificates \
vainfo \
i965-va-driver \
locales \ locales \
&& apt-get clean autoclean -y\ && apt-get remove gnupg wget apt-transport-https -y \
&& apt-get autoremove -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 \
&& ln -s /opt/ffmpeg/bin/ffmpeg /usr/local/bin \
&& ln -s /opt/ffmpeg/bin/ffprobe /usr/local/bin \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
@ -65,4 +57,4 @@ VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \ "--datadir", "/config", \
"--cachedir", "/cache", \ "--cachedir", "/cache", \
"--ffmpeg", "/usr/local/bin/ffmpeg"] "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]

@ -38,7 +38,7 @@ COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
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 - && \
curl -s https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \ curl -ks https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \ echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \
echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \ echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
apt-get update && \ apt-get update && \
@ -74,4 +74,4 @@ VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \ "--datadir", "/config", \
"--cachedir", "/cache", \ "--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg"] "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]

@ -1,4 +1,6 @@
using System; #pragma warning disable CS1591
using System.Buffers.Binary;
using System.IO; using System.IO;
namespace DvdLib namespace DvdLib
@ -12,19 +14,12 @@ namespace DvdLib
public override ushort ReadUInt16() public override ushort ReadUInt16()
{ {
return BitConverter.ToUInt16(ReadAndReverseBytes(2), 0); return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2));
} }
public override uint ReadUInt32() public override uint ReadUInt32()
{ {
return BitConverter.ToUInt32(ReadAndReverseBytes(4), 0); return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4));
}
private byte[] ReadAndReverseBytes(int count)
{
byte[] val = base.ReadBytes(count);
Array.Reverse(val, 0, count);
return val;
} }
} }
} }

@ -1,17 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<Compile Include="..\SharedVersion.cs" /> <PropertyGroup>
</ItemGroup> <ProjectGuid>{713F42B5-878E-499D-A878-E4C652B1D5E8}</ProjectGuid>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <Compile Include="..\SharedVersion.cs" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

@ -1,3 +1,5 @@
#pragma warning disable CS1591
using System.IO; using System.IO;
namespace DvdLib.Ifo namespace DvdLib.Ifo
@ -5,6 +7,7 @@ namespace DvdLib.Ifo
public class Cell public class Cell
{ {
public CellPlaybackInfo PlaybackInfo { get; private set; } public CellPlaybackInfo PlaybackInfo { get; private set; }
public CellPositionInfo PositionInfo { get; private set; } public CellPositionInfo PositionInfo { get; private set; }
internal void ParsePlayback(BinaryReader br) internal void ParsePlayback(BinaryReader br)

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

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

@ -1,9 +1,13 @@
#pragma warning disable CS1591
namespace DvdLib.Ifo namespace DvdLib.Ifo
{ {
public class Chapter public class Chapter
{ {
public ushort ProgramChainNumber { get; private set; } public ushort ProgramChainNumber { get; private set; }
public ushort ProgramNumber { get; private set; } public ushort ProgramNumber { get; private set; }
public uint ChapterNumber { get; private set; } public uint ChapterNumber { get; private set; }
public Chapter(ushort pgcNum, ushort programNum, uint chapterNum) public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)

@ -1,8 +1,9 @@
#pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using MediaBrowser.Model.IO;
namespace DvdLib.Ifo namespace DvdLib.Ifo
{ {
@ -13,13 +14,10 @@ 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>();
private readonly IFileSystem _fileSystem; public Dvd(string path)
public Dvd(string path, IFileSystem fileSystem)
{ {
_fileSystem = fileSystem;
Titles = new List<Title>(); Titles = new List<Title>();
var allFiles = _fileSystem.GetFiles(path, true).ToList(); var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories);
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));
@ -76,7 +74,7 @@ namespace DvdLib.Ifo
} }
} }
private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles) private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
{ {
var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum); var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
@ -119,12 +117,19 @@ 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) continue; if (t == null)
{
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])) break; if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1]))
{
break;
}
chapNum++; chapNum++;
} }
while (vtsFs.Position < (baseAddr + endaddr)); while (vtsFs.Position < (baseAddr + endaddr));
@ -149,7 +154,10 @@ 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) t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum); if (t != null)
{
t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
}
} }
} }
} }

@ -1,3 +1,5 @@
#pragma warning disable CS1591
using System; using System;
namespace DvdLib.Ifo namespace DvdLib.Ifo
@ -13,8 +15,14 @@ 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) FrameRate = 30; if ((data[3] & 0x80) != 0)
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,10 +1,12 @@
#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 readonly List<Cell> Cells; public IReadOnlyList<Cell> Cells { get; }
public Program(List<Cell> cells) public Program(List<Cell> cells)
{ {

@ -1,3 +1,5 @@
#pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -20,7 +22,9 @@ 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
@ -31,9 +35,11 @@ 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;
@ -69,8 +75,15 @@ namespace DvdLib.Ifo
StillTime = br.ReadByte(); StillTime = br.ReadByte();
byte pbMode = br.ReadByte(); byte pbMode = br.ReadByte();
if (pbMode == 0) PlaybackMode = ProgramPlaybackMode.Sequential; if (pbMode == 0)
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,3 +1,5 @@
#pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -6,8 +8,11 @@ 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;
@ -15,6 +20,7 @@ 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;
@ -53,7 +59,10 @@ 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) EntryProgramChain = pgc; if (entryPgc)
{
EntryProgramChain = pgc;
}
br.BaseStream.Seek(curPos, SeekOrigin.Begin); br.BaseStream.Seek(curPos, SeekOrigin.Begin);
} }

@ -1,3 +1,5 @@
#pragma warning disable CS1591
using System; using System;
namespace DvdLib.Ifo namespace DvdLib.Ifo

@ -11,6 +11,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Dlna.Api namespace Emby.Dlna.Api
{ {
@ -108,7 +109,7 @@ namespace Emby.Dlna.Api
public string Filename { get; set; } public string Filename { get; set; }
} }
public class DlnaServerService : IService, IRequiresRequest public class DlnaServerService : IService
{ {
private const string XMLContentType = "text/xml; charset=UTF-8"; private const string XMLContentType = "text/xml; charset=UTF-8";
@ -127,11 +128,13 @@ namespace Emby.Dlna.Api
public DlnaServerService( public DlnaServerService(
IDlnaManager dlnaManager, IDlnaManager dlnaManager,
IHttpResultFactory httpResultFactory, IHttpResultFactory httpResultFactory,
IServerConfigurationManager configurationManager) IServerConfigurationManager configurationManager,
IHttpContextAccessor httpContextAccessor)
{ {
_dlnaManager = dlnaManager; _dlnaManager = dlnaManager;
_resultFactory = httpResultFactory; _resultFactory = httpResultFactory;
_configurationManager = configurationManager; _configurationManager = configurationManager;
Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor));
} }
private string GetHeader(string name) private string GetHeader(string name)

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

@ -1,13 +1,15 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
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;
@ -31,7 +33,8 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ITVSeriesManager _tvSeriesManager; private readonly ITVSeriesManager _tvSeriesManager;
public ContentDirectory(IDlnaManager dlna, public ContentDirectory(
IDlnaManager dlna,
IUserDataManager userDataManager, IUserDataManager userDataManager,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
ILibraryManager libraryManager, ILibraryManager libraryManager,
@ -130,18 +133,13 @@ namespace Emby.Dlna.ContentDirectory
foreach (var user in _userManager.Users) foreach (var user in _userManager.Users)
{ {
if (user.Policy.IsAdministrator) if (user.HasPermission(PermissionKind.IsAdministrator))
{ {
return user; return user;
} }
} }
foreach (var user in _userManager.Users) return _userManager.Users.FirstOrDefault();
{
return user;
}
return null;
} }
} }
} }

@ -10,6 +10,7 @@ using System.Threading;
using System.Xml; using System.Xml;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
@ -17,7 +18,6 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
@ -28,6 +28,12 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Book = MediaBrowser.Controller.Entities.Book;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Dlna.ContentDirectory namespace Emby.Dlna.ContentDirectory
{ {
@ -460,12 +466,12 @@ namespace Emby.Dlna.ContentDirectory
} }
else if (search.SearchType == SearchType.Playlist) else if (search.SearchType == SearchType.Playlist)
{ {
//items = items.OfType<Playlist>(); // items = items.OfType<Playlist>();
isFolder = true; isFolder = true;
} }
else if (search.SearchType == SearchType.MusicAlbum) else if (search.SearchType == SearchType.MusicAlbum)
{ {
//items = items.OfType<MusicAlbum>(); // items = items.OfType<MusicAlbum>();
isFolder = true; isFolder = true;
} }
@ -731,7 +737,7 @@ namespace Emby.Dlna.ContentDirectory
return GetGenres(item, user, query); return GetGenres(item, user, query);
} }
var array = new ServerItem[] var array = new[]
{ {
new ServerItem(item) new ServerItem(item)
{ {
@ -920,7 +926,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetMovieCollections(User user, InternalItemsQuery query) private QueryResult<ServerItem> GetMovieCollections(User user, InternalItemsQuery query)
{ {
query.Recursive = true; query.Recursive = true;
//query.Parent = parent; // query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(BoxSet).Name }; query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
@ -1115,7 +1121,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query) private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query)
{ {
query.Parent = null; query.Parent = null;
query.IncludeItemTypes = new[] { typeof(Playlist).Name }; query.IncludeItemTypes = new[] { nameof(Playlist) };
query.SetUser(user); query.SetUser(user);
query.Recursive = true; query.Recursive = true;
@ -1132,10 +1138,9 @@ namespace Emby.Dlna.ContentDirectory
{ {
UserId = user.Id, UserId = user.Id,
Limit = 50, Limit = 50,
IncludeItemTypes = new[] { typeof(Audio).Name }, IncludeItemTypes = new[] { nameof(Audio) },
ParentId = parent == null ? Guid.Empty : parent.Id, ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true GroupItems = true
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items); return ToResult(items);
@ -1150,7 +1155,6 @@ namespace Emby.Dlna.ContentDirectory
Limit = query.Limit, Limit = query.Limit,
StartIndex = query.StartIndex, StartIndex = query.StartIndex,
UserId = query.User.Id UserId = query.User.Id
}, new[] { parent }, query.DtoOptions); }, new[] { parent }, query.DtoOptions);
return ToResult(result); return ToResult(result);
@ -1167,7 +1171,6 @@ namespace Emby.Dlna.ContentDirectory
IncludeItemTypes = new[] { typeof(Episode).Name }, IncludeItemTypes = new[] { typeof(Episode).Name },
ParentId = parent == null ? Guid.Empty : parent.Id, ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false GroupItems = false
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items); return ToResult(items);
@ -1177,14 +1180,14 @@ namespace Emby.Dlna.ContentDirectory
{ {
query.OrderBy = Array.Empty<(string, SortOrder)>(); query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(new LatestItemsQuery var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
{ {
UserId = user.Id, UserId = user.Id,
Limit = 50, Limit = 50,
IncludeItemTypes = new[] { typeof(Movie).Name }, IncludeItemTypes = new[] { nameof(Movie) },
ParentId = parent == null ? Guid.Empty : parent.Id, ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true GroupItems = true
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items); return ToResult(items);
@ -1217,7 +1220,11 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true, Recursive = true,
ParentId = parentId, ParentId = parentId,
GenreIds = new[] { item.Id }, GenreIds = new[] { item.Id },
IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name }, IncludeItemTypes = new[]
{
nameof(Movie),
nameof(Series)
},
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
@ -1350,6 +1357,7 @@ namespace Emby.Dlna.ContentDirectory
internal class ServerItem internal class ServerItem
{ {
public BaseItem Item { get; set; } public BaseItem Item { get; set; }
public StubType? StubType { get; set; } public StubType? StubType { get; set; }
public ServerItem(BaseItem item) public ServerItem(BaseItem item)

@ -6,14 +6,13 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Xml; using System.Xml;
using Emby.Dlna.Configuration;
using Emby.Dlna.ContentDirectory; using Emby.Dlna.ContentDirectory;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
@ -23,6 +22,13 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
using Season = MediaBrowser.Controller.Entities.TV.Season;
using Series = MediaBrowser.Controller.Entities.TV.Series;
using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute;
namespace Emby.Dlna.Didl namespace Emby.Dlna.Didl
{ {
@ -92,21 +98,21 @@ namespace Emby.Dlna.Didl
{ {
using (var writer = XmlWriter.Create(builder, settings)) using (var writer = XmlWriter.Create(builder, settings))
{ {
//writer.WriteStartDocument(); // writer.WriteStartDocument();
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
writer.WriteAttributeString("xmlns", "dc", null, NS_DC); writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
//didl.SetAttribute("xmlns:sec", NS_SEC); // didl.SetAttribute("xmlns:sec", NS_SEC);
WriteXmlRootAttributes(_profile, writer); WriteXmlRootAttributes(_profile, writer);
WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo); WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo);
writer.WriteFullEndElement(); writer.WriteFullEndElement();
//writer.WriteEndDocument(); // writer.WriteEndDocument();
} }
return builder.ToString(); return builder.ToString();
@ -421,61 +427,102 @@ namespace Emby.Dlna.Didl
case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows"); case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes"); case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
case StubType.Series: return _localization.GetLocalizedString("Shows"); case StubType.Series: return _localization.GetLocalizedString("Shows");
default: break;
} }
} }
if (item is Episode episode && context is Season season) return item is Episode episode
? GetEpisodeDisplayName(episode, context)
: item.Name;
}
/// <summary>
/// Gets episode display name appropriate for the given context.
/// </summary>
/// <remarks>
/// If context is a season, this will return a string containing just episode number and name.
/// Otherwise the result will include series nams and season number.
/// </remarks>
/// <param name="episode">The episode.</param>
/// <param name="context">Current context.</param>
/// <returns>Formatted name of the episode.</returns>
private string GetEpisodeDisplayName(Episode episode, BaseItem context)
{
string[] components;
if (context is Season season)
{ {
// This is a special embedded within a season // This is a special embedded within a season
if (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value == 0 if (episode.ParentIndexNumber.HasValue && episode.ParentIndexNumber.Value == 0
&& season.IndexNumber.HasValue && season.IndexNumber.Value != 0) && season.IndexNumber.HasValue && season.IndexNumber.Value != 0)
{ {
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("ValueSpecialEpisodeName"), _localization.GetLocalizedString("ValueSpecialEpisodeName"),
item.Name); episode.Name);
} }
if (item.IndexNumber.HasValue) // inside a season use simple format (ex. '12 - Episode Name')
{ var epNumberName = GetEpisodeIndexFullName(episode);
var number = item.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); components = new[] { epNumberName, episode.Name };
}
else
{
// outside a season include series and season details (ex. 'TV Show - S05E11 - Episode Name')
var epNumberName = GetEpisodeNumberDisplayName(episode);
components = new[] { episode.SeriesName, epNumberName, episode.Name };
}
if (episode.IndexNumberEnd.HasValue) return string.Join(" - ", components.Where(NotNullOrWhiteSpace));
{ }
number += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture);
}
return number + " - " + item.Name; /// <summary>
} /// Gets complete episode number.
} /// </summary>
else if (item is Episode ep) /// <param name="episode">The episode.</param>
/// <returns>For single episodes returns just the number. For double episodes - current and ending numbers.</returns>
private string GetEpisodeIndexFullName(Episode episode)
{
var name = string.Empty;
if (episode.IndexNumber.HasValue)
{ {
var parent = ep.GetParent(); name += episode.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var name = parent.Name + " - ";
if (ep.ParentIndexNumber.HasValue) if (episode.IndexNumberEnd.HasValue)
{
name += "S" + ep.ParentIndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
}
else if (!item.IndexNumber.HasValue)
{ {
return name + " - " + item.Name; name += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture);
} }
}
name += "E" + ep.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); return name;
if (ep.IndexNumberEnd.HasValue) }
{
name += "-" + ep.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture); /// <summary>
} /// Gets episode number formatted as 'S##E##'.
/// </summary>
/// <param name="episode">The episode.</param>
/// <returns>Formatted episode number.</returns>
private string GetEpisodeNumberDisplayName(Episode episode)
{
var name = string.Empty;
var seasonNumber = episode.Season?.IndexNumber;
name += " - " + item.Name; if (seasonNumber.HasValue)
return name; {
name = "S" + seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture);
} }
return item.Name; var indexName = GetEpisodeIndexFullName(episode);
if (!string.IsNullOrWhiteSpace(indexName))
{
name += "E" + indexName;
}
return name;
} }
private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
{ {
writer.WriteStartElement(string.Empty, "res", NS_DIDL); writer.WriteStartElement(string.Empty, "res", NS_DIDL);
@ -628,7 +675,7 @@ namespace Emby.Dlna.Didl
return; return;
} }
MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null; XmlAttribute secAttribute = null;
foreach (var attribute in _profile.XmlRootAttributes) foreach (var attribute in _profile.XmlRootAttributes)
{ {
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase)) if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@ -658,13 +705,13 @@ namespace Emby.Dlna.Didl
} }
/// <summary> /// <summary>
/// Adds fields used by both items and folders /// Adds fields used by both items and folders.
/// </summary> /// </summary>
private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
{ {
// Don't filter on dc:title because not all devices will include it in the filter // Don't filter on dc:title because not all devices will include it in the filter
// MediaMonkey for example won't display content without a title // MediaMonkey for example won't display content without a title
//if (filter.Contains("dc:title")) // if (filter.Contains("dc:title"))
{ {
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC); AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
} }
@ -703,7 +750,7 @@ namespace Emby.Dlna.Didl
AddValue(writer, "dc", "description", desc, NS_DC); AddValue(writer, "dc", "description", desc, NS_DC);
} }
} }
//if (filter.Contains("upnp:longDescription")) // if (filter.Contains("upnp:longDescription"))
//{ //{
// if (!string.IsNullOrWhiteSpace(item.Overview)) // if (!string.IsNullOrWhiteSpace(item.Overview))
// { // {
@ -718,6 +765,7 @@ namespace Emby.Dlna.Didl
{ {
AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC); AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
} }
if (filter.Contains("upnp:rating")) if (filter.Contains("upnp:rating"))
{ {
AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP); AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
@ -953,7 +1001,6 @@ namespace Emby.Dlna.Didl
} }
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN"); AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
} }
private void AddImageResElement( private void AddImageResElement(
@ -1006,10 +1053,12 @@ namespace Emby.Dlna.Didl
{ {
return GetImageInfo(item, ImageType.Primary); return GetImageInfo(item, ImageType.Primary);
} }
if (item.HasImage(ImageType.Thumb)) if (item.HasImage(ImageType.Thumb))
{ {
return GetImageInfo(item, ImageType.Thumb); return GetImageInfo(item, ImageType.Thumb);
} }
if (item.HasImage(ImageType.Backdrop)) if (item.HasImage(ImageType.Backdrop))
{ {
if (item is Channel) if (item is Channel)
@ -1018,19 +1067,58 @@ namespace Emby.Dlna.Didl
} }
} }
item = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); // For audio tracks without art use album art if available.
if (item is Audio audioItem)
{
var album = audioItem.AlbumEntity;
return album != null && album.HasImage(ImageType.Primary)
? GetImageInfo(album, ImageType.Primary)
: null;
}
// Don't look beyond album/playlist level. Metadata service may assign an image from a different album/show to the parent folder.
if (item is MusicAlbum || item is Playlist)
{
return null;
}
if (item != null) // For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item.
var parentWithImage = GetFirstParentWithImageBelowUserRoot(item);
if (parentWithImage != null)
{ {
if (item.HasImage(ImageType.Primary)) return GetImageInfo(parentWithImage, ImageType.Primary);
{
return GetImageInfo(item, ImageType.Primary);
}
} }
return null; return null;
} }
private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
{
if (item == null)
{
return null;
}
if (item.HasImage(ImageType.Primary))
{
return item;
}
var parent = item.GetParent();
if (parent is UserRootFolder)
{
return null;
}
// terminate in case we went past user root folder (unlikely?)
if (parent is Folder folder && folder.IsRoot)
{
return null;
}
return GetFirstParentWithImageBelowUserRoot(parent);
}
private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type) private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
{ {
var imageInfo = item.GetImageInfo(type, 0); var imageInfo = item.GetImageInfo(type, 0);
@ -1050,25 +1138,24 @@ namespace Emby.Dlna.Didl
if (width == 0 || height == 0) if (width == 0 || height == 0)
{ {
//_imageProcessor.GetImageSize(item, imageInfo); // _imageProcessor.GetImageSize(item, imageInfo);
width = null; width = null;
height = null; height = null;
} }
else if (width == -1 || height == -1) else if (width == -1 || height == -1)
{ {
width = null; width = null;
height = null; height = null;
} }
//try // try
//{ //{
// var size = _imageProcessor.GetImageSize(imageInfo); // var size = _imageProcessor.GetImageSize(imageInfo);
// width = size.Width; // width = size.Width;
// height = size.Height; // height = size.Height;
//} //}
//catch // catch
//{ //{
//} //}

@ -12,7 +12,6 @@ namespace Emby.Dlna.Didl
public Filter() public Filter()
: this("*") : this("*")
{ {
} }
public Filter(string filter) public Filter(string filter)
@ -26,7 +25,7 @@ namespace Emby.Dlna.Didl
{ {
// 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. // 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 true;
//return _all || ListHelper.ContainsIgnoreCase(_fields, field); // return _all || ListHelper.ContainsIgnoreCase(_fields, field);
} }
} }
} }

@ -31,7 +31,7 @@ namespace Emby.Dlna
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILogger _logger; private readonly ILogger<DlnaManager> _logger;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
@ -49,7 +49,7 @@ namespace Emby.Dlna
_xmlSerializer = xmlSerializer; _xmlSerializer = xmlSerializer;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_appPaths = appPaths; _appPaths = appPaths;
_logger = loggerFactory.CreateLogger("Dlna"); _logger = loggerFactory.CreateLogger<DlnaManager>();
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_appHost = appHost; _appHost = appHost;
} }
@ -88,7 +88,6 @@ namespace Emby.Dlna
.Select(i => i.Item2) .Select(i => i.Item2)
.ToList(); .ToList();
} }
} }
public DeviceProfile GetDefaultProfile() public DeviceProfile GetDefaultProfile()
@ -141,55 +140,73 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.DeviceDescription)) if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
{ {
if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription)) if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.FriendlyName)) if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
{ {
if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)) if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.Manufacturer)) if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
{ {
if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)) if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl)) if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
{ {
if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)) if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.ModelDescription)) if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
{ {
if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)) if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.ModelName)) if (!string.IsNullOrEmpty(profileInfo.ModelName))
{ {
if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName)) if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.ModelNumber)) if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
{ {
if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)) if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.ModelUrl)) if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
{ {
if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)) if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
{
return false; return false;
}
} }
if (!string.IsNullOrEmpty(profileInfo.SerialNumber)) if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
{ {
if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber)) if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
{
return false; return false;
}
} }
return true; return true;
@ -251,7 +268,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);
@ -439,6 +456,7 @@ 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");
@ -464,6 +482,7 @@ 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);
} }
@ -474,7 +493,7 @@ 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 serlialize properly to xml (different root element tag name) /// If it's a subclass it may not serlialize properly to xml (different root element tag name).
/// </summary> /// </summary>
/// <param name="profile"></param> /// <param name="profile"></param>
/// <returns></returns> /// <returns></returns>
@ -493,6 +512,7 @@ namespace Emby.Dlna
class InternalProfileInfo class InternalProfileInfo
{ {
internal DeviceProfileInfo Info { get; set; } internal DeviceProfileInfo Info { get; set; }
internal string Path { get; set; } internal string Path { get; set; }
} }
@ -566,9 +586,9 @@ namespace Emby.Dlna
new Foobar2000Profile(), new Foobar2000Profile(),
new SharpSmartTvProfile(), new SharpSmartTvProfile(),
new MediaMonkeyProfile(), new MediaMonkeyProfile(),
//new Windows81Profile(), // new Windows81Profile(),
//new WindowsMediaCenterProfile(), // new WindowsMediaCenterProfile(),
//new WindowsPhoneProfile(), // new WindowsPhoneProfile(),
new DirectTvProfile(), new DirectTvProfile(),
new DishHopperJoeyProfile(), new DishHopperJoeyProfile(),
new DefaultProfile(), new DefaultProfile(),

@ -1,5 +1,10 @@
<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>

@ -31,18 +31,26 @@ namespace Emby.Dlna.Eventing
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;
subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300; _logger.LogDebug(
int timeoutSeconds = subscription.TimeoutSeconds; "Renewing event subscription for {0} with timeout of {1} to {2}",
subscription.SubscriptionTime = DateTime.UtcNow; subscription.NotificationType,
timeoutSeconds,
subscription.CallbackUrl);
_logger.LogDebug( return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
"Renewing event subscription for {0} with timeout of {1} to {2}", }
subscription.NotificationType,
timeoutSeconds,
subscription.CallbackUrl);
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds); return new EventSubscriptionResponse
{
Content = string.Empty,
ContentType = "text/plain"
};
} }
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl) public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
@ -144,12 +152,17 @@ namespace Emby.Dlna.Eventing
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">"); builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
foreach (var key in stateVariables.Keys) foreach (var key in stateVariables.Keys)
{ {
builder.Append("<e:property>"); builder.Append("<e:property>")
builder.Append("<" + key + ">"); .Append('<')
builder.Append(stateVariables[key]); .Append(key)
builder.Append("</" + key + ">"); .Append('>')
builder.Append("</e:property>"); .Append(stateVariables[key])
.Append("</")
.Append(key)
.Append('>')
.Append("</e:property>");
} }
builder.Append("</e:propertyset>"); builder.Append("</e:propertyset>");
var options = new HttpRequestOptions var options = new HttpRequestOptions
@ -169,7 +182,6 @@ namespace Emby.Dlna.Eventing
{ {
using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false)) using (await _httpClient.SendAsync(options, new HttpMethod("NOTIFY")).ConfigureAwait(false))
{ {
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)

@ -7,10 +7,13 @@ 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; }

@ -33,10 +33,8 @@ namespace Emby.Dlna.Main
public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
{ {
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly ILogger _logger; private readonly ILogger<DlnaEntryPoint> _logger;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private PlayToManager _manager;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -47,14 +45,13 @@ namespace Emby.Dlna.Main
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery; private readonly IDeviceDiscovery _deviceDiscovery;
private SsdpDevicePublisher _Publisher;
private readonly ISocketFactory _socketFactory; private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
private PlayToManager _manager;
private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer; private ISsdpCommunicationsServer _communicationsServer;
internal IContentDirectory ContentDirectory { get; private set; } internal IContentDirectory ContentDirectory { get; private set; }
@ -65,7 +62,8 @@ namespace Emby.Dlna.Main
public static DlnaEntryPoint Current; public static DlnaEntryPoint Current;
public DlnaEntryPoint(IServerConfigurationManager config, public DlnaEntryPoint(
IServerConfigurationManager config,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IServerApplicationHost appHost, IServerApplicationHost appHost,
ISessionManager sessionManager, ISessionManager sessionManager,
@ -99,7 +97,7 @@ namespace Emby.Dlna.Main
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_socketFactory = socketFactory; _socketFactory = socketFactory;
_networkManager = networkManager; _networkManager = networkManager;
_logger = loggerFactory.CreateLogger("Dlna"); _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
ContentDirectory = new ContentDirectory.ContentDirectory( ContentDirectory = new ContentDirectory.ContentDirectory(
dlnaManager, dlnaManager,
@ -133,20 +131,20 @@ namespace Emby.Dlna.Main
{ {
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
ReloadComponents(); await ReloadComponents().ConfigureAwait(false);
_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
} }
void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{ {
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
{ {
ReloadComponents(); await ReloadComponents().ConfigureAwait(false);
} }
} }
private async void ReloadComponents() private async Task ReloadComponents()
{ {
var options = _config.GetDlnaConfiguration(); var options = _config.GetDlnaConfiguration();
@ -180,7 +178,7 @@ namespace Emby.Dlna.Main
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows || var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
OperatingSystem.Id == OperatingSystemId.Linux; OperatingSystem.Id == OperatingSystemId.Linux;
_communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding) _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{ {
IsShared = true IsShared = true
}; };
@ -231,20 +229,22 @@ namespace Emby.Dlna.Main
return; return;
} }
if (_Publisher != null) if (_publisher != null)
{ {
return; return;
} }
try try
{ {
_Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost); _publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
_Publisher.LogFunction = LogMessage; {
_Publisher.SupportPnpRootDevice = false; LogFunction = LogMessage,
SupportPnpRootDevice = false
};
await RegisterServerEndpoints().ConfigureAwait(false); await RegisterServerEndpoints().ConfigureAwait(false);
_Publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -266,6 +266,12 @@ namespace Emby.Dlna.Main
continue; continue;
} }
// Limit to LAN addresses only
if (!_networkManager.IsAddressInSubnets(address, true, true))
{
continue;
}
var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
@ -275,7 +281,7 @@ namespace Emby.Dlna.Main
var device = new SsdpRootDevice var device = new SsdpRootDevice
{ {
CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info. CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
Location = uri, // Must point to the URL that serves your devices UPnP description document. Location = uri, // Must point to the URL that serves your devices UPnP description document.
Address = address, Address = address,
SubnetMask = _networkManager.GetLocalIpSubnetMask(address), SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
@ -287,13 +293,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)
@ -319,12 +325,13 @@ namespace Emby.Dlna.Main
{ {
guid = text.GetMD5(); guid = text.GetMD5();
} }
return guid.ToString("N", CultureInfo.InvariantCulture); return guid.ToString("N", CultureInfo.InvariantCulture);
} }
private void SetProperies(SsdpDevice device, string fullDeviceType) private void SetProperies(SsdpDevice device, string fullDeviceType)
{ {
var service = fullDeviceType.Replace("urn:", string.Empty).Replace(":1", string.Empty); var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
var serviceParts = service.Split(':'); var serviceParts = service.Split(':');
@ -335,7 +342,6 @@ namespace Emby.Dlna.Main
device.DeviceType = serviceParts[2]; device.DeviceType = serviceParts[2];
} }
private readonly object _syncLock = new object();
private void StartPlayToManager() private void StartPlayToManager()
{ {
lock (_syncLock) lock (_syncLock)
@ -347,7 +353,8 @@ namespace Emby.Dlna.Main
try try
{ {
_manager = new PlayToManager(_logger, _manager = new PlayToManager(
_logger,
_sessionManager, _sessionManager,
_libraryManager, _libraryManager,
_userManager, _userManager,
@ -386,6 +393,7 @@ namespace Emby.Dlna.Main
{ {
_logger.LogError(ex, "Error disposing PlayTo manager"); _logger.LogError(ex, "Error disposing PlayTo manager");
} }
_manager = null; _manager = null;
} }
} }
@ -412,11 +420,11 @@ namespace Emby.Dlna.Main
public void DisposeDevicePublisher() public void DisposeDevicePublisher()
{ {
if (_Publisher != null) if (_publisher != null)
{ {
_logger.LogInformation("Disposing SsdpDevicePublisher"); _logger.LogInformation("Disposing SsdpDevicePublisher");
_Publisher.Dispose(); _publisher.Dispose();
_Publisher = null; _publisher = null;
} }
} }
} }

@ -4,12 +4,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Security;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Server;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -19,8 +19,6 @@ namespace Emby.Dlna.PlayTo
{ {
public class Device : IDisposable public class Device : IDisposable
{ {
#region Fields & Properties
private Timer _timer; private Timer _timer;
public DeviceInfo Properties { get; set; } public DeviceInfo Properties { get; set; }
@ -34,9 +32,10 @@ namespace Emby.Dlna.PlayTo
{ {
get get
{ {
RefreshVolumeIfNeeded(); RefreshVolumeIfNeeded().GetAwaiter().GetResult();
return _volume; return _volume;
} }
set => _volume = value; set => _volume = value;
} }
@ -52,10 +51,10 @@ namespace Emby.Dlna.PlayTo
public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED; public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
#endregion
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
public Action OnDeviceUnavailable { get; set; } public Action OnDeviceUnavailable { get; set; }
@ -76,24 +75,24 @@ namespace Emby.Dlna.PlayTo
private DateTime _lastVolumeRefresh; private DateTime _lastVolumeRefresh;
private bool _volumeRefreshActive; private bool _volumeRefreshActive;
private void RefreshVolumeIfNeeded() private Task RefreshVolumeIfNeeded()
{ {
if (!_volumeRefreshActive) if (_volumeRefreshActive
{ && DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
return;
}
if (DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
{ {
_lastVolumeRefresh = DateTime.UtcNow; _lastVolumeRefresh = DateTime.UtcNow;
RefreshVolume(CancellationToken.None); return RefreshVolume();
} }
return Task.CompletedTask;
} }
private async void RefreshVolume(CancellationToken cancellationToken) private async Task RefreshVolume(CancellationToken cancellationToken = default)
{ {
if (_disposed) if (_disposed)
{
return; return;
}
try try
{ {
@ -141,8 +140,6 @@ namespace Emby.Dlna.PlayTo
} }
} }
#region Commanding
public Task VolumeDown(CancellationToken cancellationToken) public Task VolumeDown(CancellationToken cancellationToken)
{ {
var sendVolume = Math.Max(Volume - 5, 0); var sendVolume = Math.Max(Volume - 5, 0);
@ -211,7 +208,9 @@ namespace Emby.Dlna.PlayTo
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
if (command == null) if (command == null)
{
return false; return false;
}
var service = GetServiceRenderingControl(); var service = GetServiceRenderingControl();
@ -232,7 +231,7 @@ namespace Emby.Dlna.PlayTo
} }
/// <summary> /// <summary>
/// Sets volume on a scale of 0-100 /// Sets volume on a scale of 0-100.
/// </summary> /// </summary>
public async Task SetVolume(int value, CancellationToken cancellationToken) public async Task SetVolume(int value, CancellationToken cancellationToken)
{ {
@ -240,7 +239,9 @@ namespace Emby.Dlna.PlayTo
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
if (command == null) if (command == null)
{
return; return;
}
var service = GetServiceRenderingControl(); var service = GetServiceRenderingControl();
@ -263,7 +264,9 @@ namespace Emby.Dlna.PlayTo
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
if (command == null) if (command == null)
{
return; return;
}
var service = GetAvTransportService(); var service = GetAvTransportService();
@ -288,7 +291,9 @@ namespace Emby.Dlna.PlayTo
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
if (command == null) if (command == null)
{
return; return;
}
var dictionary = new Dictionary<string, string> var dictionary = new Dictionary<string, string>
{ {
@ -329,7 +334,7 @@ namespace Emby.Dlna.PlayTo
return string.Empty; return string.Empty;
} }
return DescriptionXmlBuilder.Escape(value); return SecurityElement.Escape(value);
} }
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken) private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
@ -401,11 +406,8 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true); RestartTimer(true);
} }
#endregion
#region Get data
private int _connectFailureCount; private int _connectFailureCount;
private async void TimerCallback(object sender) private async void TimerCallback(object sender)
{ {
if (_disposed) if (_disposed)
@ -458,7 +460,9 @@ namespace Emby.Dlna.PlayTo
_connectFailureCount = 0; _connectFailureCount = 0;
if (_disposed) if (_disposed)
{
return; return;
}
// If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
if (transportState.Value == TRANSPORTSTATE.STOPPED) if (transportState.Value == TRANSPORTSTATE.STOPPED)
@ -478,7 +482,9 @@ namespace Emby.Dlna.PlayTo
catch (Exception ex) catch (Exception ex)
{ {
if (_disposed) if (_disposed)
{
return; return;
}
_logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name); _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
@ -494,6 +500,7 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
} }
RestartTimerInactive(); RestartTimerInactive();
} }
} }
@ -578,7 +585,9 @@ namespace Emby.Dlna.PlayTo
cancellationToken: cancellationToken).ConfigureAwait(false); cancellationToken: cancellationToken).ConfigureAwait(false);
if (result == null || result.Document == null) if (result == null || result.Document == null)
{
return; return;
}
var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse") var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
.Select(i => i.Element("CurrentMute")) .Select(i => i.Element("CurrentMute"))
@ -750,7 +759,7 @@ namespace Emby.Dlna.PlayTo
if (track == null) if (track == null)
{ {
//If track is null, some vendors do this, use GetMediaInfo instead // If track is null, some vendors do this, use GetMediaInfo instead
return (true, null); return (true, null);
} }
@ -794,7 +803,6 @@ namespace Emby.Dlna.PlayTo
} }
catch (XmlException) catch (XmlException)
{ {
} }
// first try to add a root node with a dlna namesapce // first try to add a root node with a dlna namesapce
@ -806,7 +814,6 @@ namespace Emby.Dlna.PlayTo
} }
catch (XmlException) catch (XmlException)
{ {
} }
// some devices send back invalid xml // some devices send back invalid xml
@ -816,7 +823,6 @@ namespace Emby.Dlna.PlayTo
} }
catch (XmlException) catch (XmlException)
{ {
} }
return null; return null;
@ -871,10 +877,6 @@ namespace Emby.Dlna.PlayTo
return new string[4]; return new string[4];
} }
#endregion
#region From XML
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken) private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
{ {
if (AvCommands != null) if (AvCommands != null)
@ -1069,8 +1071,6 @@ namespace Emby.Dlna.PlayTo
return new Device(deviceProperties, httpClient, logger, config); return new Device(deviceProperties, httpClient, logger, config);
} }
#endregion
private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private static DeviceIcon CreateIcon(XElement element) private static DeviceIcon CreateIcon(XElement element)
{ {
@ -1194,8 +1194,6 @@ namespace Emby.Dlna.PlayTo
}); });
} }
#region IDisposable
bool _disposed; bool _disposed;
public void Dispose() public void Dispose()
@ -1222,8 +1220,6 @@ namespace Emby.Dlna.PlayTo
_disposed = true; _disposed = true;
} }
#endregion
public override string ToString() public override string ToString()
{ {
return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl); return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);

@ -7,6 +7,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
@ -22,6 +23,7 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Photo = MediaBrowser.Controller.Entities.Photo;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
{ {
@ -146,11 +148,14 @@ namespace Emby.Dlna.PlayTo
{ {
var positionTicks = GetProgressPositionTicks(streamInfo); var positionTicks = GetProgressPositionTicks(streamInfo);
ReportPlaybackStopped(streamInfo, positionTicks); await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
} }
streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager); streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
if (streamInfo.Item == null) return; if (streamInfo.Item == null)
{
return;
}
var newItemProgress = GetProgressInfo(streamInfo); var newItemProgress = GetProgressInfo(streamInfo);
@ -173,11 +178,14 @@ namespace Emby.Dlna.PlayTo
{ {
var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager); var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
if (streamInfo.Item == null) return; if (streamInfo.Item == null)
{
return;
}
var positionTicks = GetProgressPositionTicks(streamInfo); var positionTicks = GetProgressPositionTicks(streamInfo);
ReportPlaybackStopped(streamInfo, positionTicks); await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false); var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
@ -185,7 +193,7 @@ namespace Emby.Dlna.PlayTo
(_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) : (_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
mediaSource.RunTimeTicks; mediaSource.RunTimeTicks;
var playedToCompletion = (positionTicks.HasValue && positionTicks.Value == 0); var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
if (!playedToCompletion && duration.HasValue && positionTicks.HasValue) if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
{ {
@ -210,7 +218,7 @@ namespace Emby.Dlna.PlayTo
} }
} }
private async void ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks) private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
{ {
try try
{ {
@ -220,7 +228,6 @@ namespace Emby.Dlna.PlayTo
SessionId = _session.Id, SessionId = _session.Id,
PositionTicks = positionTicks, PositionTicks = positionTicks,
MediaSourceId = streamInfo.MediaSourceId MediaSourceId = streamInfo.MediaSourceId
}).ConfigureAwait(false); }).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
@ -418,6 +425,7 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
return; return;
} }
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
} }
} }
@ -441,7 +449,13 @@ namespace Emby.Dlna.PlayTo
} }
} }
private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) private PlaylistItem CreatePlaylistItem(
BaseItem item,
User user,
long startPostionTicks,
string mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex)
{ {
var deviceInfo = _device.Properties; var deviceInfo = _device.Properties;
@ -700,6 +714,7 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentException("Volume argument cannot be null"); throw new ArgumentException("Volume argument cannot be null");
} }
default: default:
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -785,12 +800,15 @@ namespace Emby.Dlna.PlayTo
public int? SubtitleStreamIndex { get; set; } public int? SubtitleStreamIndex { get; set; }
public string DeviceProfileId { get; set; } public string DeviceProfileId { get; set; }
public string DeviceId { get; set; } public string DeviceId { get; set; }
public string MediaSourceId { get; set; } public string MediaSourceId { get; set; }
public string LiveStreamId { get; set; } public string LiveStreamId { get; set; }
public BaseItem Item { get; set; } public BaseItem Item { get; set; }
private MediaSourceInfo MediaSource; private MediaSourceInfo MediaSource;
private IMediaSourceManager _mediaSourceManager; private IMediaSourceManager _mediaSourceManager;
@ -908,7 +926,8 @@ namespace Emby.Dlna.PlayTo
return 0; return 0;
} }
public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) /// <inheritdoc />
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
{ {
if (_disposed) if (_disposed)
{ {
@ -924,10 +943,12 @@ namespace Emby.Dlna.PlayTo
{ {
return SendPlayCommand(data as PlayRequest, cancellationToken); return SendPlayCommand(data as PlayRequest, cancellationToken);
} }
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{ {
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
} }
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{ {
return SendGeneralCommand(data as GeneralCommand, cancellationToken); return SendGeneralCommand(data as GeneralCommand, cancellationToken);

@ -78,9 +78,15 @@ namespace Emby.Dlna.PlayTo
var info = e.Argument; var info = e.Argument;
if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty; if (!info.Headers.TryGetValue("USN", out string usn))
{
usn = string.Empty;
}
if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty; if (!info.Headers.TryGetValue("NT", out string nt))
{
nt = string.Empty;
}
string location = info.Location.ToString(); string location = info.Location.ToString();
@ -88,7 +94,7 @@ namespace Emby.Dlna.PlayTo
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 && if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1) nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
{ {
//_logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location); // _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
return; return;
} }
@ -112,7 +118,6 @@ namespace Emby.Dlna.PlayTo
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -133,6 +138,7 @@ namespace Emby.Dlna.PlayTo
usn = usn.Substring(index); usn = usn.Substring(index);
found = true; found = true;
} }
index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase); index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
if (index != -1) if (index != -1)
{ {
@ -184,7 +190,8 @@ namespace Emby.Dlna.PlayTo
serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress); serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
} }
controller = new PlayToController(sessionInfo, controller = new PlayToController(
sessionInfo,
_sessionManager, _sessionManager,
_libraryManager, _libraryManager,
_logger, _logger,
@ -242,7 +249,6 @@ namespace Emby.Dlna.PlayTo
} }
catch catch
{ {
} }
_sessionLock.Dispose(); _sessionLock.Dispose();

@ -12,6 +12,7 @@ namespace Emby.Dlna.PlayTo
public class MediaChangedEventArgs : EventArgs public class MediaChangedEventArgs : EventArgs
{ {
public uBaseObject OldMediaInfo { get; set; } public uBaseObject OldMediaInfo { get; set; }
public uBaseObject NewMediaInfo { get; set; } public uBaseObject NewMediaInfo { get; set; }
} }
} }

@ -91,7 +91,6 @@ namespace Emby.Dlna.PlayTo
using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false)) using (await _httpClient.SendAsync(options, new HttpMethod("SUBSCRIBE")).ConfigureAwait(false))
{ {
} }
} }

@ -44,10 +44,12 @@ namespace Emby.Dlna.PlayTo
{ {
return MediaBrowser.Model.Entities.MediaType.Audio; return MediaBrowser.Model.Entities.MediaType.Audio;
} }
if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1) if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
{ {
return MediaBrowser.Model.Entities.MediaType.Video; return MediaBrowser.Model.Entities.MediaType.Video;
} }
if (classType.IndexOf("image", StringComparison.Ordinal) != -1) if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
{ {
return MediaBrowser.Model.Entities.MediaType.Photo; return MediaBrowser.Model.Entities.MediaType.Photo;

@ -12,7 +12,7 @@ namespace Emby.Dlna.Profiles
{ {
Name = "Generic Device"; Name = "Generic Device";
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*"; ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
Manufacturer = "Jellyfin"; Manufacturer = "Jellyfin";
ModelDescription = "UPnP/AV 1.0 Compliant Media Server"; ModelDescription = "UPnP/AV 1.0 Compliant Media Server";
@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
public void AddXmlRootAttribute(string name, string value) public void AddXmlRootAttribute(string name, string value)
{ {
var atts = XmlRootAttributes ?? new XmlAttribute[] { }; var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
var list = atts.ToList(); var list = atts.ToList();
list.Add(new XmlAttribute list.Add(new XmlAttribute

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

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

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

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

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

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

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

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

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

@ -21,7 +21,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -26,7 +26,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -27,7 +27,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>10</TimelineOffsetSeconds> <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
<RequiresPlainVideoItems>true</RequiresPlainVideoItems> <RequiresPlainVideoItems>true</RequiresPlainVideoItems>
<RequiresPlainFolders>true</RequiresPlainFolders> <RequiresPlainFolders>true</RequiresPlainFolders>

@ -27,7 +27,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>10</TimelineOffsetSeconds> <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -25,7 +25,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -27,7 +27,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -27,7 +27,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -28,7 +28,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>10</TimelineOffsetSeconds> <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -21,7 +21,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -27,7 +27,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -27,7 +27,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>true</RequiresPlainVideoItems> <RequiresPlainVideoItems>true</RequiresPlainVideoItems>
<RequiresPlainFolders>true</RequiresPlainFolders> <RequiresPlainFolders>true</RequiresPlainFolders>

@ -29,7 +29,7 @@
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<SonyAggregationFlags>10</SonyAggregationFlags> <SonyAggregationFlags>10</SonyAggregationFlags>
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -29,7 +29,7 @@
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<SonyAggregationFlags>10</SonyAggregationFlags> <SonyAggregationFlags>10</SonyAggregationFlags>
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -29,7 +29,7 @@
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<SonyAggregationFlags>10</SonyAggregationFlags> <SonyAggregationFlags>10</SonyAggregationFlags>
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -29,7 +29,7 @@
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<SonyAggregationFlags>10</SonyAggregationFlags> <SonyAggregationFlags>10</SonyAggregationFlags>
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -29,7 +29,7 @@
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<SonyAggregationFlags>10</SonyAggregationFlags> <SonyAggregationFlags>10</SonyAggregationFlags>
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -29,7 +29,7 @@
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<SonyAggregationFlags>10</SonyAggregationFlags> <SonyAggregationFlags>10</SonyAggregationFlags>
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -28,7 +28,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>5</TimelineOffsetSeconds> <TimelineOffsetSeconds>5</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -28,7 +28,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>40</TimelineOffsetSeconds> <TimelineOffsetSeconds>40</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

@ -27,7 +27,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

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

@ -18,6 +18,7 @@ namespace Emby.Dlna.Service
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
protected IServerConfigurationManager Config { get; } protected IServerConfigurationManager Config { get; }
protected ILogger Logger { get; } protected ILogger Logger { get; }
protected BaseControlHandler(IServerConfigurationManager config, ILogger logger) protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
@ -135,6 +136,7 @@ namespace Emby.Dlna.Service
break; break;
} }
default: default:
{ {
await reader.SkipAsync().ConfigureAwait(false); await reader.SkipAsync().ConfigureAwait(false);
@ -211,7 +213,9 @@ namespace Emby.Dlna.Service
private class ControlRequestInfo private class ControlRequestInfo
{ {
public string LocalName { get; set; } public string LocalName { get; set; }
public string NamespaceURI { get; set; } public string NamespaceURI { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
} }

@ -17,7 +17,7 @@ namespace Emby.Dlna.Service
Logger = logger; Logger = logger;
HttpClient = httpClient; HttpClient = httpClient;
EventManager = new EventManager(Logger, HttpClient); EventManager = new EventManager(logger, HttpClient);
} }
public EventSubscriptionResponse CancelEventSubscription(string subscriptionId) public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)

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

@ -77,7 +77,7 @@ namespace Emby.Dlna.Ssdp
// (Optional) Set the filter so we only see notifications for devices we care about // (Optional) Set the filter so we only see notifications for devices we care about
// (can be any search target value i.e device type, uuid value etc - any value that appears in the // (can be any search target value i.e device type, uuid value etc - any value that appears in the
// DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method). // DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method).
//_DeviceLocator.NotificationFilter = "upnp:rootdevice"; // _DeviceLocator.NotificationFilter = "upnp:rootdevice";
// Connect our event handler so we process devices as they are found // Connect our event handler so we process devices as they are found
_deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable; _deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable;
@ -100,15 +100,13 @@ namespace Emby.Dlna.Ssdp
var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
var args = new GenericEventArgs<UpnpDeviceInfo> var args = new GenericEventArgs<UpnpDeviceInfo>(
{ new UpnpDeviceInfo
Argument = new UpnpDeviceInfo
{ {
Location = e.DiscoveredDevice.DescriptionLocation, Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers, Headers = headers,
LocalIpAddress = e.LocalIpAddress LocalIpAddress = e.LocalIpAddress
} });
};
DeviceDiscoveredInternal?.Invoke(this, args); DeviceDiscoveredInternal?.Invoke(this, args);
} }
@ -121,14 +119,12 @@ namespace Emby.Dlna.Ssdp
var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
var args = new GenericEventArgs<UpnpDeviceInfo> var args = new GenericEventArgs<UpnpDeviceInfo>(
{ new UpnpDeviceInfo
Argument = new UpnpDeviceInfo
{ {
Location = e.DiscoveredDevice.DescriptionLocation, Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers Headers = headers
} });
};
DeviceLeft?.Invoke(this, args); DeviceLeft?.Invoke(this, args);
} }

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
namespace Emby.Dlna.Ssdp namespace Emby.Dlna.Ssdp
@ -10,24 +11,17 @@ namespace Emby.Dlna.Ssdp
{ {
var node = container.Element(name); var node = container.Element(name);
return node == null ? null : node.Value; return node?.Value;
} }
public static string GetAttributeValue(this XElement container, XName name) public static string GetAttributeValue(this XElement container, XName name)
{ {
var node = container.Attribute(name); var node = container.Attribute(name);
return node == null ? null : node.Value; return node?.Value;
} }
public static string GetDescendantValue(this XElement container, XName name) public static string GetDescendantValue(this XElement container, XName name)
{ => container.Descendants(name).FirstOrDefault()?.Value;
foreach (var node in container.Descendants(name))
{
return node.Value;
}
return null;
}
} }
} }

@ -1,10 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<ProjectGuid>{08FFF49B-F175-4807-A2B5-73B0EBD9F716}</ProjectGuid>
</PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

@ -4,17 +4,18 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Photo = MediaBrowser.Controller.Entities.Photo;
namespace Emby.Drawing namespace Emby.Drawing
{ {
@ -29,12 +30,11 @@ namespace Emby.Drawing
private static readonly HashSet<string> _transparentImageTypes private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
private readonly ILogger _logger; private readonly ILogger<ImageProcessor> _logger;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths; private readonly IServerApplicationPaths _appPaths;
private readonly IImageEncoder _imageEncoder; private readonly IImageEncoder _imageEncoder;
private readonly Func<ILibraryManager> _libraryManager; private readonly IMediaEncoder _mediaEncoder;
private readonly Func<IMediaEncoder> _mediaEncoder;
private bool _disposed = false; private bool _disposed = false;
@ -45,20 +45,17 @@ namespace Emby.Drawing
/// <param name="appPaths">The server application paths.</param> /// <param name="appPaths">The server application paths.</param>
/// <param name="fileSystem">The filesystem.</param> /// <param name="fileSystem">The filesystem.</param>
/// <param name="imageEncoder">The image encoder.</param> /// <param name="imageEncoder">The image encoder.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="mediaEncoder">The media encoder.</param> /// <param name="mediaEncoder">The media encoder.</param>
public ImageProcessor( public ImageProcessor(
ILogger<ImageProcessor> logger, ILogger<ImageProcessor> logger,
IServerApplicationPaths appPaths, IServerApplicationPaths appPaths,
IFileSystem fileSystem, IFileSystem fileSystem,
IImageEncoder imageEncoder, IImageEncoder imageEncoder,
Func<ILibraryManager> libraryManager, IMediaEncoder mediaEncoder)
Func<IMediaEncoder> mediaEncoder)
{ {
_logger = logger; _logger = logger;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_imageEncoder = imageEncoder; _imageEncoder = imageEncoder;
_libraryManager = libraryManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_appPaths = appPaths; _appPaths = appPaths;
} }
@ -119,28 +116,11 @@ namespace Emby.Drawing
=> _transparentImageTypes.Contains(Path.GetExtension(path)); => _transparentImageTypes.Contains(Path.GetExtension(path));
/// <inheritdoc /> /// <inheritdoc />
public async Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options) public async Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
{ {
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
var libraryManager = _libraryManager();
ItemImageInfo originalImage = options.Image; ItemImageInfo originalImage = options.Image;
BaseItem item = options.Item; BaseItem item = options.Item;
if (!originalImage.IsLocalFile)
{
if (item == null)
{
item = libraryManager.GetItemById(options.ItemId);
}
originalImage = await libraryManager.ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false);
}
string originalImagePath = originalImage.Path; string originalImagePath = originalImage.Path;
DateTime dateModified = originalImage.DateModified; DateTime dateModified = originalImage.DateModified;
ImageDimensions? originalImageSize = null; ImageDimensions? originalImageSize = null;
@ -252,7 +232,7 @@ namespace Emby.Drawing
return ImageFormat.Jpg; return ImageFormat.Jpg;
} }
private string GetMimeType(ImageFormat format, string path) private string? GetMimeType(ImageFormat format, string path)
=> format switch => format switch
{ {
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"), ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
@ -312,10 +292,6 @@ namespace Emby.Drawing
/// <inheritdoc /> /// <inheritdoc />
public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info) public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info)
=> GetImageDimensions(item, info, true);
/// <inheritdoc />
public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem)
{ {
int width = info.Width; int width = info.Width;
int height = info.Height; int height = info.Height;
@ -326,17 +302,12 @@ namespace Emby.Drawing
} }
string path = info.Path; string path = info.Path;
_logger.LogInformation("Getting image size for item {ItemType} {Path}", item.GetType().Name, path); _logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
ImageDimensions size = GetImageDimensions(path); ImageDimensions size = GetImageDimensions(path);
info.Width = size.Width; info.Width = size.Width;
info.Height = size.Height; info.Height = size.Height;
if (updateItem)
{
_libraryManager().UpdateImages(item);
}
return size; return size;
} }
@ -344,6 +315,27 @@ namespace Emby.Drawing
public ImageDimensions GetImageDimensions(string path) public ImageDimensions GetImageDimensions(string path)
=> _imageEncoder.GetImageSize(path); => _imageEncoder.GetImageSize(path);
/// <inheritdoc />
public string GetImageBlurHash(string path)
{
var size = GetImageDimensions(path);
if (size.Width <= 0 || size.Height <= 0)
{
return string.Empty;
}
// We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
// One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
// See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
float xCompF = MathF.Sqrt(16.0f * size.Width / size.Height);
float yCompF = xCompF * size.Height / size.Width;
int xComp = Math.Min((int)xCompF + 1, 9);
int yComp = Math.Min((int)yCompF + 1, 9);
return _imageEncoder.GetImageBlurHash(xComp, yComp, path);
}
/// <inheritdoc /> /// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ItemImageInfo image) public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
=> (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
@ -351,19 +343,19 @@ namespace Emby.Drawing
/// <inheritdoc /> /// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
{ {
try return GetImageCacheTag(item, new ItemImageInfo
{ {
return GetImageCacheTag(item, new ItemImageInfo Path = chapter.ImagePath,
{ Type = ImageType.Chapter,
Path = chapter.ImagePath, DateModified = chapter.ImageDateModified
Type = ImageType.Chapter, });
DateModified = chapter.ImageDateModified }
});
} /// <inheritdoc />
catch public string GetImageCacheTag(User user)
{ {
return null; return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
} .ToString("N", CultureInfo.InvariantCulture);
} }
private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
@ -384,13 +376,13 @@ namespace Emby.Drawing
{ {
string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
string cacheExtension = _mediaEncoder().SupportsEncoder("libwebp") ? ".webp" : ".png"; string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
var file = _fileSystem.GetFileInfo(outputPath); var file = _fileSystem.GetFileInfo(outputPath);
if (!file.Exists) if (!file.Exists)
{ {
await _mediaEncoder().ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath); dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
} }
else else

@ -42,5 +42,11 @@ namespace Emby.Drawing
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <inheritdoc />
public string GetImageBlurHash(int xComp, int yComp, string path)
{
throw new NotImplementedException();
}
} }
} }

@ -1,9 +1,9 @@
#nullable enable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Emby.Naming.Common; using Emby.Naming.Common;
@ -21,8 +21,7 @@ namespace Emby.Naming.Audio
public bool IsMultiPart(string path) public bool IsMultiPart(string path)
{ {
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);
if (filename.Length == 0)
if (string.IsNullOrEmpty(filename))
{ {
return false; return false;
} }
@ -39,18 +38,22 @@ namespace Emby.Naming.Audio
filename = filename.Replace(')', ' '); filename = filename.Replace(')', ' ');
filename = Regex.Replace(filename, @"\s+", " "); filename = Regex.Replace(filename, @"\s+", " ");
filename = filename.TrimStart(); ReadOnlySpan<char> trimmedFilename = filename.TrimStart();
foreach (var prefix in _options.AlbumStackingPrefixes) foreach (var prefix in _options.AlbumStackingPrefixes)
{ {
if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) != 0) if (!trimmedFilename.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{ {
continue; continue;
} }
var tmp = filename.Substring(prefix.Length); var tmp = trimmedFilename.Slice(prefix.Length).Trim();
tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty; int index = tmp.IndexOf(' ');
if (index != -1)
{
tmp = tmp.Slice(0, index);
}
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{ {

@ -1,3 +1,4 @@
#nullable enable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -11,7 +12,7 @@ namespace Emby.Naming.Audio
{ {
public static bool IsAudioFile(string path, NamingOptions options) public static bool IsAudioFile(string path, NamingOptions options)
{ {
var extension = Path.GetExtension(path) ?? string.Empty; var extension = Path.GetExtension(path);
return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
} }
} }

@ -64,6 +64,7 @@ namespace Emby.Naming.AudioBook
{ {
result.ChapterNumber = int.Parse(matches[0].Groups[0].Value); result.ChapterNumber = int.Parse(matches[0].Groups[0].Value);
} }
if (matches.Count > 1) if (matches.Count > 1)
{ {
result.PartNumber = int.Parse(matches[matches.Count - 1].Groups[0].Value); result.PartNumber = int.Parse(matches[matches.Count - 1].Groups[0].Value);

@ -23,11 +23,6 @@ namespace Emby.Naming.Common
{ {
} }
public EpisodeExpression()
: this(null)
{
}
public string Expression public string Expression
{ {
get => _expression; get => _expression;
@ -48,6 +43,6 @@ namespace Emby.Naming.Common
public string[] DateTimeFormats { get; set; } public string[] DateTimeFormats { get; set; }
public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled)); public Regex Regex => _regex ??= new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
} }
} }

@ -5,17 +5,17 @@ namespace Emby.Naming.Common
public enum MediaType public enum MediaType
{ {
/// <summary> /// <summary>
/// The audio /// The audio.
/// </summary> /// </summary>
Audio = 0, Audio = 0,
/// <summary> /// <summary>
/// The photo /// The photo.
/// </summary> /// </summary>
Photo = 1, Photo = 1,
/// <summary> /// <summary>
/// The video /// The video.
/// </summary> /// </summary>
Video = 2 Video = 2
} }

@ -136,12 +136,13 @@ namespace Emby.Naming.Common
CleanDateTimes = new[] CleanDateTimes = new[]
{ {
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*" @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
}; };
CleanStrings = new[] CleanStrings = new[]
{ {
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"(\[.*\])" @"(\[.*\])"
}; };
@ -276,7 +277,7 @@ namespace Emby.Naming.Common
// This isn't a Kodi naming rule, but the expression below causes false positives, // This isn't a Kodi naming rule, but the expression below causes false positives,
// so we make sure this one gets tested first. // so we make sure this one gets tested first.
// "Foo Bar 889" // "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$") new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
{ {
IsNamed = true IsNamed = true
}, },
@ -299,32 +300,32 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming // *** End Kodi Standard Naming
// [bar] Foo - 1 [baz] // [bar] Foo - 1 [baz]
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$") new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d+))[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
// "01.avi" // "01.avi"
new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$") new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
@ -334,34 +335,34 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"([0-9]+)-([0-9]+)"), new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi" // "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "01.blah.avi" // "01.blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.[^\\\/]+$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\.[^\\\/]+$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah" // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "01 episode title.avi" // "01 episode title.avi"
new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>\d{1,3})([^\\\/]*)$") new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "Episode 16", "Episode 16 - Title" // "Episode 16", "Episode 16 - Title"
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
@ -505,7 +506,63 @@ namespace Emby.Naming.Common
RuleType = ExtraRuleType.Suffix, RuleType = ExtraRuleType.Suffix,
Token = "-short", Token = "-short",
MediaType = MediaType.Video MediaType = MediaType.Video
} },
new ExtraRule
{
ExtraType = ExtraType.BehindTheScenes,
RuleType = ExtraRuleType.DirectoryName,
Token = "behind the scenes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.DeletedScene,
RuleType = ExtraRuleType.DirectoryName,
Token = "deleted scenes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Interview,
RuleType = ExtraRuleType.DirectoryName,
Token = "interviews",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Scene,
RuleType = ExtraRuleType.DirectoryName,
Token = "scenes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.DirectoryName,
Token = "samples",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.DirectoryName,
Token = "shorts",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.DirectoryName,
Token = "featurettes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Unknown,
RuleType = ExtraRuleType.DirectoryName,
Token = "extras",
MediaType = MediaType.Video,
},
}; };
Format3DRules = new[] Format3DRules = new[]
@ -568,17 +625,17 @@ namespace Emby.Naming.Common
AudioBookPartsExpressions = new[] AudioBookPartsExpressions = new[]
{ {
// Detect specified chapters, like CH 01 // Detect specified chapters, like CH 01
@"ch(?:apter)?[\s_-]?(?<chapter>\d+)", @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
// Detect specified parts, like Part 02 // Detect specified parts, like Part 02
@"p(?:ar)?t[\s_-]?(?<part>\d+)", @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)",
// Chapter is often beginning of filename // Chapter is often beginning of filename
@"^(?<chapter>\d+)", "^(?<chapter>[0-9]+)",
// Part if often ending of filename // Part if often ending of filename
@"(?<part>\d+)$", "(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part) // Sometimes named as 0001_005 (chapter_part)
@"(?<chapter>\d+)_(?<part>\d+)", "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number. // Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?<chapter>\d+)" @"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
}; };
var extensions = VideoFileExtensions.ToList(); var extensions = VideoFileExtensions.ToList();
@ -618,16 +675,16 @@ namespace Emby.Naming.Common
MultipleEpisodeExpressions = new string[] MultipleEpisodeExpressions = new string[]
{ {
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$" @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$"
}.Select(i => new EpisodeExpression(i) }.Select(i => new EpisodeExpression(i)
{ {
IsNamed = true IsNamed = true

@ -1,5 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<ProjectGuid>{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}</ProjectGuid>
</PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>

@ -1,3 +1,4 @@
#nullable enable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -16,11 +17,11 @@ namespace Emby.Naming.Subtitles
_options = options; _options = options;
} }
public SubtitleInfo ParseFile(string path) public SubtitleInfo? ParseFile(string path)
{ {
if (string.IsNullOrEmpty(path)) if (path.Length == 0)
{ {
throw new ArgumentNullException(nameof(path)); throw new ArgumentException("File path can't be empty.", nameof(path));
} }
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path);
@ -52,11 +53,6 @@ namespace Emby.Naming.Subtitles
private string[] GetFlags(string path) private string[] GetFlags(string path)
{ {
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
}
// Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _. // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
var file = Path.GetFileName(path); var file = Path.GetFileName(path);

@ -80,6 +80,15 @@ namespace Emby.Naming.Video
result.Rule = rule; result.Rule = rule;
} }
} }
else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
return result; return result;
} }

@ -5,30 +5,29 @@ using MediaType = Emby.Naming.Common.MediaType;
namespace Emby.Naming.Video namespace Emby.Naming.Video
{ {
/// <summary>
/// A rule used to match a file path with an <see cref="MediaBrowser.Model.Entities.ExtraType"/>.
/// </summary>
public class ExtraRule public class ExtraRule
{ {
/// <summary> /// <summary>
/// Gets or sets the token. /// Gets or sets the token to use for matching against the file path.
/// </summary> /// </summary>
/// <value>The token.</value>
public string Token { get; set; } public string Token { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the extra. /// Gets or sets the type of the extra to return when matched.
/// </summary> /// </summary>
/// <value>The type of the extra.</value>
public ExtraType ExtraType { get; set; } public ExtraType ExtraType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the rule. /// Gets or sets the type of the rule.
/// </summary> /// </summary>
/// <value>The type of the rule.</value>
public ExtraRuleType RuleType { get; set; } public ExtraRuleType RuleType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the media. /// Gets or sets the type of the media to return when matched.
/// </summary> /// </summary>
/// <value>The type of the media.</value>
public MediaType MediaType { get; set; } public MediaType MediaType { get; set; }
} }
} }

@ -5,18 +5,23 @@ namespace Emby.Naming.Video
public enum ExtraRuleType public enum ExtraRuleType
{ {
/// <summary> /// <summary>
/// The suffix /// Match <see cref="ExtraRule.Token"/> against a suffix in the file name.
/// </summary> /// </summary>
Suffix = 0, Suffix = 0,
/// <summary> /// <summary>
/// The filename /// Match <see cref="ExtraRule.Token"/> against the file name, excluding the file extension.
/// </summary> /// </summary>
Filename = 1, Filename = 1,
/// <summary> /// <summary>
/// The regex /// Match <see cref="ExtraRule.Token"/> against the file name, including the file extension.
/// </summary> /// </summary>
Regex = 2 Regex = 2,
/// <summary>
/// Match <see cref="ExtraRule.Token"/> against the name of the directory containing the file.
/// </summary>
DirectoryName = 3,
} }
} }

@ -227,7 +227,7 @@ namespace Emby.Naming.Video
} }
return remainingFiles return remainingFiles
.Where(i => i.ExtraType == null) .Where(i => i.ExtraType != null)
.Where(i => baseNames.Any(b => .Where(i => baseNames.Any(b =>
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase))) i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
.ToList(); .ToList();

@ -89,14 +89,14 @@ namespace Emby.Naming.Video
if (parseName) if (parseName)
{ {
var cleanDateTimeResult = CleanDateTime(name); var cleanDateTimeResult = CleanDateTime(name);
name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null if (extraResult.ExtraType == null
&& TryCleanString(cleanDateTimeResult.Name, out ReadOnlySpan<char> newName)) && TryCleanString(name, out ReadOnlySpan<char> newName))
{ {
name = newName.ToString(); name = newName.ToString();
} }
year = cleanDateTimeResult.Year;
} }
return new VideoFileInfo return new VideoFileInfo

@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Notifications; using MediaBrowser.Controller.Notifications;
@ -149,9 +150,7 @@ namespace Emby.Notifications.Api
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
public object Get(GetNotificationsSummary request) public object Get(GetNotificationsSummary request)
{ {
return new NotificationsSummary return new NotificationsSummary();
{
};
} }
public Task Post(AddAdminNotification request) public Task Post(AddAdminNotification request)
@ -164,7 +163,10 @@ namespace Emby.Notifications.Api
Level = request.Level, Level = request.Level,
Name = request.Name, Name = request.Name,
Url = request.Url, Url = request.Url,
UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray() UserIds = _userManager.Users
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
.Select(user => user.Id)
.ToArray()
}; };
return _notificationManager.SendNotification(notification, CancellationToken.None); return _notificationManager.SendNotification(notification, CancellationToken.None);

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

Loading…
Cancel
Save