Compare commits

..

1 Commits

Author SHA1 Message Date
Vasily
52c399d4d1 Add back taglib-sharp submodule
This fixes v10.1.0 build
2019-01-25 23:46:25 +03:00
1147 changed files with 56875 additions and 26872 deletions

View File

@@ -1,371 +0,0 @@
name: $(Date:yyyyMMdd)$(Rev:.r)
variables:
- name: TestProjects
value: 'tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj'
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
pr:
autoCancel: true
trigger:
batch: true
jobs:
- job: main_build
displayName: Main Build
pool:
vmImage: ubuntu-latest
strategy:
matrix:
release:
BuildConfiguration: Release
debug:
BuildConfiguration: Debug
maxParallel: 2
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: true
- task: CmdLine@2
displayName: "Check out web"
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
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: "Check out web (PR)"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
inputs:
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.js'
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:
versionSpec: '10.x'
- task: CmdLine@2
displayName: "Build Web UI"
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 the web UI
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 # Optional
contents: '**'
targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: false # Optional
- task: DotNetCoreCLI@2
displayName: Publish
inputs:
command: publish
publishWebProjects: false
projects: '$(RestoreBuildProjects)'
arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'
zipAfterPublish: false
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Naming'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
artifactName: 'Jellyfin.Naming'
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Controller'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
artifactName: 'Jellyfin.Controller'
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Model'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
artifactName: 'Jellyfin.Model'
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Common'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
- job: main_test
displayName: Main Test
pool:
vmImage: windows-latest
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: false
- task: DotNetCoreCLI@2
displayName: Build
inputs:
command: build
publishWebProjects: false
projects: '$(TestProjects)'
arguments: '--configuration $(BuildConfiguration)'
zipAfterPublish: false
- task: VisualStudioTestPlatformInstaller@1
inputs:
packageFeedSelector: 'nugetOrg' # Options: nugetOrg, customFeed, netShare
versionSelector: 'latestPreRelease' # Required when packageFeedSelector == NugetOrg || PackageFeedSelector == CustomFeed# Options: latestPreRelease, latestStable, specificVersion
- task: VSTest@2
inputs:
testSelector: 'testAssemblies' # Options: testAssemblies, testPlan, testRun
testAssemblyVer2: | # Required when testSelector == TestAssemblies
**\bin\$(BuildConfiguration)\**\*test*.dll
!**\obj\**
!**\xunit.runner.visualstudio.testadapter.dll
!**\xunit.runner.visualstudio.dotnetcore.testadapter.dll
#testPlan: # Required when testSelector == TestPlan
#testSuite: # Required when testSelector == TestPlan
#testConfiguration: # Required when testSelector == TestPlan
#tcmTestRun: '$(test.RunId)' # Optional
searchFolder: '$(System.DefaultWorkingDirectory)'
#testFiltercriteria: # Optional
#runOnlyImpactedTests: False # Optional
#runAllTestsAfterXBuilds: '50' # Optional
#uiTests: false # Optional
#vstestLocationMethod: 'version' # Optional. Options: version, location
#vsTestVersion: 'latest' # Optional. Options: latest, 16.0, 15.0, 14.0, toolsInstaller
#vstestLocation: # Optional
#runSettingsFile: # Optional
#overrideTestrunParameters: # Optional
#pathtoCustomTestAdapters: # Optional
runInParallel: True # Optional
runTestsInIsolation: True # Optional
codeCoverageEnabled: True # Optional
#otherConsoleOptions: # Optional
#distributionBatchType: 'basedOnTestCases' # Optional. Options: basedOnTestCases, basedOnExecutionTime, basedOnAssembly
#batchingBasedOnAgentsOption: 'autoBatchSize' # Optional. Options: autoBatchSize, customBatchSize
#customBatchSizeValue: '10' # Required when distributionBatchType == BasedOnTestCases && BatchingBasedOnAgentsOption == CustomBatchSize
#batchingBasedOnExecutionTimeOption: 'autoBatchSize' # Optional. Options: autoBatchSize, customTimeBatchSize
#customRunTimePerBatchValue: '60' # Required when distributionBatchType == BasedOnExecutionTime && BatchingBasedOnExecutionTimeOption == CustomTimeBatchSize
#dontDistribute: False # Optional
#testRunTitle: # Optional
#platform: # Optional
configuration: 'Debug' # Optional
publishRunAttachments: true # Optional
#diagnosticsEnabled: false # Optional
#collectDumpOn: 'onAbortOnly' # Optional. Options: onAbortOnly, always, never
#rerunFailedTests: False # Optional
#rerunType: 'basedOnTestFailurePercentage' # Optional. Options: basedOnTestFailurePercentage, basedOnTestFailureCount
#rerunFailedThreshold: '30' # Optional
#rerunFailedTestCasesMaxLimit: '5' # Optional
#rerunMaxAttempts: '3' # Optional
# - task: PublishTestResults@2
# inputs:
# testResultsFormat: 'VSTest' # Options: JUnit, NUnit, VSTest, xUnit, cTest
# testResultsFiles: '**/*.trx'
# #searchFolder: '$(System.DefaultWorkingDirectory)' # Optional
# mergeTestResults: true # Optional
# #failTaskOnFailedTests: false # Optional
# #testRunTitle: # Optional
# #buildPlatform: # Optional
# #buildConfiguration: # Optional
# #publishRunAttachments: true # Optional
- job: main_build_win
displayName: Main Build Windows
pool:
vmImage: windows-latest
strategy:
matrix:
release:
BuildConfiguration: Release
maxParallel: 2
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: true
- task: CmdLine@2
displayName: "Check out web"
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
inputs:
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: "Check out web (PR)"
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
inputs:
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.js'
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:
versionSpec: '10.x'
- task: CmdLine@2
displayName: "Build Web UI"
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 the web UI
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 # Optional
contents: '**'
targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: false # Optional
- task: CmdLine@2
displayName: Clone the UX repository
inputs:
script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
- task: PowerShell@2
displayName: Build the NSIS Installer
inputs:
targetType: 'filePath' # Optional. Options: filePath, inline
filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath
arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
#script: '# Write your PowerShell commands here.Write-Host Hello World' # Required when targetType == Inline
errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue
#failOnStderr: false # Optional
#ignoreLASTEXITCODE: false # Optional
#pwsh: false # Optional
workingDirectory: $(Build.SourcesDirectory) # Optional
- task: CopyFiles@2
displayName: Copy the NSIS Installer to the artifact directory
inputs:
sourceFolder: $(Build.SourcesDirectory)/deployment/windows/ # Optional
contents: 'jellyfin*.exe'
targetFolder: $(System.ArtifactsDirectory)/setup
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: PublishPipelineArtifact@0
displayName: 'Publish Setup Artifact'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
targetPath: '$(build.artifactstagingdirectory)/setup'
artifactName: 'Jellyfin Server Setup'
- job: dotnet_compat
displayName: Compatibility Check
pool:
vmImage: ubuntu-latest
dependsOn: main_build
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds)
strategy:
matrix:
Naming:
NugetPackageName: Jellyfin.Naming
AssemblyFileName: Emby.Naming.dll
Controller:
NugetPackageName: Jellyfin.Controller
AssemblyFileName: MediaBrowser.Controller.dll
Model:
NugetPackageName: Jellyfin.Model
AssemblyFileName: MediaBrowser.Model.dll
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
maxParallel: 2
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download the New Assembly Build Artifact
inputs:
source: 'current' # Options: current, specific
#preferTriggeringPipeline: false # Optional
#tags: # Optional
artifact: '$(NugetPackageName)' # Optional
#patterns: '**' # Optional
path: '$(System.ArtifactsDirectory)/new-artifacts'
#project: # Required when source == Specific
#pipeline: # Required when source == Specific
runVersion: 'latest' # Required when source == Specific. Options: latest, latestFromBranch, specific
#runBranch: 'refs/heads/master' # Required when source == Specific && runVersion == LatestFromBranch
#runId: # Required when source == Specific && runVersion == Specific
- task: CopyFiles@2
displayName: Copy New Assembly to new-release folder
inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/new-release
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadPipelineArtifact@2
displayName: Download the Reference Assembly Build Artifact
inputs:
source: 'specific' # Options: current, specific
#preferTriggeringPipeline: false # Optional
#tags: # Optional
artifact: '$(NugetPackageName)' # Optional
#patterns: '**' # Optional
path: '$(System.ArtifactsDirectory)/current-artifacts'
project: '$(System.TeamProjectId)' # Required when source == Specific
pipeline: '$(System.DefinitionId)' # Required when source == Specific
runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
runBranch: 'refs/heads/$(System.PullRequest.TargetBranch)' # Required when source == Specific && runVersion == LatestFromBranch
#runId: # Required when source == Specific && runVersion == Specific
- task: CopyFiles@2
displayName: Copy Reference Assembly to current-release folder
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadGitHubRelease@0
displayName: Download ABI compatibility check tool from GitHub
inputs:
connection: Jellyfin Release Download
userRepository: EraYaN/dotnet-compatibility
defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
#version: # Required when defaultVersionType != Latest
itemPattern: '**-ci.zip' # Optional
downloadPath: '$(System.ArtifactsDirectory)'
- task: ExtractFiles@1
displayName: Extract ABI compatibility check tool
inputs:
archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip'
destinationFolder: $(System.ArtifactsDirectory)/tools
cleanDestinationFolder: true
- task: CmdLine@2
displayName: Execute ABI compatibility check tool
inputs:
script: 'dotnet tools/CompatibilityCheckerCoreCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines'
workingDirectory: $(System.ArtifactsDirectory) # Optional
#failOnStderr: false # Optional

View File

@@ -1,46 +0,0 @@
name: Nightly-$(date:yyyyMMdd).$(rev:r)
variables:
- name: Version
value: '1.0.0'
trigger: none
pr: none
jobs:
- job: publish_artifacts_nightly
displayName: Publish Artifacts Nightly
pool:
vmImage: ubuntu-latest
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download the Windows Setup Artifact
inputs:
source: 'specific' # Options: current, specific
artifact: 'Jellyfin Server Setup' # Optional
path: '$(System.ArtifactsDirectory)/win-installer'
project: '$(System.TeamProjectId)' # Required when source == Specific
pipelineId: 1 # Required when source == Specific
runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
runBranch: 'refs/heads/master' # Required when source == Specific && runVersion == LatestFromBranch
- task: SSH@0
displayName: 'Create Drop directory'
inputs:
sshEndpoint: 'Jellyfin Build Server'
commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_nightly_azure_upload'
- task: CopyFilesOverSSH@0
displayName: 'Copy the Windows Setup to the Repo'
inputs:
sshEndpoint: 'Jellyfin Build Server'
sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
contents: 'jellyfin_*.exe'
targetFolder: '/srv/incoming/jellyfin_nightly_azure_upload/win-installer'
- task: SSH@0
displayName: 'Clean up SCP symlink'
inputs:
sshEndpoint: 'Jellyfin Build Server'
commands: 'rm -f /srv/incoming/jellyfin_nightly_azure_upload'

View File

@@ -1,48 +0,0 @@
name: Release-$(Version)-$(date:yyyyMMdd).$(rev:r)
variables:
- name: Version
value: '1.0.0'
- name: UsedRunId
value: 0
trigger: none
pr: none
jobs:
- job: publish_artifacts_release
displayName: Publish Artifacts Release
pool:
vmImage: ubuntu-latest
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download the Windows Setup Artifact
inputs:
source: 'specific' # Options: current, specific
artifact: 'Jellyfin Server Setup' # Optional
path: '$(System.ArtifactsDirectory)/win-installer'
project: '$(System.TeamProjectId)' # Required when source == Specific
pipelineId: 1 # Required when source == Specific
runVersion: 'specific' # Required when source == Specific. Options: latest, latestFromBranch, specific
runId: $(UsedRunId)
- task: SSH@0
displayName: 'Create Drop directory'
inputs:
sshEndpoint: 'Jellyfin Build Server'
commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_release_azure_upload'
- task: CopyFilesOverSSH@0
displayName: 'Copy the Windows Setup to the Repo'
inputs:
sshEndpoint: 'Jellyfin Build Server'
sourceFolder: '$(System.ArtifactsDirectory)/win-installer'
contents: 'jellyfin_*.exe'
targetFolder: '/srv/incoming/jellyfin_release_azure_upload/win-installer'
- task: SSH@0
displayName: 'Clean up SCP symlink'
inputs:
sshEndpoint: 'Jellyfin Build Server'
commands: 'rm -f /srv/incoming/jellyfin_release_azure_upload'

View File

@@ -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)"

View File

@@ -8,4 +8,3 @@ README.md
deployment/*/dist deployment/*/dist
deployment/*/pkg-dist deployment/*/pkg-dist
deployment/collect-dist/ deployment/collect-dist/
ci/

View File

@@ -1,30 +1,12 @@
---
kind: pipeline kind: pipeline
name: build-debug name: build
steps: steps:
- name: submodules - name: submodules
image: docker:git image: docker:git
commands: commands:
- git submodule update --init --recursive - git submodule update --init --recursive
- name: build - name: build
image: microsoft/dotnet:2-sdk image: microsoft/dotnet:2-sdk
commands: commands:
- dotnet publish "Jellyfin.Server" --configuration Debug --output "../ci/ci-debug" - dotnet publish --configuration release --output /release Jellyfin.Server
---
kind: pipeline
name: build-release
steps:
- name: submodules
image: docker:git
commands:
- git submodule update --init --recursive
- name: build
image: microsoft/dotnet:2-sdk
commands:
- dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release"

View File

@@ -15,10 +15,6 @@ insert_final_newline = true
end_of_line = lf end_of_line = lf
max_line_length = null max_line_length = null
# YAML indentation
[*.{yml,yaml}]
indent_size = 2
# XML indentation # XML indentation
[*.{csproj,xml}] [*.{csproj,xml}]
indent_size = 2 indent_size = 2
@@ -59,77 +55,15 @@ dotnet_style_prefer_conditional_expression_over_return = true:silent
############################### ###############################
# Naming Conventions # # Naming Conventions #
############################### ###############################
# Style Definitions (From Roslyn) # Style Definitions
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Non-private static fields are PascalCase # Use PascalCase for constant fields
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
# Constants are PascalCase
dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
dotnet_naming_symbols.constants.applicable_kinds = field, local
dotnet_naming_symbols.constants.required_modifiers = const
dotnet_naming_style.constant_style.capitalization = pascal_case
# Static fields are camelCase and start with s_
dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
dotnet_naming_symbols.static_fields.applicable_kinds = field
dotnet_naming_symbols.static_fields.required_modifiers = static
dotnet_naming_style.static_field_style.capitalization = camel_case
dotnet_naming_style.static_field_style.required_prefix = _
# Instance fields are camelCase and start with _
dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
dotnet_naming_symbols.instance_fields.applicable_kinds = field
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
# Locals and parameters are camelCase
dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
dotnet_naming_style.camel_case_style.capitalization = camel_case
# Local functions are PascalCase
dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_style.local_function_style.capitalization = pascal_case
# By default, name items with PascalCase
dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.all_members.applicable_kinds = *
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
############################### ###############################
# C# Coding Conventions # # C# Coding Conventions #
############################### ###############################

4
.gitattributes vendored
View File

@@ -1,5 +1 @@
* text=auto eol=lf
*.png binary
*.jpg binary
CONTRIBUTORS.md merge=union CONTRIBUTORS.md merge=union

View File

@@ -8,29 +8,28 @@ assignees: ''
--- ---
**Describe the bug** **Describe the bug**
<!-- A clear and concise description of what the bug is. --> A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
<!-- Steps to reproduce the behavior: --> Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See error
**Expected behavior** **Expected behavior**
<!-- A clear and concise description of what you expected to happen. --> A clear and concise description of what you expected to happen.
**Logs** **Logs**
<!-- Please paste any log errors. --> Please paste any log errors.
**Screenshots** **Screenshots**
<!-- If applicable, add screenshots to help explain your problem. --> If applicable, add screenshots to help explain your problem.
**System (please complete the following information):** **System (please complete the following information):**
- OS: [e.g. Docker, Debian, Windows] - OS: [e.g. Docker, Debian, Windows]
- Browser: [e.g. Firefox, Chrome, Safari] - Browser: [e.g. Firefox, Chrome, Safari]
- Jellyfin Version: [e.g. 10.0.1] - Jellyfin Version: [e.g. 10.0.1]
- Reverse proxy: [e.g. no, nginx, apache, etc.]
**Additional context** **Additional context**
<!-- Add any other context about the problem here. --> Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Enhancement request
about: Suggest an modification to an existing feature
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest a new feature
title: ''
labels: feature
assignees: ''
---
**Describe the feature you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,32 +0,0 @@
---
name: Media playback issue
about: Create a media playback issue report
title: ''
labels: mediaplayback
assignees: ''
---
**Media Info of the file**
<!-- Use the Media Info tool (set to text format, download here: https://mediaarea.net/en/MediaInfo) or copy the info from the web ui for the file with the playback issue. -->
**Logs**
<!-- Please paste any log message from during the playback issue, for example the ffmpeg command line can be very useful. -->
**Stats for Nerds Screenshots**
<!-- If available, add screenshots of the stats for nerds screen to help show the issue problem. -->
**Server System (please complete the following information):**
- OS: [e.g. Docker on Linux, Docker on Windows, Debian, Windows]
- Jellyfin Version: [e.g. 10.0.1]
- Hardware settings & device: [e.g. NVENC on GTX1060, VAAPI on Intel i7 8700K]
- Reverse proxy: [e.g. no, nginx, apache, etc.]
- Other hardware notes: [e.g. Media mounted in CIFS/SMB share, Media mounted from Google Drive]
**Client System (please complete the following information):**
- Device: [e.g. Apple iPhone XS, Xbox One S, LG OLED55C8, Samsung Galaxy Note9, Custom HTPC]
- OS: [e.g. iOS, Android, Windows, macOS]
- Client: [e.g. Web/Browser, webOS, Android, Android TV, Electron]
- Browser (if Web client): [e.g. Firefox, Chrome, Safari]
- Client and Browser Version: [e.g. 10.3.4 and 68.0]

View File

@@ -1,11 +1,9 @@
<!--
Ensure your title is short, descriptive, and in the imperative mood (Fix X, Change Y, instead of Fixed X, Changed Y). Ensure your title is short, descriptive, and in the imperative mood (Fix X, Change Y, instead of Fixed X, Changed Y).
For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our documentation. For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://jellyfin.readthedocs.io/en/latest/developer-docs/contributing/ page.
-->
**Changes** **Changes**
<!-- Describe your changes here in 1-5 sentences. --> Describe your changes here in 1-5 sentences.
**Issues** **Issues**
<!-- Tag any issues that this PR solves here. Tag any issues that this PR solves here.
ex. Fixes # --> Fixes #

22
.github/stale.yml vendored
View File

@@ -1,22 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 90
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 14
# Issues with these labels will never be considered stale
exemptLabels:
- regression
- security
- dotnet-3.0-future
- roadmap
- future
- feature
- enhancement
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
Issues go stale after 90d of inactivity. Mark the issue as fresh by adding a comment or commit. Stale issues close after an additional 14d of inactivity.
If this issue is safe to close now please do so.
If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

14
.gitignore vendored
View File

@@ -239,6 +239,11 @@ pip-log.txt
########## ##########
.idea/ .idea/
##########
# Visual Studio Code
##########
.vscode/
######################### #########################
# Build artifacts # Build artifacts
######################### #########################
@@ -259,12 +264,3 @@ deployment/**/pkg-dist-tmp/
deployment/collect-dist/ deployment/collect-dist/
jellyfin_version.ini jellyfin_version.ini
ci/
# Doxygen
doc/
# Deployment artifacts
dist
*.exe

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "ThirdParty/taglib-sharp"]
path = ThirdParty/taglib-sharp
url = https://github.com/mono/taglib-sharp
[submodule "MediaBrowser.WebDashboard/jellyfin-web"]
path = MediaBrowser.WebDashboard/jellyfin-web
url = https://github.com/jellyfin/jellyfin-web.git

View File

@@ -11,7 +11,6 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -92,7 +92,7 @@ namespace BDInfo
} }
DirectoryRoot = DirectoryRoot =
_fileSystem.GetDirectoryInfo(Path.GetDirectoryName(DirectoryBDMV.FullName)); _fileSystem.GetDirectoryInfo(_fileSystem.GetDirectoryName(DirectoryBDMV.FullName));
DirectoryBDJO = DirectoryBDJO =
GetDirectory("BDJO", DirectoryBDMV, 0); GetDirectory("BDJO", DirectoryBDMV, 0);
DirectoryCLIPINF = DirectoryCLIPINF =
@@ -150,7 +150,7 @@ namespace BDInfo
Is3D = true; Is3D = true;
} }
if (File.Exists(Path.Combine(DirectoryRoot.FullName, "FilmIndex.xml"))) if (_fileSystem.FileExists(Path.Combine(DirectoryRoot.FullName, "FilmIndex.xml")))
{ {
IsDBOX = true; IsDBOX = true;
} }
@@ -165,7 +165,7 @@ namespace BDInfo
foreach (var file in files) foreach (var file in files)
{ {
PlaylistFiles.Add( PlaylistFiles.Add(
file.Name.ToUpper(), new TSPlaylistFile(this, file)); file.Name.ToUpper(), new TSPlaylistFile(this, file, _fileSystem));
} }
} }
@@ -185,7 +185,7 @@ namespace BDInfo
foreach (var file in files) foreach (var file in files)
{ {
StreamClipFiles.Add( StreamClipFiles.Add(
file.Name.ToUpper(), new TSStreamClipFile(file)); file.Name.ToUpper(), new TSStreamClipFile(file, _fileSystem));
} }
} }
@@ -212,6 +212,7 @@ namespace BDInfo
public void Scan() public void Scan()
{ {
var errorStreamClipFiles = new List<TSStreamClipFile>();
foreach (var streamClipFile in StreamClipFiles.Values) foreach (var streamClipFile in StreamClipFiles.Values)
{ {
try try
@@ -220,6 +221,7 @@ namespace BDInfo
} }
catch (Exception ex) catch (Exception ex)
{ {
errorStreamClipFiles.Add(streamClipFile);
if (StreamClipFileScanError != null) if (StreamClipFileScanError != null)
{ {
if (StreamClipFileScanError(streamClipFile, ex)) if (StreamClipFileScanError(streamClipFile, ex))
@@ -248,6 +250,7 @@ namespace BDInfo
StreamFiles.Values.CopyTo(streamFiles, 0); StreamFiles.Values.CopyTo(streamFiles, 0);
Array.Sort(streamFiles, CompareStreamFiles); Array.Sort(streamFiles, CompareStreamFiles);
var errorPlaylistFiles = new List<TSPlaylistFile>();
foreach (var playlistFile in PlaylistFiles.Values) foreach (var playlistFile in PlaylistFiles.Values)
{ {
try try
@@ -256,6 +259,7 @@ namespace BDInfo
} }
catch (Exception ex) catch (Exception ex)
{ {
errorPlaylistFiles.Add(playlistFile);
if (PlaylistFileScanError != null) if (PlaylistFileScanError != null)
{ {
if (PlaylistFileScanError(playlistFile, ex)) if (PlaylistFileScanError(playlistFile, ex))
@@ -271,6 +275,7 @@ namespace BDInfo
} }
} }
var errorStreamFiles = new List<TSStreamFile>();
foreach (var streamFile in streamFiles) foreach (var streamFile in streamFiles)
{ {
try try
@@ -291,6 +296,7 @@ namespace BDInfo
} }
catch (Exception ex) catch (Exception ex)
{ {
errorStreamFiles.Add(streamFile);
if (StreamFileScanError != null) if (StreamFileScanError != null)
{ {
if (StreamFileScanError(streamFile, ex)) if (StreamFileScanError(streamFile, ex))
@@ -339,7 +345,7 @@ namespace BDInfo
{ {
return dir; return dir;
} }
var parentFolder = Path.GetDirectoryName(dir.FullName); var parentFolder = _fileSystem.GetDirectoryName(dir.FullName);
if (string.IsNullOrEmpty(parentFolder)) if (string.IsNullOrEmpty(parentFolder))
{ {
dir = null; dir = null;
@@ -425,7 +431,7 @@ namespace BDInfo
{ {
return 1; return 1;
} }
else if ((x != null && x.FileInfo != null) && (y == null || y.FileInfo == null)) else if ((x != null || x.FileInfo != null) && (y == null || y.FileInfo == null))
{ {
return -1; return -1;
} }
@@ -445,5 +451,6 @@ namespace BDInfo
} }
} }
} }
} }
} }

View File

@@ -9,8 +9,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyCompany("Jellyfin Project")]
[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyProduct("Jellyfin: The Free Software Media System")]
[assembly: AssemblyCopyright("Copyright © 2016 CinemaSquid. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyCopyright("Copyright © 2016 CinemaSquid. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")] [assembly: NeutralResourcesLanguage("en")]

View File

@@ -1,4 +1,4 @@
//============================================================================ //============================================================================
// BDInfo - Blu-ray Video and Audio Analysis Tool // BDInfo - Blu-ray Video and Audio Analysis Tool
// Copyright © 2010 Cinema Squid // Copyright © 2010 Cinema Squid
// //
@@ -28,6 +28,7 @@ namespace BDInfo
{ {
public class TSPlaylistFile public class TSPlaylistFile
{ {
private readonly IFileSystem _fileSystem;
private FileSystemMetadata FileInfo = null; private FileSystemMetadata FileInfo = null;
public string FileType = null; public string FileType = null;
public bool IsInitialized = false; public bool IsInitialized = false;
@@ -63,19 +64,21 @@ namespace BDInfo
new List<TSGraphicsStream>(); new List<TSGraphicsStream>();
public TSPlaylistFile(BDROM bdrom, public TSPlaylistFile(BDROM bdrom,
FileSystemMetadata fileInfo) FileSystemMetadata fileInfo, IFileSystem fileSystem)
{ {
BDROM = bdrom; BDROM = bdrom;
FileInfo = fileInfo; FileInfo = fileInfo;
_fileSystem = fileSystem;
Name = fileInfo.Name.ToUpper(); Name = fileInfo.Name.ToUpper();
} }
public TSPlaylistFile(BDROM bdrom, public TSPlaylistFile(BDROM bdrom,
string name, string name,
List<TSStreamClip> clips) List<TSStreamClip> clips, IFileSystem fileSystem)
{ {
BDROM = bdrom; BDROM = bdrom;
Name = name; Name = name;
_fileSystem = fileSystem;
IsCustom = true; IsCustom = true;
foreach (var clip in clips) foreach (var clip in clips)
{ {
@@ -228,7 +231,7 @@ namespace BDInfo
Streams.Clear(); Streams.Clear();
StreamClips.Clear(); StreamClips.Clear();
fileStream = File.OpenRead(FileInfo.FullName); fileStream = _fileSystem.OpenRead(FileInfo.FullName);
fileReader = new BinaryReader(fileStream); fileReader = new BinaryReader(fileStream);
byte[] data = new byte[fileStream.Length]; byte[] data = new byte[fileStream.Length];

View File

@@ -1,4 +1,4 @@
//============================================================================ //============================================================================
// BDInfo - Blu-ray Video and Audio Analysis Tool // BDInfo - Blu-ray Video and Audio Analysis Tool
// Copyright © 2010 Cinema Squid // Copyright © 2010 Cinema Squid
// //
@@ -28,6 +28,7 @@ namespace BDInfo
{ {
public class TSStreamClipFile public class TSStreamClipFile
{ {
private readonly IFileSystem _fileSystem;
public FileSystemMetadata FileInfo = null; public FileSystemMetadata FileInfo = null;
public string FileType = null; public string FileType = null;
public bool IsValid = false; public bool IsValid = false;
@@ -36,9 +37,10 @@ namespace BDInfo
public Dictionary<ushort, TSStream> Streams = public Dictionary<ushort, TSStream> Streams =
new Dictionary<ushort, TSStream>(); new Dictionary<ushort, TSStream>();
public TSStreamClipFile(FileSystemMetadata fileInfo) public TSStreamClipFile(FileSystemMetadata fileInfo, IFileSystem fileSystem)
{ {
FileInfo = fileInfo; FileInfo = fileInfo;
_fileSystem = fileSystem;
Name = fileInfo.Name.ToUpper(); Name = fileInfo.Name.ToUpper();
} }
@@ -55,7 +57,7 @@ namespace BDInfo
#endif #endif
Streams.Clear(); Streams.Clear();
fileStream = File.OpenRead(FileInfo.FullName); fileStream = _fileSystem.OpenRead(FileInfo.FullName);
fileReader = new BinaryReader(fileStream); fileReader = new BinaryReader(fileStream);
byte[] data = new byte[fileStream.Length]; byte[] data = new byte[fileStream.Length];

View File

@@ -15,21 +15,6 @@
- [cvium](https://github.com/cvium) - [cvium](https://github.com/cvium)
- [wtayl0r](https://github.com/wtayl0r) - [wtayl0r](https://github.com/wtayl0r)
- [TtheCreator](https://github.com/Tthecreator) - [TtheCreator](https://github.com/Tthecreator)
- [dkanada](https://github.com/dkanada)
- [LogicalPhallacy](https://github.com/LogicalPhallacy/)
- [RazeLighter777](https://github.com/RazeLighter777)
- [WillWill56](https://github.com/WillWill56)
- [Liggy](https://github.com/Liggy)
- [fruhnow](https://github.com/fruhnow)
- [Lynxy](https://github.com/Lynxy)
- [fasheng](https://github.com/fasheng)
- [ploughpuff](https://github.com/ploughpuff)
- [pjeanjean](https://github.com/pjeanjean)
- [DrPandemic](https://github.com/drpandemic)
- [joern-h](https://github.com/joern-h)
- [Khinenw](https://github.com/HelloWorld017)
- [fhriley](https://github.com/fhriley)
- [nevado](https://github.com/nevado)
# Emby Contributors # Emby Contributors

View File

@@ -1,42 +1,37 @@
ARG DOTNET_VERSION=2.2 ARG DOTNET_VERSION=2
ARG FFMPEG_VERSION=latest
FROM node:alpine as web-builder
ARG JELLYFIN_WEB_VERSION=v10.4.2
RUN apk add curl \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& yarn install \
&& yarn build \
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder # Download ffmpeg first to allow quicker rebuild of other layers
FROM alpine as ffmpeg
ARG FFMPEG_URL=https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.0.3-64bit-static.tar.xz
RUN wget ${FFMPEG_URL} -O - | tar Jxf - \
&& mkdir ffmpeg-bin \
&& mv ffmpeg*/ffmpeg ffmpeg-bin \
&& mv ffmpeg*/ffprobe ffmpeg-bin
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 RUN export DOTNET_CLI_TELEMETRY_OPTOUT=1 \
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" && dotnet clean \
&& dotnet publish \
--configuration release \
--output /jellyfin \
Jellyfin.Server
FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION} FROM microsoft/dotnet:${DOTNET_VERSION}-runtime
COPY --from=ffmpeg / /
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web COPY --from=ffmpeg /ffmpeg-bin/* /usr/bin/
# Install dependencies: EXPOSE 8096
# libfontconfig1: needed for Skia VOLUME /config /media
# mesa-va-drivers: needed for VAAPI
# libfontconfig1 is required for Skia
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \ && apt-get install --no-install-recommends --no-install-suggests -y \
libfontconfig1 mesa-va-drivers \ libfontconfig1 \
&& apt-get clean autoclean \ && apt-get clean autoclean \
&& apt-get autoremove \ && apt-get autoremove \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/{apt,dpkg,cache,log}
&& mkdir -p /cache /config /media \ ENTRYPOINT dotnet /jellyfin/jellyfin.dll -programdata /config
&& chmod 777 /cache /config /media
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
--datadir /config \
--cachedir /cache \
--ffmpeg /usr/local/bin/ffmpeg

View File

@@ -1,44 +1,24 @@
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=3.0 ARG DOTNET_VERSION=3.0
FROM node:alpine as web-builder FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch-arm32v7 as builder
ARG JELLYFIN_WEB_VERSION=v10.4.2
RUN apk add curl \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& yarn install \
&& yarn build \
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 #TODO Remove or update the sed line when we update dotnet version.
# TODO Remove or update the sed line when we update dotnet version. RUN export DOTNET_CLI_TELEMETRY_OPTOUT=1 \
RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; && find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; \
# Discard objs - may cause failures if exists && dotnet clean -maxcpucount:1 \
RUN find . -type d -name obj | xargs -r rm -r && dotnet publish \
# Build -maxcpucount:1 \
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" --configuration release \
--output /jellyfin \
Jellyfin.Server
FROM multiarch/qemu-user-static:x86_64-arm as qemu FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096 EXPOSE 8096
VOLUME /cache /config /media RUN apt-get update \
ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ && apt-get install -y ffmpeg
--datadir /config \ VOLUME /config /media
--cachedir /cache \ ENTRYPOINT dotnet /jellyfin/jellyfin.dll -programdata /config
--ffmpeg /usr/bin/ffmpeg

View File

@@ -1,44 +1,33 @@
# Requires binfm_misc registration # Requires binfm_misc registration for aarch64
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=3.0 ARG DOTNET_VERSION=3.0
FROM node:alpine as web-builder FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
ARG JELLYFIN_WEB_VERSION=v10.4.2 FROM alpine as qemu_extract
RUN apk add curl \ COPY --from=qemu /usr/bin qemu_user_static.tgz
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ RUN tar -xzvf qemu_user_static.tgz
&& cd jellyfin-web-* \
&& yarn install \
&& yarn build \
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch-arm64v8 as builder
COPY --from=qemu_extract qemu-* /usr/bin
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 #TODO Remove or update the sed line when we update dotnet version.
# TODO Remove or update the sed line when we update dotnet version. RUN export DOTNET_CLI_TELEMETRY_OPTOUT=1 \
RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; && find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; \
# Discard objs - may cause failures if exists && dotnet clean \
RUN find . -type d -name obj | xargs -r rm -r && dotnet publish \
# Build --configuration release \
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" --output /jellyfin \
Jellyfin.Server
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8 COPY --from=qemu_extract qemu-* /usr/bin
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096 EXPOSE 8096
VOLUME /cache /config /media RUN apt-get update \
ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ && apt-get install -y ffmpeg
--datadir /config \ VOLUME /config /media
--cachedir /cache \ ENTRYPOINT dotnet /jellyfin/jellyfin.dll -programdata /config
--ffmpeg /usr/bin/ffmpeg

2565
Doxyfile

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -0,0 +1,36 @@
namespace DvdLib.Ifo
{
public enum AudioCodec
{
AC3 = 0,
MPEG1 = 2,
MPEG2ext = 3,
LPCM = 4,
DTS = 6,
}
public enum ApplicationMode
{
Unspecified = 0,
Karaoke = 1,
Surround = 2,
}
public class AudioAttributes
{
public readonly AudioCodec Codec;
public readonly bool MultichannelExtensionPresent;
public readonly ApplicationMode Mode;
public readonly byte QuantDRC;
public readonly byte SampleRate;
public readonly byte Channels;
public readonly ushort LanguageCode;
public readonly byte LanguageExtension;
public readonly byte CodeExtension;
}
public class MultiChannelExtension
{
}
}

View File

@@ -26,17 +26,17 @@ namespace DvdLib.Ifo
if (vmgPath == null) if (vmgPath == null)
{ {
foreach (var ifo in allFiles) var allIfos = allFiles.Where(i => string.Equals(i.Extension, ".ifo", StringComparison.OrdinalIgnoreCase));
{
if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var nums = ifo.Name.Split(new [] { '_' }, StringSplitOptions.RemoveEmptyEntries); foreach (var ifo in allIfos)
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber)) {
var num = ifo.Name.Split('_').ElementAtOrDefault(1);
var numbersRead = new List<ushort>();
if (!string.IsNullOrEmpty(num) && ushort.TryParse(num, out var ifoNumber) && !numbersRead.Contains(ifoNumber))
{ {
ReadVTS(ifoNumber, ifo.FullName); ReadVTS(ifoNumber, ifo.FullName);
numbersRead.Add(ifoNumber);
} }
} }
} }
@@ -76,7 +76,7 @@ namespace DvdLib.Ifo
} }
} }
private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles) private void ReadVTS(ushort vtsNum, List<FileSystemMetadata> allFiles)
{ {
var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum); var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace DvdLib.Ifo
{
public class ProgramChainCommandTable
{
public readonly ushort LastByteAddress;
public readonly List<VirtualMachineCommand> PreCommands;
public readonly List<VirtualMachineCommand> PostCommands;
public readonly List<VirtualMachineCommand> CellCommands;
}
public class VirtualMachineCommand
{
public readonly byte[] Command;
}
}

View File

@@ -25,10 +25,13 @@ namespace DvdLib.Ifo
public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
private ushort _nextProgramNumber; private ushort _nextProgramNumber;
public readonly ProgramChain Next;
private ushort _prevProgramNumber; private ushort _prevProgramNumber;
public readonly ProgramChain Previous;
private ushort _goupProgramNumber; private ushort _goupProgramNumber;
public readonly ProgramChain Goup; // ?? maybe Group
public ProgramPlaybackMode PlaybackMode { get; private set; } public ProgramPlaybackMode PlaybackMode { get; private set; }
public uint ProgramCount { get; private set; } public uint ProgramCount { get; private set; }
@@ -37,6 +40,7 @@ namespace DvdLib.Ifo
public byte[] Palette { get; private set; } // 16*4 entries public byte[] Palette { get; private set; } // 16*4 entries
private ushort _commandTableOffset; private ushort _commandTableOffset;
public readonly ProgramChainCommandTable CommandTable;
private ushort _programMapOffset; private ushort _programMapOffset;
private ushort _cellPlaybackOffset; private ushort _cellPlaybackOffset;

View File

@@ -0,0 +1,46 @@
namespace DvdLib.Ifo
{
public enum VideoCodec
{
MPEG1 = 0,
MPEG2 = 1,
}
public enum VideoFormat
{
NTSC = 0,
PAL = 1,
}
public enum AspectRatio
{
ar4to3 = 0,
ar16to9 = 3
}
public enum FilmMode
{
None = -1,
Camera = 0,
Film = 1,
}
public class VideoAttributes
{
public readonly VideoCodec Codec;
public readonly VideoFormat Format;
public readonly AspectRatio Aspect;
public readonly bool AutomaticPanScan;
public readonly bool AutomaticLetterBox;
public readonly bool Line21CCField1;
public readonly bool Line21CCField2;
public readonly int Width;
public readonly int Height;
public readonly bool Letterboxed;
public readonly FilmMode FilmMode;
public VideoAttributes()
{
}
}
}

View File

@@ -9,8 +9,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyCompany("Jellyfin Project")]
[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyProduct("Jellyfin: The Free Software Media System")]
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")] [assembly: NeutralResourcesLanguage("en")]

View File

@@ -6,7 +6,6 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Main; using Emby.Dlna.Main;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
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;
@@ -109,13 +108,12 @@ namespace Emby.Dlna.Api
public class DlnaServerService : IService, IRequiresRequest public class DlnaServerService : IService, IRequiresRequest
{ {
private readonly IDlnaManager _dlnaManager;
private const string XMLContentType = "text/xml; charset=UTF-8"; private const string XMLContentType = "text/xml; charset=UTF-8";
private readonly IDlnaManager _dlnaManager;
private readonly IHttpResultFactory _resultFactory;
private readonly IServerConfigurationManager _configurationManager;
public IRequest Request { get; set; } public IRequest Request { get; set; }
private IHttpResultFactory _resultFactory;
private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory; private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory;
@@ -123,14 +121,10 @@ namespace Emby.Dlna.Api
private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar; private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar;
public DlnaServerService( public DlnaServerService(IDlnaManager dlnaManager, IHttpResultFactory httpResultFactory)
IDlnaManager dlnaManager,
IHttpResultFactory httpResultFactory,
IServerConfigurationManager configurationManager)
{ {
_dlnaManager = dlnaManager; _dlnaManager = dlnaManager;
_resultFactory = httpResultFactory; _resultFactory = httpResultFactory;
_configurationManager = configurationManager;
} }
private string GetHeader(string name) private string GetHeader(string name)
@@ -142,7 +136,7 @@ namespace Emby.Dlna.Api
{ {
var url = Request.AbsoluteUri; var url = Request.AbsoluteUri;
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress); var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers.ToDictionary(), request.UuId, serverAddress);
var cacheLength = TimeSpan.FromDays(1); var cacheLength = TimeSpan.FromDays(1);
var cacheKey = Request.RawUrl.GetMD5(); var cacheKey = Request.RawUrl.GetMD5();
@@ -153,21 +147,21 @@ namespace Emby.Dlna.Api
public object Get(GetContentDirectory request) public object Get(GetContentDirectory request)
{ {
var xml = ContentDirectory.GetServiceXml(); var xml = ContentDirectory.GetServiceXml(Request.Headers.ToDictionary());
return _resultFactory.GetResult(Request, xml, XMLContentType); return _resultFactory.GetResult(Request, xml, XMLContentType);
} }
public object Get(GetMediaReceiverRegistrar request) public object Get(GetMediaReceiverRegistrar request)
{ {
var xml = MediaReceiverRegistrar.GetServiceXml(); var xml = MediaReceiverRegistrar.GetServiceXml(Request.Headers.ToDictionary());
return _resultFactory.GetResult(Request, xml, XMLContentType); return _resultFactory.GetResult(Request, xml, XMLContentType);
} }
public object Get(GetConnnectionManager request) public object Get(GetConnnectionManager request)
{ {
var xml = ConnectionManager.GetServiceXml(); var xml = ConnectionManager.GetServiceXml(Request.Headers.ToDictionary());
return _resultFactory.GetResult(Request, xml, XMLContentType); return _resultFactory.GetResult(Request, xml, XMLContentType);
} }
@@ -199,7 +193,7 @@ namespace Emby.Dlna.Api
return service.ProcessControlRequest(new ControlRequest return service.ProcessControlRequest(new ControlRequest
{ {
Headers = Request.Headers, Headers = Request.Headers.ToDictionary(),
InputXml = requestStream, InputXml = requestStream,
TargetServerUuId = id, TargetServerUuId = id,
RequestedUrl = Request.AbsoluteUri RequestedUrl = Request.AbsoluteUri
@@ -211,25 +205,14 @@ namespace Emby.Dlna.Api
var pathInfo = Parse(Request.PathInfo); var pathInfo = Parse(Request.PathInfo);
var first = pathInfo[0]; var first = pathInfo[0];
string baseUrl = _configurationManager.Configuration.BaseUrl;
// backwards compatibility // backwards compatibility
if (baseUrl.Length == 0 // TODO: Work out what this is doing.
&& (string.Equals(first, "mediabrowser", StringComparison.OrdinalIgnoreCase) if (string.Equals(first, "mediabrowser", StringComparison.OrdinalIgnoreCase) ||
|| string.Equals(first, "emby", StringComparison.OrdinalIgnoreCase))) string.Equals(first, "emby", StringComparison.OrdinalIgnoreCase) ||
string.Equals(first, "jellyfin", StringComparison.OrdinalIgnoreCase))
{ {
index++; index++;
} }
else if (string.Equals(first, baseUrl.Remove(0, 1)))
{
index++;
var second = pathInfo[1];
if (string.Equals(second, "mediabrowser", StringComparison.OrdinalIgnoreCase)
|| string.Equals(second, "emby", StringComparison.OrdinalIgnoreCase))
{
index++;
}
}
return pathInfo[index]; return pathInfo[index];
} }
@@ -253,9 +236,7 @@ namespace Emby.Dlna.Api
public object Get(GetIcon request) public object Get(GetIcon request)
{ {
var contentType = "image/" + Path.GetExtension(request.Filename) var contentType = "image/" + Path.GetExtension(request.Filename).TrimStart('.').ToLower();
.TrimStart('.')
.ToLowerInvariant();
var cacheLength = TimeSpan.FromDays(365); var cacheLength = TimeSpan.FromDays(365);
var cacheKey = Request.RawUrl.GetMD5(); var cacheKey = Request.RawUrl.GetMD5();

View File

@@ -7,7 +7,6 @@ namespace Emby.Dlna.Configuration
public bool EnableServer { get; set; } public bool EnableServer { get; set; }
public bool EnableDebugLog { get; set; } public bool EnableDebugLog { get; set; }
public bool BlastAliveMessages { get; set; } public bool BlastAliveMessages { get; set; }
public bool SendOnlyMatchedHost { get; set; }
public int ClientDiscoveryIntervalSeconds { get; set; } public int ClientDiscoveryIntervalSeconds { get; set; }
public int BlastAliveMessageIntervalSeconds { get; set; } public int BlastAliveMessageIntervalSeconds { get; set; }
public string DefaultUserId { get; set; } public string DefaultUserId { get; set; }
@@ -17,7 +16,6 @@ namespace Emby.Dlna.Configuration
EnablePlayTo = true; EnablePlayTo = true;
EnableServer = true; EnableServer = true;
BlastAliveMessages = true; BlastAliveMessages = true;
SendOnlyMatchedHost = true;
ClientDiscoveryIntervalSeconds = 60; ClientDiscoveryIntervalSeconds = 60;
BlastAliveMessageIntervalSeconds = 1800; BlastAliveMessageIntervalSeconds = 1800;
} }

View File

@@ -1,7 +1,9 @@
using System.Collections.Generic;
using Emby.Dlna.Service; using Emby.Dlna.Service;
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.Model.Xml;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager namespace Emby.Dlna.ConnectionManager
@@ -11,16 +13,18 @@ namespace Emby.Dlna.ConnectionManager
private readonly IDlnaManager _dlna; private readonly IDlnaManager _dlna;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory;
public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient) public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient, IXmlReaderSettingsFactory xmlReaderSettingsFactory)
: base(logger, httpClient) : base(logger, httpClient)
{ {
_dlna = dlna; _dlna = dlna;
_config = config; _config = config;
_logger = logger; _logger = logger;
XmlReaderSettingsFactory = xmlReaderSettingsFactory;
} }
public string GetServiceXml() public string GetServiceXml(IDictionary<string, string> headers)
{ {
return new ConnectionManagerXmlBuilder().GetXml(); return new ConnectionManagerXmlBuilder().GetXml();
} }
@@ -30,7 +34,7 @@ namespace Emby.Dlna.ConnectionManager
var profile = _dlna.GetProfile(request.Headers) ?? var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile(); _dlna.GetDefaultProfile();
return new ControlHandler(_config, _logger, profile).ProcessControlRequest(request); return new ControlHandler(_config, _logger, XmlReaderSettingsFactory, profile).ProcessControlRequest(request);
} }
} }
} }

View File

@@ -4,6 +4,7 @@ using Emby.Dlna.Service;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Xml;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager namespace Emby.Dlna.ConnectionManager
@@ -31,8 +32,7 @@ namespace Emby.Dlna.ConnectionManager
}; };
} }
public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile) public ControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory, DeviceProfile profile) : base(config, logger, xmlReaderSettingsFactory)
: base(config, logger)
{ {
_profile = profile; _profile = profile;
} }

View File

@@ -11,6 +11,7 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.TV; using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Xml;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ContentDirectory namespace Emby.Dlna.ContentDirectory
@@ -27,6 +28,7 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IUserViewManager _userViewManager; private readonly IUserViewManager _userViewManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory;
private readonly ITVSeriesManager _tvSeriesManager; private readonly ITVSeriesManager _tvSeriesManager;
public ContentDirectory(IDlnaManager dlna, public ContentDirectory(IDlnaManager dlna,
@@ -36,12 +38,7 @@ namespace Emby.Dlna.ContentDirectory
IServerConfigurationManager config, IServerConfigurationManager config,
IUserManager userManager, IUserManager userManager,
ILogger logger, ILogger logger,
IHttpClient httpClient, IHttpClient httpClient, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, IMediaEncoder mediaEncoder, IXmlReaderSettingsFactory xmlReaderSettingsFactory, ITVSeriesManager tvSeriesManager)
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
IUserViewManager userViewManager,
IMediaEncoder mediaEncoder,
ITVSeriesManager tvSeriesManager)
: base(logger, httpClient) : base(logger, httpClient)
{ {
_dlna = dlna; _dlna = dlna;
@@ -54,6 +51,7 @@ namespace Emby.Dlna.ContentDirectory
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_userViewManager = userViewManager; _userViewManager = userViewManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
XmlReaderSettingsFactory = xmlReaderSettingsFactory;
_tvSeriesManager = tvSeriesManager; _tvSeriesManager = tvSeriesManager;
} }
@@ -67,7 +65,7 @@ namespace Emby.Dlna.ContentDirectory
} }
} }
public string GetServiceXml() public string GetServiceXml(IDictionary<string, string> headers)
{ {
return new ContentDirectoryXmlBuilder().GetXml(); return new ContentDirectoryXmlBuilder().GetXml();
} }
@@ -78,6 +76,7 @@ namespace Emby.Dlna.ContentDirectory
_dlna.GetDefaultProfile(); _dlna.GetDefaultProfile();
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase)); var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
string accessToken = null;
var user = GetUser(profile); var user = GetUser(profile);
@@ -86,7 +85,7 @@ namespace Emby.Dlna.ContentDirectory
_libraryManager, _libraryManager,
profile, profile,
serverAddress, serverAddress,
null, accessToken,
_imageProcessor, _imageProcessor,
_userDataManager, _userDataManager,
user, user,
@@ -96,6 +95,7 @@ namespace Emby.Dlna.ContentDirectory
_mediaSourceManager, _mediaSourceManager,
_userViewManager, _userViewManager,
_mediaEncoder, _mediaEncoder,
XmlReaderSettingsFactory,
_tvSeriesManager) _tvSeriesManager)
.ProcessControlRequest(request); .ProcessControlRequest(request);
} }

View File

@@ -25,6 +25,7 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Xml;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ContentDirectory namespace Emby.Dlna.ContentDirectory
@@ -50,22 +51,8 @@ namespace Emby.Dlna.ContentDirectory
private readonly DeviceProfile _profile; private readonly DeviceProfile _profile;
public ControlHandler( public ControlHandler(ILogger logger, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, string accessToken, IImageProcessor imageProcessor, IUserDataManager userDataManager, User user, int systemUpdateId, IServerConfigurationManager config, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, IMediaEncoder mediaEncoder, IXmlReaderSettingsFactory xmlReaderSettingsFactory, ITVSeriesManager tvSeriesManager)
ILogger logger, : base(config, logger, xmlReaderSettingsFactory)
ILibraryManager libraryManager,
DeviceProfile profile,
string serverAddress,
string accessToken,
IImageProcessor imageProcessor,
IUserDataManager userDataManager,
User user, int systemUpdateId,
IServerConfigurationManager config,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
IUserViewManager userViewManager,
IMediaEncoder mediaEncoder,
ITVSeriesManager tvSeriesManager)
: base(config, logger)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userDataManager = userDataManager; _userDataManager = userDataManager;
@@ -76,7 +63,7 @@ namespace Emby.Dlna.ContentDirectory
_profile = profile; _profile = profile;
_config = config; _config = config;
_didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, _logger, mediaEncoder); _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, _logger, libraryManager, mediaEncoder);
} }
protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams)
@@ -273,7 +260,7 @@ namespace Emby.Dlna.ContentDirectory
if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue) if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
{ {
var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount); var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
_didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id); _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
} }
@@ -286,10 +273,10 @@ namespace Emby.Dlna.ContentDirectory
} }
else else
{ {
var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount); var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
totalCount = childrenResult.TotalRecordCount; totalCount = childrenResult.TotalRecordCount;
provided = childrenResult.Items.Count; provided = childrenResult.Items.Length;
foreach (var i in childrenResult.Items) foreach (var i in childrenResult.Items)
{ {
@@ -309,7 +296,6 @@ namespace Emby.Dlna.ContentDirectory
} }
} }
} }
writer.WriteFullEndElement(); writer.WriteFullEndElement();
//writer.WriteEndDocument(); //writer.WriteEndDocument();
} }
@@ -387,7 +373,7 @@ namespace Emby.Dlna.ContentDirectory
totalCount = childrenResult.TotalRecordCount; totalCount = childrenResult.TotalRecordCount;
provided = childrenResult.Items.Count; provided = childrenResult.Items.Length;
var dlnaOptions = _config.GetDlnaConfiguration(); var dlnaOptions = _config.GetDlnaConfiguration();
@@ -468,7 +454,7 @@ namespace Emby.Dlna.ContentDirectory
User = user, User = user,
Recursive = true, Recursive = true,
IsMissing = false, IsMissing = false,
ExcludeItemTypes = new[] { typeof(Book).Name }, ExcludeItemTypes = new[] { typeof(Game).Name, typeof(Book).Name },
IsFolder = isFolder, IsFolder = isFolder,
MediaTypes = mediaTypes.ToArray(), MediaTypes = mediaTypes.ToArray(),
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
@@ -497,26 +483,27 @@ namespace Emby.Dlna.ContentDirectory
return GetGenreItems(item, Guid.Empty, user, sort, startIndex, limit); return GetGenreItems(item, Guid.Empty, user, sort, startIndex, limit);
} }
if ((!stubType.HasValue || stubType.Value != StubType.Folder) if (!stubType.HasValue || stubType.Value != StubType.Folder)
&& item is IHasCollectionType collectionFolder)
{ {
if (string.Equals(CollectionType.Music, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase)) var collectionFolder = item as IHasCollectionType;
if (collectionFolder != null && string.Equals(CollectionType.Music, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
{ {
return GetMusicFolders(item, user, stubType, sort, startIndex, limit); return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
} }
else if (string.Equals(CollectionType.Movies, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase)) if (collectionFolder != null && string.Equals(CollectionType.Movies, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
{ {
return GetMovieFolders(item, user, stubType, sort, startIndex, limit); return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
} }
else if (string.Equals(CollectionType.TvShows, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase)) if (collectionFolder != null && string.Equals(CollectionType.TvShows, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
{ {
return GetTvFolders(item, user, stubType, sort, startIndex, limit); return GetTvFolders(item, user, stubType, sort, startIndex, limit);
} }
else if (string.Equals(CollectionType.Folders, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
if (collectionFolder != null && string.Equals(CollectionType.Folders, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
{ {
return GetFolders(item, user, stubType, sort, startIndex, limit); return GetFolders(item, user, stubType, sort, startIndex, limit);
} }
else if (string.Equals(CollectionType.LiveTv, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase)) if (collectionFolder != null && string.Equals(CollectionType.LiveTv, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
{ {
return GetLiveTvChannels(item, user, stubType, sort, startIndex, limit); return GetLiveTvChannels(item, user, stubType, sort, startIndex, limit);
} }
@@ -537,7 +524,7 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
IsVirtualItem = false, IsVirtualItem = false,
ExcludeItemTypes = new[] { typeof(Book).Name }, ExcludeItemTypes = new[] { typeof(Game).Name, typeof(Book).Name },
IsPlaceHolder = false, IsPlaceHolder = false,
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
}; };
@@ -678,7 +665,7 @@ namespace Emby.Dlna.ContentDirectory
return new QueryResult<ServerItem> return new QueryResult<ServerItem>
{ {
Items = list, Items = list.ToArray(),
TotalRecordCount = list.Count TotalRecordCount = list.Count
}; };
} }
@@ -756,7 +743,7 @@ namespace Emby.Dlna.ContentDirectory
return new QueryResult<ServerItem> return new QueryResult<ServerItem>
{ {
Items = list, Items = list.ToArray(),
TotalRecordCount = list.Count TotalRecordCount = list.Count
}; };
} }
@@ -861,7 +848,7 @@ namespace Emby.Dlna.ContentDirectory
return new QueryResult<ServerItem> return new QueryResult<ServerItem>
{ {
Items = list, Items = list.ToArray(),
TotalRecordCount = list.Count TotalRecordCount = list.Count
}; };
} }

View File

@@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Http;
namespace Emby.Dlna namespace Emby.Dlna
{ {
public class ControlRequest public class ControlRequest
{ {
public IHeaderDictionary Headers { get; set; } public IDictionary<string, string> Headers { get; set; }
public Stream InputXml { get; set; } public Stream InputXml { get; set; }
@@ -15,7 +15,7 @@ namespace Emby.Dlna
public ControlRequest() public ControlRequest()
{ {
Headers = new HeaderDictionary(); Headers = new Dictionary<string, string>();
} }
} }
} }

View File

@@ -43,30 +43,22 @@ namespace Emby.Dlna.Didl
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
public DidlBuilder( public DidlBuilder(DeviceProfile profile, User user, IImageProcessor imageProcessor, string serverAddress, string accessToken, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, ILogger logger, ILibraryManager libraryManager, IMediaEncoder mediaEncoder)
DeviceProfile profile,
User user,
IImageProcessor imageProcessor,
string serverAddress,
string accessToken,
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
ILogger logger,
IMediaEncoder mediaEncoder)
{ {
_profile = profile; _profile = profile;
_user = user;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_serverAddress = serverAddress; _serverAddress = serverAddress;
_accessToken = accessToken;
_userDataManager = userDataManager; _userDataManager = userDataManager;
_localization = localization; _localization = localization;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_logger = logger; _logger = logger;
_libraryManager = libraryManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_accessToken = accessToken;
_user = user;
} }
public static string NormalizeDlnaMediaUrl(string url) public static string NormalizeDlnaMediaUrl(string url)
@@ -125,8 +117,7 @@ namespace Emby.Dlna.Didl
} }
} }
public void WriteItemElement( public void WriteItemElement(DlnaOptions options,
DlnaOptions options,
XmlWriter writer, XmlWriter writer,
BaseItem item, BaseItem item,
User user, User user,
@@ -158,7 +149,7 @@ namespace Emby.Dlna.Didl
AddGeneralProperties(item, null, context, writer, filter); AddGeneralProperties(item, null, context, writer, filter);
AddSamsungBookmarkInfo(item, user, writer, streamInfo); AddSamsungBookmarkInfo(item, user, writer);
// refID? // refID?
// storeAttribute(itemNode, object, ClassProperties.REF_ID, false); // storeAttribute(itemNode, object, ClassProperties.REF_ID, false);
@@ -181,6 +172,19 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement(); writer.WriteFullEndElement();
} }
private string GetMimeType(string input)
{
var mime = MimeTypes.GetMimeType(input);
// TODO: Instead of being hard-coded here, this should probably be moved into all of the existing profiles
if (string.Equals(mime, "video/mp2t", StringComparison.OrdinalIgnoreCase))
{
mime = "video/mpeg";
}
return mime;
}
private void AddVideoResource(DlnaOptions options, XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null) private void AddVideoResource(DlnaOptions options, XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
{ {
if (streamInfo == null) if (streamInfo == null)
@@ -228,15 +232,12 @@ namespace Emby.Dlna.Didl
AddVideoResource(writer, video, deviceId, filter, contentFeature, streamInfo); AddVideoResource(writer, video, deviceId, filter, contentFeature, streamInfo);
} }
var subtitleProfiles = streamInfo.GetSubtitleProfiles(_mediaEncoder, false, _serverAddress, _accessToken); var subtitleProfiles = streamInfo.GetSubtitleProfiles(_mediaEncoder, false, _serverAddress, _accessToken)
.Where(subtitle => subtitle.DeliveryMethod == SubtitleDeliveryMethod.External)
.ToList();
foreach (var subtitle in subtitleProfiles) foreach (var subtitle in subtitleProfiles)
{ {
if (subtitle.DeliveryMethod != SubtitleDeliveryMethod.External)
{
continue;
}
var subtitleAdded = AddSubtitleElement(writer, subtitle); var subtitleAdded = AddSubtitleElement(writer, subtitle);
if (subtitleAdded && _profile.EnableSingleSubtitleLimit) if (subtitleAdded && _profile.EnableSingleSubtitleLimit)
@@ -249,8 +250,7 @@ namespace Emby.Dlna.Didl
private bool AddSubtitleElement(XmlWriter writer, SubtitleStreamInfo info) private bool AddSubtitleElement(XmlWriter writer, SubtitleStreamInfo info)
{ {
var subtitleProfile = _profile.SubtitleProfiles var subtitleProfile = _profile.SubtitleProfiles
.FirstOrDefault(i => string.Equals(info.Format, i.Format, StringComparison.OrdinalIgnoreCase) .FirstOrDefault(i => string.Equals(info.Format, i.Format, StringComparison.OrdinalIgnoreCase) && i.Method == SubtitleDeliveryMethod.External);
&& i.Method == SubtitleDeliveryMethod.External);
if (subtitleProfile == null) if (subtitleProfile == null)
{ {
@@ -265,7 +265,7 @@ namespace Emby.Dlna.Didl
// <sec:CaptionInfo sec:type="srt">http://192.168.1.3:9999/video.srt</sec:CaptionInfo> // <sec:CaptionInfo sec:type="srt">http://192.168.1.3:9999/video.srt</sec:CaptionInfo>
writer.WriteStartElement("sec", "CaptionInfoEx", null); writer.WriteStartElement("sec", "CaptionInfoEx", null);
writer.WriteAttributeString("sec", "type", null, info.Format.ToLowerInvariant()); writer.WriteAttributeString("sec", "type", null, info.Format.ToLower());
writer.WriteString(info.Url); writer.WriteString(info.Url);
writer.WriteFullEndElement(); writer.WriteFullEndElement();
@@ -282,7 +282,7 @@ namespace Emby.Dlna.Didl
else else
{ {
writer.WriteStartElement(string.Empty, "res", NS_DIDL); writer.WriteStartElement(string.Empty, "res", NS_DIDL);
var protocolInfo = string.Format("http-get:*:text/{0}:*", info.Format.ToLowerInvariant()); var protocolInfo = string.Format("http-get:*:text/{0}:*", info.Format.ToLower());
writer.WriteAttributeString("protocolInfo", protocolInfo); writer.WriteAttributeString("protocolInfo", protocolInfo);
writer.WriteString(info.Url); writer.WriteString(info.Url);
@@ -371,7 +371,7 @@ namespace Emby.Dlna.Didl
var filename = url.Substring(0, url.IndexOf('?')); var filename = url.Substring(0, url.IndexOf('?'));
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType) var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
? MimeTypes.GetMimeType(filename) ? GetMimeType(filename)
: mediaProfile.MimeType; : mediaProfile.MimeType;
writer.WriteAttributeString("protocolInfo", string.Format( writer.WriteAttributeString("protocolInfo", string.Format(
@@ -387,39 +387,91 @@ namespace Emby.Dlna.Didl
private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context) private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context)
{ {
if (itemStubType.HasValue) if (itemStubType.HasValue && itemStubType.Value == StubType.Latest)
{ {
switch (itemStubType.Value) return _localization.GetLocalizedString("Latest");
{ }
case StubType.Latest: return _localization.GetLocalizedString("Latest"); if (itemStubType.HasValue && itemStubType.Value == StubType.Playlists)
case StubType.Playlists: return _localization.GetLocalizedString("Playlists"); {
case StubType.AlbumArtists: return _localization.GetLocalizedString("HeaderAlbumArtists"); return _localization.GetLocalizedString("Playlists");
case StubType.Albums: return _localization.GetLocalizedString("Albums"); }
case StubType.Artists: return _localization.GetLocalizedString("Artists"); if (itemStubType.HasValue && itemStubType.Value == StubType.AlbumArtists)
case StubType.Songs: return _localization.GetLocalizedString("Songs"); {
case StubType.Genres: return _localization.GetLocalizedString("Genres"); return _localization.GetLocalizedString("HeaderAlbumArtists");
case StubType.FavoriteAlbums: return _localization.GetLocalizedString("HeaderFavoriteAlbums"); }
case StubType.FavoriteArtists: return _localization.GetLocalizedString("HeaderFavoriteArtists"); if (itemStubType.HasValue && itemStubType.Value == StubType.Albums)
case StubType.FavoriteSongs: return _localization.GetLocalizedString("HeaderFavoriteSongs"); {
case StubType.ContinueWatching: return _localization.GetLocalizedString("HeaderContinueWatching"); return _localization.GetLocalizedString("Albums");
case StubType.Movies: return _localization.GetLocalizedString("Movies"); }
case StubType.Collections: return _localization.GetLocalizedString("Collections"); if (itemStubType.HasValue && itemStubType.Value == StubType.Artists)
case StubType.Favorites: return _localization.GetLocalizedString("Favorites"); {
case StubType.NextUp: return _localization.GetLocalizedString("HeaderNextUp"); return _localization.GetLocalizedString("Artists");
case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows"); }
case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes"); if (itemStubType.HasValue && itemStubType.Value == StubType.Songs)
case StubType.Series: return _localization.GetLocalizedString("Shows"); {
default: break; return _localization.GetLocalizedString("Songs");
} }
if (itemStubType.HasValue && itemStubType.Value == StubType.Genres)
{
return _localization.GetLocalizedString("Genres");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.FavoriteAlbums)
{
return _localization.GetLocalizedString("HeaderFavoriteAlbums");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.FavoriteArtists)
{
return _localization.GetLocalizedString("HeaderFavoriteArtists");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.FavoriteSongs)
{
return _localization.GetLocalizedString("HeaderFavoriteSongs");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.ContinueWatching)
{
return _localization.GetLocalizedString("HeaderContinueWatching");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.Movies)
{
return _localization.GetLocalizedString("Movies");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.Collections)
{
return _localization.GetLocalizedString("Collections");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.Favorites)
{
return _localization.GetLocalizedString("Favorites");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.NextUp)
{
return _localization.GetLocalizedString("HeaderNextUp");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.FavoriteSeries)
{
return _localization.GetLocalizedString("HeaderFavoriteShows");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.FavoriteEpisodes)
{
return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
}
if (itemStubType.HasValue && itemStubType.Value == StubType.Series)
{
return _localization.GetLocalizedString("Shows");
} }
if (item is Episode episode && context is Season season) var episode = item as Episode;
var season = context as Season;
if (episode != null && season != null)
{ {
// 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 (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value == 0)
&& season.IndexNumber.HasValue && season.IndexNumber.Value != 0)
{ {
return string.Format(_localization.GetLocalizedString("ValueSpecialEpisodeName"), item.Name); if (season.IndexNumber.HasValue && season.IndexNumber.Value != 0)
{
return string.Format(_localization.GetLocalizedString("ValueSpecialEpisodeName"), item.Name);
}
} }
if (item.IndexNumber.HasValue) if (item.IndexNumber.HasValue)
@@ -507,7 +559,7 @@ namespace Emby.Dlna.Didl
var filename = url.Substring(0, url.IndexOf('?')); var filename = url.Substring(0, url.IndexOf('?'));
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType) var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
? MimeTypes.GetMimeType(filename) ? GetMimeType(filename)
: mediaProfile.MimeType; : mediaProfile.MimeType;
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container, var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container,
@@ -532,10 +584,19 @@ namespace Emby.Dlna.Didl
} }
public static bool IsIdRoot(string id) public static bool IsIdRoot(string id)
=> string.IsNullOrWhiteSpace(id) {
|| string.Equals(id, "0", StringComparison.OrdinalIgnoreCase) if (string.IsNullOrWhiteSpace(id) ||
string.Equals(id, "0", StringComparison.OrdinalIgnoreCase)
// Samsung sometimes uses 1 as root // Samsung sometimes uses 1 as root
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase); || string.Equals(id, "1", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null) public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
{ {
@@ -581,7 +642,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement(); writer.WriteFullEndElement();
} }
private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo) private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer)
{ {
if (!item.SupportsPositionTicksResume || item is Folder) if (!item.SupportsPositionTicksResume || item is Folder)
{ {
@@ -605,11 +666,10 @@ namespace Emby.Dlna.Didl
} }
var userdata = _userDataManager.GetUserData(user, item); var userdata = _userDataManager.GetUserData(user, item);
var playbackPositionTicks = (streamInfo != null && streamInfo.StartPositionTicks > 0) ? streamInfo.StartPositionTicks : userdata.PlaybackPositionTicks;
if (playbackPositionTicks > 0) if (userdata.PlaybackPositionTicks > 0)
{ {
var elementValue = string.Format("BM={0}", Convert.ToInt32(TimeSpan.FromTicks(playbackPositionTicks).TotalSeconds).ToString(_usCulture)); var elementValue = string.Format("BM={0}", Convert.ToInt32(TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds).ToString(_usCulture));
AddValue(writer, "sec", "dcmInfo", elementValue, secAttribute.Value); AddValue(writer, "sec", "dcmInfo", elementValue, secAttribute.Value);
} }
} }
@@ -748,7 +808,7 @@ namespace Emby.Dlna.Didl
{ {
writer.WriteString(_profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre.musicGenre"); writer.WriteString(_profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre.musicGenre");
} }
else if (item is Genre) else if (item is Genre || item is GameGenre)
{ {
writer.WriteString(_profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre"); writer.WriteString(_profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre");
} }
@@ -784,7 +844,7 @@ namespace Emby.Dlna.Didl
// var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) // var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
// ?? PersonType.Actor; // ?? PersonType.Actor;
// AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP); // AddValue(writer, "upnp", type.ToLower(), actor.Name, NS_UPNP);
// index++; // index++;
@@ -799,9 +859,10 @@ namespace Emby.Dlna.Didl
{ {
AddCommonFields(item, itemStubType, context, writer, filter); AddCommonFields(item, itemStubType, context, writer, filter);
var hasArtists = item as IHasArtist;
var hasAlbumArtists = item as IHasAlbumArtist; var hasAlbumArtists = item as IHasAlbumArtist;
if (item is IHasArtist hasArtists) if (hasArtists != null)
{ {
foreach (var artist in hasArtists.Artists) foreach (var artist in hasArtists.Artists)
{ {
@@ -871,7 +932,13 @@ namespace Emby.Dlna.Didl
private void AddCover(BaseItem item, BaseItem context, StubType? stubType, XmlWriter writer) private void AddCover(BaseItem item, BaseItem context, StubType? stubType, XmlWriter writer)
{ {
ImageDownloadInfo imageInfo = GetImageInfo(item); ImageDownloadInfo imageInfo = null;
// Finally, just use the image from the item
if (imageInfo == null)
{
imageInfo = GetImageInfo(item);
}
if (imageInfo == null) if (imageInfo == null)
{ {
@@ -901,6 +968,8 @@ namespace Emby.Dlna.Didl
} }
} }
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
{ {
AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG"); AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG");
@@ -909,9 +978,6 @@ namespace Emby.Dlna.Didl
AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG"); AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG");
AddImageResElement(item, writer, 160, 160, "png", "PNG_TN"); AddImageResElement(item, writer, 160, 160, "png", "PNG_TN");
} }
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
} }
private void AddEmbeddedImageAsCover(string name, XmlWriter writer) private void AddEmbeddedImageAsCover(string name, XmlWriter writer)
@@ -952,7 +1018,7 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("protocolInfo", string.Format( writer.WriteAttributeString("protocolInfo", string.Format(
"http-get:*:{0}:{1}", "http-get:*:{0}:{1}",
MimeTypes.GetMimeType("file." + format), GetMimeType("file." + format),
contentFeatures contentFeatures
)); ));
@@ -1028,8 +1094,8 @@ namespace Emby.Dlna.Didl
//{ //{
// var size = _imageProcessor.GetImageSize(imageInfo); // var size = _imageProcessor.GetImageSize(imageInfo);
// width = size.Width; // width = Convert.ToInt32(size.Width);
// height = size.Height; // height = Convert.ToInt32(size.Height);
//} //}
//catch //catch
//{ //{
@@ -1052,7 +1118,7 @@ namespace Emby.Dlna.Didl
}; };
} }
private class ImageDownloadInfo class ImageDownloadInfo
{ {
internal Guid ItemId; internal Guid ItemId;
internal string ImageTag; internal string ImageTag;
@@ -1068,7 +1134,7 @@ namespace Emby.Dlna.Didl
internal ItemImageInfo ItemImageInfo; internal ItemImageInfo ItemImageInfo;
} }
private class ImageUrlInfo class ImageUrlInfo
{ {
internal string Url; internal string Url;
@@ -1083,11 +1149,11 @@ namespace Emby.Dlna.Didl
public static string GetClientId(Guid idValue, StubType? stubType) public static string GetClientId(Guid idValue, StubType? stubType)
{ {
var id = idValue.ToString("N", CultureInfo.InvariantCulture); var id = idValue.ToString("N");
if (stubType.HasValue) if (stubType.HasValue)
{ {
id = stubType.Value.ToString().ToLowerInvariant() + "_" + id; id = stubType.Value.ToString().ToLower() + "_" + id;
} }
return id; return id;
@@ -1097,12 +1163,13 @@ namespace Emby.Dlna.Didl
{ {
var url = string.Format("{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0", var url = string.Format("{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0",
_serverAddress, _serverAddress,
info.ItemId.ToString("N", CultureInfo.InvariantCulture), info.ItemId.ToString("N"),
info.Type, info.Type,
info.ImageTag, info.ImageTag,
format, format,
maxWidth.ToString(CultureInfo.InvariantCulture), maxWidth.ToString(CultureInfo.InvariantCulture),
maxHeight.ToString(CultureInfo.InvariantCulture)); maxHeight.ToString(CultureInfo.InvariantCulture)
);
var width = info.Width; var width = info.Width;
var height = info.Height; var height = info.Height;
@@ -1111,11 +1178,15 @@ namespace Emby.Dlna.Didl
if (width.HasValue && height.HasValue) if (width.HasValue && height.HasValue)
{ {
var newSize = DrawingUtils.Resize( var newSize = DrawingUtils.Resize(new ImageSize
new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight); {
Height = height.Value,
Width = width.Value
width = newSize.Width; }, 0, 0, maxWidth, maxHeight);
height = newSize.Height;
width = Convert.ToInt32(newSize.Width);
height = Convert.ToInt32(newSize.Height);
var normalizedFormat = format var normalizedFormat = format
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase); .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);

View File

@@ -1,12 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Emby.Dlna.Profiles; using Emby.Dlna.Profiles;
using Emby.Dlna.Server; using Emby.Dlna.Server;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@@ -17,10 +14,9 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Reflection;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace Emby.Dlna namespace Emby.Dlna
{ {
@@ -32,7 +28,7 @@ namespace Emby.Dlna
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; private readonly IAssemblyInfo _assemblyInfo;
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal); private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
@@ -41,8 +37,7 @@ namespace Emby.Dlna
IFileSystem fileSystem, IFileSystem fileSystem,
IApplicationPaths appPaths, IApplicationPaths appPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IAssemblyInfo assemblyInfo)
IServerApplicationHost appHost)
{ {
_xmlSerializer = xmlSerializer; _xmlSerializer = xmlSerializer;
_fileSystem = fileSystem; _fileSystem = fileSystem;
@@ -50,13 +45,14 @@ namespace Emby.Dlna
_logger = loggerFactory.CreateLogger("Dlna"); _logger = loggerFactory.CreateLogger("Dlna");
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_appHost = appHost; _appHost = appHost;
_assemblyInfo = assemblyInfo;
} }
public async Task InitProfilesAsync() public void InitProfiles()
{ {
try try
{ {
await ExtractSystemProfilesAsync(); ExtractSystemProfiles();
LoadProfiles(); LoadProfiles();
} }
catch (Exception ex) catch (Exception ex)
@@ -206,13 +202,16 @@ namespace Emby.Dlna
} }
} }
public DeviceProfile GetProfile(IHeaderDictionary headers) public DeviceProfile GetProfile(IDictionary<string, string> headers)
{ {
if (headers == null) if (headers == null)
{ {
throw new ArgumentNullException(nameof(headers)); throw new ArgumentNullException(nameof(headers));
} }
// Convert to case insensitive
headers = new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase);
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification)); var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
if (profile != null) if (profile != null)
@@ -228,12 +227,12 @@ namespace Emby.Dlna
return profile; return profile;
} }
private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo) private bool IsMatch(IDictionary<string, string> headers, DeviceIdentification profileInfo)
{ {
return profileInfo.Headers.Any(i => IsMatch(headers, i)); return profileInfo.Headers.Any(i => IsMatch(headers, i));
} }
private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header) private bool IsMatch(IDictionary<string, string> headers, HttpHeaderInfo header)
{ {
// Handle invalid user setup // Handle invalid user setup
if (string.IsNullOrEmpty(header.Name)) if (string.IsNullOrEmpty(header.Name))
@@ -241,14 +240,14 @@ namespace Emby.Dlna
return false; return false;
} }
if (headers.TryGetValue(header.Name, out StringValues value)) if (headers.TryGetValue(header.Name, out string value))
{ {
switch (header.Match) switch (header.Match)
{ {
case HeaderMatchType.Equals: case HeaderMatchType.Equals:
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.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:
@@ -301,7 +300,7 @@ namespace Emby.Dlna
profile = ReserializeProfile(tempProfile); profile = ReserializeProfile(tempProfile);
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); profile.Id = path.ToLower().GetMD5().ToString("N");
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile); _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
@@ -353,48 +352,45 @@ namespace Emby.Dlna
Info = new DeviceProfileInfo Info = new DeviceProfileInfo
{ {
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture), Id = file.FullName.ToLower().GetMD5().ToString("N"),
Name = _fileSystem.GetFileNameWithoutExtension(file), Name = _fileSystem.GetFileNameWithoutExtension(file),
Type = type Type = type
} }
}; };
} }
private async Task ExtractSystemProfilesAsync() private void ExtractSystemProfiles()
{ {
var namespaceName = GetType().Namespace + ".Profiles.Xml."; var namespaceName = GetType().Namespace + ".Profiles.Xml.";
var systemProfilesPath = SystemProfilesPath; var systemProfilesPath = SystemProfilesPath;
foreach (var name in _assembly.GetManifestResourceNames()) foreach (var name in _assemblyInfo.GetManifestResourceNames(GetType())
.Where(i => i.StartsWith(namespaceName))
.ToList())
{ {
if (!name.StartsWith(namespaceName))
{
continue;
}
var filename = Path.GetFileName(name).Substring(namespaceName.Length); var filename = Path.GetFileName(name).Substring(namespaceName.Length);
var path = Path.Combine(systemProfilesPath, filename); var path = Path.Combine(systemProfilesPath, filename);
using (var stream = _assembly.GetManifestResourceStream(name)) using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), name))
{ {
var fileInfo = _fileSystem.GetFileInfo(path); var fileInfo = _fileSystem.GetFileInfo(path);
if (!fileInfo.Exists || fileInfo.Length != stream.Length) if (!fileInfo.Exists || fileInfo.Length != stream.Length)
{ {
Directory.CreateDirectory(systemProfilesPath); _fileSystem.CreateDirectory(systemProfilesPath);
using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
{ {
await stream.CopyToAsync(fileStream); stream.CopyTo(fileStream);
} }
} }
} }
} }
// Not necessary, but just to make it easy to find // Not necessary, but just to make it easy to find
Directory.CreateDirectory(UserProfilesPath); _fileSystem.CreateDirectory(UserProfilesPath);
} }
public void DeleteProfile(string id) public void DeleteProfile(string id)
@@ -494,7 +490,7 @@ namespace Emby.Dlna
internal string Path { get; set; } internal string Path { get; set; }
} }
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) public string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId, string serverAddress)
{ {
var profile = GetProfile(headers) ?? var profile = GetProfile(headers) ??
GetDefaultProfile(); GetDefaultProfile();
@@ -510,12 +506,12 @@ namespace Emby.Dlna
? ImageFormat.Png ? ImageFormat.Png
: ImageFormat.Jpg; : ImageFormat.Jpg;
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant(); var resource = GetType().Namespace + ".Images." + filename.ToLower();
return new ImageStream return new ImageStream
{ {
Format = format, Format = format,
Stream = _assembly.GetManifestResourceStream(resource) Stream = _assemblyInfo.GetManifestResourceStream(GetType(), resource)
}; };
} }
} }

View File

@@ -14,7 +14,6 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -59,9 +58,4 @@
<EmbeddedResource Include="Profiles\Xml\Xbox One.xml" /> <EmbeddedResource Include="Profiles\Xml\Xbox One.xml" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
</ItemGroup>
</Project> </Project>

View File

@@ -55,7 +55,7 @@ namespace Emby.Dlna.Eventing
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl) public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
{ {
var timeout = ParseTimeout(requestedTimeoutString) ?? 300; var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); var id = "uuid:" + Guid.NewGuid().ToString("N");
// Remove logging for now because some devices are sending this very frequently // Remove logging for now because some devices are sending this very frequently
// TODO re-enable with dlna debug logging setting // TODO re-enable with dlna debug logging setting

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Emby.Dlna namespace Emby.Dlna
{ {
public interface IUpnpService public interface IUpnpService
@@ -5,8 +7,9 @@ namespace Emby.Dlna
/// <summary> /// <summary>
/// Gets the content directory XML. /// Gets the content directory XML.
/// </summary> /// </summary>
/// <param name="headers">The headers.</param>
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
string GetServiceXml(); string GetServiceXml(IDictionary<string, string> headers);
/// <summary> /// <summary>
/// Processes the control request. /// Processes the control request.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Net.Sockets; using System.Linq;
using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.PlayTo; using Emby.Dlna.PlayTo;
@@ -21,10 +20,11 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
using MediaBrowser.Model.Threading;
using MediaBrowser.Model.Xml;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Rssdp; using Rssdp;
using Rssdp.Infrastructure; using Rssdp.Infrastructure;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Dlna.Main namespace Emby.Dlna.Main
{ {
@@ -50,7 +50,9 @@ namespace Emby.Dlna.Main
private SsdpDevicePublisher _Publisher; private SsdpDevicePublisher _Publisher;
private readonly ITimerFactory _timerFactory;
private readonly ISocketFactory _socketFactory; private readonly ISocketFactory _socketFactory;
private readonly IEnvironmentInfo _environmentInfo;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private ISsdpCommunicationsServer _communicationsServer; private ISsdpCommunicationsServer _communicationsServer;
@@ -76,8 +78,11 @@ namespace Emby.Dlna.Main
IDeviceDiscovery deviceDiscovery, IDeviceDiscovery deviceDiscovery,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
ISocketFactory socketFactory, ISocketFactory socketFactory,
ITimerFactory timerFactory,
IEnvironmentInfo environmentInfo,
INetworkManager networkManager, INetworkManager networkManager,
IUserViewManager userViewManager, IUserViewManager userViewManager,
IXmlReaderSettingsFactory xmlReaderSettingsFactory,
ITVSeriesManager tvSeriesManager) ITVSeriesManager tvSeriesManager)
{ {
_config = config; _config = config;
@@ -94,11 +99,12 @@ namespace Emby.Dlna.Main
_deviceDiscovery = deviceDiscovery; _deviceDiscovery = deviceDiscovery;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_socketFactory = socketFactory; _socketFactory = socketFactory;
_timerFactory = timerFactory;
_environmentInfo = environmentInfo;
_networkManager = networkManager; _networkManager = networkManager;
_logger = loggerFactory.CreateLogger("Dlna"); _logger = loggerFactory.CreateLogger("Dlna");
ContentDirectory = new ContentDirectory.ContentDirectory( ContentDirectory = new ContentDirectory.ContentDirectory(dlnaManager,
dlnaManager,
userDataManager, userDataManager,
imageProcessor, imageProcessor,
libraryManager, libraryManager,
@@ -110,17 +116,18 @@ namespace Emby.Dlna.Main
mediaSourceManager, mediaSourceManager,
userViewManager, userViewManager,
mediaEncoder, mediaEncoder,
xmlReaderSettingsFactory,
tvSeriesManager); tvSeriesManager);
ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient); ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient, xmlReaderSettingsFactory);
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config); MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config, xmlReaderSettingsFactory);
Current = this; Current = this;
} }
public async Task RunAsync() public void Run()
{ {
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); ((DlnaManager)_dlnaManager).InitProfiles();
ReloadComponents(); ReloadComponents();
@@ -166,10 +173,9 @@ namespace Emby.Dlna.Main
{ {
if (_communicationsServer == null) if (_communicationsServer == null)
{ {
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows || var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows;
OperatingSystem.Id == OperatingSystemId.Linux;
_communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding) _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{ {
IsShared = true IsShared = true
}; };
@@ -227,7 +233,7 @@ namespace Emby.Dlna.Main
try try
{ {
_Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost); _Publisher = new SsdpDevicePublisher(_communicationsServer, _timerFactory, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion);
_Publisher.LogFunction = LogMessage; _Publisher.LogFunction = LogMessage;
_Publisher.SupportPnpRootDevice = false; _Publisher.SupportPnpRootDevice = false;
@@ -243,21 +249,21 @@ namespace Emby.Dlna.Main
private async Task RegisterServerEndpoints() private async Task RegisterServerEndpoints()
{ {
var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false); var addresses = (await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false)).ToList();
var udn = CreateUuid(_appHost.SystemId); var udn = CreateUuid(_appHost.SystemId);
foreach (var address in addresses) foreach (var address in addresses)
{ {
if (address.AddressFamily == AddressFamily.InterNetworkV6) // TODO: Remove this condition on platforms that support it
{ //if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
// Not support IPv6 right now //{
continue; // 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.ToString());
var descriptorUri = "/dlna/" + udn + "/description.xml"; var descriptorUri = "/dlna/" + udn + "/description.xml";
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
@@ -266,8 +272,6 @@ namespace Emby.Dlna.Main
{ {
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,
SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
FriendlyName = "Jellyfin", FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin", Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server", ModelName = "Jellyfin Server",
@@ -308,7 +312,7 @@ namespace Emby.Dlna.Main
{ {
guid = text.GetMD5(); guid = text.GetMD5();
} }
return guid.ToString("N", CultureInfo.InvariantCulture); return guid.ToString("N");
} }
private void SetProperies(SsdpDevice device, string fullDeviceType) private void SetProperies(SsdpDevice device, string fullDeviceType)
@@ -349,7 +353,8 @@ namespace Emby.Dlna.Main
_userDataManager, _userDataManager,
_localization, _localization,
_mediaSourceManager, _mediaSourceManager,
_mediaEncoder); _mediaEncoder,
_timerFactory);
_manager.Start(); _manager.Start();
} }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Xml;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
@@ -35,8 +36,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
}; };
} }
public ControlHandler(IServerConfigurationManager config, ILogger logger) public ControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(config, logger, xmlReaderSettingsFactory)
: base(config, logger)
{ {
} }
} }

View File

@@ -1,6 +1,8 @@
using System.Collections.Generic;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Xml;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
@@ -8,14 +10,16 @@ namespace Emby.Dlna.MediaReceiverRegistrar
public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar
{ {
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory;
public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config) public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IXmlReaderSettingsFactory xmlReaderSettingsFactory)
: base(logger, httpClient) : base(logger, httpClient)
{ {
_config = config; _config = config;
XmlReaderSettingsFactory = xmlReaderSettingsFactory;
} }
public string GetServiceXml() public string GetServiceXml(IDictionary<string, string> headers)
{ {
return new MediaReceiverRegistrarXmlBuilder().GetXml(); return new MediaReceiverRegistrarXmlBuilder().GetXml();
} }
@@ -24,7 +28,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
{ {
return new ControlHandler( return new ControlHandler(
_config, _config,
Logger) Logger, XmlReaderSettingsFactory)
.ProcessControlRequest(request); .ProcessControlRequest(request);
} }
} }

View File

@@ -0,0 +1,9 @@
using System;
namespace Emby.Dlna.PlayTo
{
public class CurrentIdEventArgs : EventArgs
{
public string Id { get; set; }
}
}

View File

@@ -4,13 +4,13 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
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.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;
using MediaBrowser.Model.Threading;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
@@ -19,7 +19,7 @@ namespace Emby.Dlna.PlayTo
{ {
#region Fields & Properties #region Fields & Properties
private Timer _timer; private ITimer _timer;
public DeviceInfo Properties { get; set; } public DeviceInfo Properties { get; set; }
@@ -40,7 +40,12 @@ namespace Emby.Dlna.PlayTo
public TimeSpan? Duration { get; set; } public TimeSpan? Duration { get; set; }
public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0); private TimeSpan _position = TimeSpan.FromSeconds(0);
public TimeSpan Position
{
get => _position;
set => _position = value;
}
public TRANSPORTSTATE TransportState { get; private set; } public TRANSPORTSTATE TransportState { get; private set; }
@@ -56,20 +61,24 @@ namespace Emby.Dlna.PlayTo
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
public DateTime DateLastActivity { get; private set; }
public Action OnDeviceUnavailable { get; set; } public Action OnDeviceUnavailable { get; set; }
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config) private readonly ITimerFactory _timerFactory;
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config, ITimerFactory timerFactory)
{ {
Properties = deviceProperties; Properties = deviceProperties;
_httpClient = httpClient; _httpClient = httpClient;
_logger = logger; _logger = logger;
_config = config; _config = config;
_timerFactory = timerFactory;
} }
public void Start() public void Start()
{ {
_logger.LogDebug("Dlna Device.Start"); _logger.LogDebug("Dlna Device.Start");
_timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite); _timer = _timerFactory.Create(TimerCallback, null, 1000, Timeout.Infinite);
} }
private DateTime _lastVolumeRefresh; private DateTime _lastVolumeRefresh;
@@ -110,9 +119,7 @@ namespace Emby.Dlna.PlayTo
lock (_timerLock) lock (_timerLock)
{ {
if (_disposed) if (_disposed)
{
return; return;
}
_volumeRefreshActive = true; _volumeRefreshActive = true;
@@ -129,9 +136,7 @@ namespace Emby.Dlna.PlayTo
lock (_timerLock) lock (_timerLock)
{ {
if (_disposed) if (_disposed)
{
return; return;
}
_volumeRefreshActive = false; _volumeRefreshActive = false;
@@ -139,6 +144,11 @@ namespace Emby.Dlna.PlayTo
} }
} }
public void OnPlaybackStartedExternally()
{
RestartTimer(true);
}
#region Commanding #region Commanding
public Task VolumeDown(CancellationToken cancellationToken) public Task VolumeDown(CancellationToken cancellationToken)
@@ -323,9 +333,7 @@ namespace Emby.Dlna.PlayTo
private string CreateDidlMeta(string value) private string CreateDidlMeta(string value)
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{
return string.Empty; return string.Empty;
}
return DescriptionXmlBuilder.Escape(value); return DescriptionXmlBuilder.Escape(value);
} }
@@ -334,11 +342,10 @@ namespace Emby.Dlna.PlayTo
{ {
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Play"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Play");
if (command == null) if (command == null)
{
return Task.CompletedTask; return Task.CompletedTask;
}
var service = GetAvTransportService(); var service = GetAvTransportService();
if (service == null) if (service == null)
{ {
throw new InvalidOperationException("Unable to find service"); throw new InvalidOperationException("Unable to find service");
@@ -362,9 +369,7 @@ namespace Emby.Dlna.PlayTo
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
if (command == null) if (command == null)
{
return; return;
}
var service = GetAvTransportService(); var service = GetAvTransportService();
@@ -380,9 +385,7 @@ namespace Emby.Dlna.PlayTo
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
if (command == null) if (command == null)
{
return; return;
}
var service = GetAvTransportService(); var service = GetAvTransportService();
@@ -402,9 +405,7 @@ namespace Emby.Dlna.PlayTo
private async void TimerCallback(object sender) private async void TimerCallback(object sender)
{ {
if (_disposed) if (_disposed)
{
return; return;
}
try try
{ {
@@ -424,6 +425,8 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
DateLastActivity = DateTime.UtcNow;
if (transportState.HasValue) if (transportState.HasValue)
{ {
// If we're not playing anything no need to get additional data // If we're not playing anything no need to get additional data
@@ -502,9 +505,7 @@ namespace Emby.Dlna.PlayTo
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
if (command == null) if (command == null)
{
return; return;
}
var service = GetServiceRenderingControl(); var service = GetServiceRenderingControl();
@@ -517,17 +518,13 @@ namespace Emby.Dlna.PlayTo
.ConfigureAwait(false); .ConfigureAwait(false);
if (result == null || result.Document == null) if (result == null || result.Document == null)
{
return; return;
}
var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null); var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
var volumeValue = volume?.Value; var volumeValue = volume == null ? null : volume.Value;
if (string.IsNullOrWhiteSpace(volumeValue)) if (string.IsNullOrWhiteSpace(volumeValue))
{
return; return;
}
Volume = int.Parse(volumeValue, UsCulture); Volume = int.Parse(volumeValue, UsCulture);
@@ -548,9 +545,7 @@ namespace Emby.Dlna.PlayTo
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
if (command == null) if (command == null)
{
return; return;
}
var service = GetServiceRenderingControl(); var service = GetServiceRenderingControl();
@@ -565,44 +560,39 @@ namespace Emby.Dlna.PlayTo
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")).FirstOrDefault(i => i != null);
.Select(i => i.Element("CurrentMute")) var value = valueNode == null ? null : valueNode.Value;
.FirstOrDefault(i => i != null);
IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase); IsMuted = string.Equals(value, "1", StringComparison.OrdinalIgnoreCase);
} }
private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken) private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{ {
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
if (command == null) if (command == null)
{
return null; return null;
}
var service = GetAvTransportService(); var service = GetAvTransportService();
if (service == null) if (service == null)
{
return null; return null;
}
var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), false) var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), false)
.ConfigureAwait(false); .ConfigureAwait(false);
if (result == null || result.Document == null) if (result == null || result.Document == null)
{
return null; return null;
}
var transportState = var transportState =
result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null); result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
var transportStateValue = transportState == null ? null : transportState.Value; var transportStateValue = transportState == null ? null : transportState.Value;
if (transportStateValue != null if (transportStateValue != null)
&& Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state))
{ {
return state; if (Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state))
{
return state;
}
} }
return null; return null;
@@ -612,11 +602,10 @@ namespace Emby.Dlna.PlayTo
{ {
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
if (command == null) if (command == null)
{
return null; return null;
}
var service = GetAvTransportService(); var service = GetAvTransportService();
if (service == null) if (service == null)
{ {
throw new InvalidOperationException("Unable to find service"); throw new InvalidOperationException("Unable to find service");
@@ -628,9 +617,7 @@ namespace Emby.Dlna.PlayTo
.ConfigureAwait(false); .ConfigureAwait(false);
if (result == null || result.Document == null) if (result == null || result.Document == null)
{
return null; return null;
}
var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault(); var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault();
@@ -670,13 +657,11 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) private async Task<Tuple<bool, uBaseObject>> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{ {
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
if (command == null) if (command == null)
{ return new Tuple<bool, uBaseObject>(false, null);
return (false, null);
}
var service = GetAvTransportService(); var service = GetAvTransportService();
@@ -691,9 +676,7 @@ namespace Emby.Dlna.PlayTo
.ConfigureAwait(false); .ConfigureAwait(false);
if (result == null || result.Document == null) if (result == null || result.Document == null)
{ return new Tuple<bool, uBaseObject>(false, null);
return (false, null);
}
var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null); var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
var trackUri = trackUriElem == null ? null : trackUriElem.Value; var trackUri = trackUriElem == null ? null : trackUriElem.Value;
@@ -701,8 +684,8 @@ namespace Emby.Dlna.PlayTo
var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null); var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
var duration = durationElem == null ? null : durationElem.Value; var duration = durationElem == null ? null : durationElem.Value;
if (!string.IsNullOrWhiteSpace(duration) if (!string.IsNullOrWhiteSpace(duration) &&
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{ {
Duration = TimeSpan.Parse(duration, UsCulture); Duration = TimeSpan.Parse(duration, UsCulture);
} }
@@ -724,75 +707,43 @@ 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 new Tuple<bool, uBaseObject>(true, null);
} }
var trackString = (string)track; var trackString = (string)track;
if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{ {
return (true, null); return new Tuple<bool, uBaseObject>(true, null);
} }
XElement uPnpResponse = null; XElement uPnpResponse;
// Handle different variations sent back by devices
try try
{ {
uPnpResponse = ParseResponse(trackString); uPnpResponse = XElement.Parse(trackString);
} }
catch (Exception ex) catch (Exception)
{ {
_logger.LogError(ex, "Uncaught exception while parsing xml"); // first try to add a root node with a dlna namesapce
} try
{
if (uPnpResponse == null) uPnpResponse = XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + trackString + "</data>");
{ uPnpResponse = uPnpResponse.Descendants().First();
_logger.LogError("Failed to parse xml: \n {Xml}", trackString); }
return (true, null); catch (Exception ex)
{
_logger.LogError(ex, "Unable to parse xml {0}", trackString);
return new Tuple<bool, uBaseObject>(true, null);
}
} }
var e = uPnpResponse.Element(uPnpNamespaces.items); var e = uPnpResponse.Element(uPnpNamespaces.items);
var uTrack = CreateUBaseObject(e, trackUri); var uTrack = CreateUBaseObject(e, trackUri);
return (true, uTrack); return new Tuple<bool, uBaseObject>(true, uTrack);
}
private XElement ParseResponse(string xml)
{
// Handle different variations sent back by devices
try
{
return XElement.Parse(xml);
}
catch (XmlException)
{
}
// first try to add a root node with a dlna namesapce
try
{
return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>")
.Descendants()
.First();
}
catch (XmlException)
{
}
// some devices send back invalid xml
try
{
return XElement.Parse(xml.Replace("&", "&amp;"));
}
catch (XmlException)
{
}
return null;
} }
private static uBaseObject CreateUBaseObject(XElement container, string trackUri) private static uBaseObject CreateUBaseObject(XElement container, string trackUri)
@@ -850,9 +801,11 @@ namespace Emby.Dlna.PlayTo
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken) private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
{ {
if (AvCommands != null) var avCommands = AvCommands;
if (avCommands != null)
{ {
return AvCommands; return avCommands;
} }
if (_disposed) if (_disposed)
@@ -872,15 +825,18 @@ namespace Emby.Dlna.PlayTo
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
AvCommands = TransportCommands.Create(document); avCommands = TransportCommands.Create(document);
return AvCommands; AvCommands = avCommands;
return avCommands;
} }
private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken) private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken)
{ {
if (RendererCommands != null) var rendererCommands = RendererCommands;
if (rendererCommands != null)
{ {
return RendererCommands; return rendererCommands;
} }
if (_disposed) if (_disposed)
@@ -889,6 +845,7 @@ namespace Emby.Dlna.PlayTo
} }
var avService = GetServiceRenderingControl(); var avService = GetServiceRenderingControl();
if (avService == null) if (avService == null)
{ {
throw new ArgumentException("Device AvService is null"); throw new ArgumentException("Device AvService is null");
@@ -900,8 +857,9 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
RendererCommands = TransportCommands.Create(document); rendererCommands = TransportCommands.Create(document);
return RendererCommands; RendererCommands = rendererCommands;
return rendererCommands;
} }
private string NormalizeUrl(string baseUrl, string url) private string NormalizeUrl(string baseUrl, string url)
@@ -913,103 +871,85 @@ namespace Emby.Dlna.PlayTo
} }
if (!url.Contains("/")) if (!url.Contains("/"))
{
url = "/dmr/" + url; url = "/dmr/" + url;
}
if (!url.StartsWith("/")) if (!url.StartsWith("/"))
{
url = "/" + url; url = "/" + url;
}
return baseUrl + url; return baseUrl + url;
} }
private TransportCommands AvCommands { get; set; } private TransportCommands AvCommands
{
get;
set;
}
private TransportCommands RendererCommands { get; set; } private TransportCommands RendererCommands
{
get;
set;
}
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken) public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, ITimerFactory timerFactory, CancellationToken cancellationToken)
{ {
var ssdpHttpClient = new SsdpHttpClient(httpClient, config); var ssdpHttpClient = new SsdpHttpClient(httpClient, config);
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
var deviceProperties = new DeviceInfo();
var friendlyNames = new List<string>(); var friendlyNames = new List<string>();
var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault(); var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault();
if (name != null && !string.IsNullOrWhiteSpace(name.Value)) if (name != null && !string.IsNullOrWhiteSpace(name.Value))
{
friendlyNames.Add(name.Value); friendlyNames.Add(name.Value);
}
var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault(); var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault();
if (room != null && !string.IsNullOrWhiteSpace(room.Value)) if (room != null && !string.IsNullOrWhiteSpace(room.Value))
{
friendlyNames.Add(room.Value); friendlyNames.Add(room.Value);
}
var deviceProperties = new DeviceInfo() deviceProperties.Name = string.Join(" ", friendlyNames.ToArray());
{
Name = string.Join(" ", friendlyNames),
BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port)
};
var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault(); var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault();
if (model != null) if (model != null)
{
deviceProperties.ModelName = model.Value; deviceProperties.ModelName = model.Value;
}
var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault(); var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault();
if (modelNumber != null) if (modelNumber != null)
{
deviceProperties.ModelNumber = modelNumber.Value; deviceProperties.ModelNumber = modelNumber.Value;
}
var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault(); var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault();
if (uuid != null) if (uuid != null)
{
deviceProperties.UUID = uuid.Value; deviceProperties.UUID = uuid.Value;
}
var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault(); var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault();
if (manufacturer != null) if (manufacturer != null)
{
deviceProperties.Manufacturer = manufacturer.Value; deviceProperties.Manufacturer = manufacturer.Value;
}
var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault(); var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault();
if (manufacturerUrl != null) if (manufacturerUrl != null)
{
deviceProperties.ManufacturerUrl = manufacturerUrl.Value; deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
}
var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault(); var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault();
if (presentationUrl != null) if (presentationUrl != null)
{
deviceProperties.PresentationUrl = presentationUrl.Value; deviceProperties.PresentationUrl = presentationUrl.Value;
}
var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault(); var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault();
if (modelUrl != null) if (modelUrl != null)
{
deviceProperties.ModelUrl = modelUrl.Value; deviceProperties.ModelUrl = modelUrl.Value;
}
var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault(); var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault();
if (serialNumber != null) if (serialNumber != null)
{
deviceProperties.SerialNumber = serialNumber.Value; deviceProperties.SerialNumber = serialNumber.Value;
}
var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault(); var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault();
if (modelDescription != null) if (modelDescription != null)
{
deviceProperties.ModelDescription = modelDescription.Value; deviceProperties.ModelDescription = modelDescription.Value;
}
deviceProperties.BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port);
var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault(); var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault();
if (icon != null) if (icon != null)
{ {
deviceProperties.Icon = CreateIcon(icon); deviceProperties.Icon = CreateIcon(icon);
@@ -1018,15 +958,12 @@ namespace Emby.Dlna.PlayTo
foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList"))) foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList")))
{ {
if (services == null) if (services == null)
{
continue; continue;
}
var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service")); var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service"));
if (servicesList == null) if (servicesList == null)
{
continue; continue;
}
foreach (var element in servicesList) foreach (var element in servicesList)
{ {
@@ -1039,7 +976,9 @@ namespace Emby.Dlna.PlayTo
} }
} }
return new Device(deviceProperties, httpClient, logger, config); var device = new Device(deviceProperties, httpClient, logger, config, timerFactory);
return device;
} }
#endregion #endregion
@@ -1126,73 +1065,75 @@ namespace Emby.Dlna.PlayTo
private void OnPlaybackStart(uBaseObject mediaInfo) private void OnPlaybackStart(uBaseObject mediaInfo)
{ {
if (string.IsNullOrWhiteSpace(mediaInfo.Url)) if (PlaybackStart != null)
{ {
return; PlaybackStart.Invoke(this, new PlaybackStartEventArgs
{
MediaInfo = mediaInfo
});
} }
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
{
MediaInfo = mediaInfo
});
} }
private void OnPlaybackProgress(uBaseObject mediaInfo) private void OnPlaybackProgress(uBaseObject mediaInfo)
{ {
if (string.IsNullOrWhiteSpace(mediaInfo.Url)) var mediaUrl = mediaInfo.Url;
if (string.IsNullOrWhiteSpace(mediaUrl))
{ {
return; return;
} }
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs if (PlaybackProgress != null)
{ {
MediaInfo = mediaInfo PlaybackProgress.Invoke(this, new PlaybackProgressEventArgs
}); {
MediaInfo = mediaInfo
});
}
} }
private void OnPlaybackStop(uBaseObject mediaInfo) private void OnPlaybackStop(uBaseObject mediaInfo)
{ {
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs if (PlaybackStopped != null)
{ {
MediaInfo = mediaInfo PlaybackStopped.Invoke(this, new PlaybackStoppedEventArgs
}); {
MediaInfo = mediaInfo
});
}
} }
private void OnMediaChanged(uBaseObject old, uBaseObject newMedia) private void OnMediaChanged(uBaseObject old, uBaseObject newMedia)
{ {
MediaChanged?.Invoke(this, new MediaChangedEventArgs if (MediaChanged != null)
{ {
OldMediaInfo = old, MediaChanged.Invoke(this, new MediaChangedEventArgs
NewMediaInfo = newMedia {
}); OldMediaInfo = old,
NewMediaInfo = newMedia
});
}
} }
#region IDisposable #region IDisposable
bool _disposed; bool _disposed;
public void Dispose() public void Dispose()
{ {
Dispose(true); if (!_disposed)
GC.SuppressFinalize(this); {
_disposed = true;
DisposeTimer();
}
} }
protected virtual void Dispose(bool disposing) private void DisposeTimer()
{ {
if (_disposed) if (_timer != null)
{ {
return; _timer.Dispose();
_timer = null;
} }
if (disposing)
{
_timer?.Dispose();
}
_timer = null;
Properties = null;
_disposed = true;
} }
#endregion #endregion

View File

@@ -6,7 +6,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@@ -18,8 +17,8 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
@@ -43,43 +42,30 @@ namespace Emby.Dlna.PlayTo
private readonly IDeviceDiscovery _deviceDiscovery; private readonly IDeviceDiscovery _deviceDiscovery;
private readonly string _serverAddress; private readonly string _serverAddress;
private readonly string _accessToken; private readonly string _accessToken;
private readonly DateTime _creationTime;
public bool IsSessionActive => !_disposed && _device != null; public bool IsSessionActive => !_disposed && _device != null;
public bool SupportsMediaControl => IsSessionActive; public bool SupportsMediaControl => IsSessionActive;
public PlayToController( public PlayToController(SessionInfo session, ISessionManager sessionManager, ILibraryManager libraryManager, ILogger logger, IDlnaManager dlnaManager, IUserManager userManager, IImageProcessor imageProcessor, string serverAddress, string accessToken, IDeviceDiscovery deviceDiscovery, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IConfigurationManager config, IMediaEncoder mediaEncoder)
SessionInfo session,
ISessionManager sessionManager,
ILibraryManager libraryManager,
ILogger logger,
IDlnaManager dlnaManager,
IUserManager userManager,
IImageProcessor imageProcessor,
string serverAddress,
string accessToken,
IDeviceDiscovery deviceDiscovery,
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
IConfigurationManager config,
IMediaEncoder mediaEncoder)
{ {
_session = session; _session = session;
_sessionManager = sessionManager; _sessionManager = sessionManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_logger = logger;
_dlnaManager = dlnaManager; _dlnaManager = dlnaManager;
_userManager = userManager; _userManager = userManager;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_serverAddress = serverAddress; _serverAddress = serverAddress;
_accessToken = accessToken;
_deviceDiscovery = deviceDiscovery; _deviceDiscovery = deviceDiscovery;
_userDataManager = userDataManager; _userDataManager = userDataManager;
_localization = localization; _localization = localization;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_config = config; _config = config;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_accessToken = accessToken;
_logger = logger;
_creationTime = DateTime.UtcNow;
} }
public void Init(Device device) public void Init(Device device)
@@ -102,10 +88,9 @@ namespace Emby.Dlna.PlayTo
{ {
_sessionManager.ReportSessionEnded(_session.Id); _sessionManager.ReportSessionEnded(_session.Id);
} }
catch (Exception ex) catch
{ {
// Could throw if the session is already gone // Could throw if the session is already gone
_logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id);
} }
} }
@@ -113,14 +98,20 @@ namespace Emby.Dlna.PlayTo
{ {
var info = e.Argument; var info = e.Argument;
if (!_disposed info.Headers.TryGetValue("NTS", out string nts);
&& info.Headers.TryGetValue("USN", out string usn)
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty;
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
|| (info.Headers.TryGetValue("NT", out string nt) if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty;
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
if (usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 &&
!_disposed)
{ {
OnDeviceUnavailable(); if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 ||
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)
{
OnDeviceUnavailable();
}
} }
} }
@@ -383,7 +374,9 @@ namespace Emby.Dlna.PlayTo
return _device.IsPaused ? _device.SetPlay(CancellationToken.None) : _device.SetPause(CancellationToken.None); return _device.IsPaused ? _device.SetPlay(CancellationToken.None) : _device.SetPause(CancellationToken.None);
case PlaystateCommand.Seek: case PlaystateCommand.Seek:
return Seek(command.SeekPositionTicks ?? 0); {
return Seek(command.SeekPositionTicks ?? 0);
}
case PlaystateCommand.NextTrack: case PlaystateCommand.NextTrack:
return SetPlaylistIndex(_currentPlaylistIndex + 1); return SetPlaylistIndex(_currentPlaylistIndex + 1);
@@ -449,7 +442,8 @@ namespace Emby.Dlna.PlayTo
var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ?? var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ??
_dlnaManager.GetDefaultProfile(); _dlnaManager.GetDefaultProfile();
var mediaSources = item is IHasMediaSources var hasMediaSources = item as IHasMediaSources;
var mediaSources = hasMediaSources != null
? (_mediaSourceManager.GetStaticMediaSources(item, true, user)) ? (_mediaSourceManager.GetStaticMediaSources(item, true, user))
: new List<MediaSourceInfo>(); : new List<MediaSourceInfo>();
@@ -458,7 +452,7 @@ namespace Emby.Dlna.PlayTo
playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken)); playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken));
var itemXml = new DidlBuilder(profile, user, _imageProcessor, _serverAddress, _accessToken, _userDataManager, _localization, _mediaSourceManager, _logger, _mediaEncoder) var itemXml = new DidlBuilder(profile, user, _imageProcessor, _serverAddress, _accessToken, _userDataManager, _localization, _mediaSourceManager, _logger, _libraryManager, _mediaEncoder)
.GetItemDidl(_config.GetDlnaConfiguration(), item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo); .GetItemDidl(_config.GetDlnaConfiguration(), item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo);
playlistItem.Didl = itemXml; playlistItem.Didl = itemXml;
@@ -607,34 +601,22 @@ namespace Emby.Dlna.PlayTo
public void Dispose() public void Dispose()
{ {
Dispose(true); if (!_disposed)
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{ {
return; _disposed = true;
}
_device.PlaybackStart -= _device_PlaybackStart;
_device.PlaybackProgress -= _device_PlaybackProgress;
_device.PlaybackStopped -= _device_PlaybackStopped;
_device.MediaChanged -= _device_MediaChanged;
//_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
_device.OnDeviceUnavailable = null;
if (disposing)
{
_device.Dispose(); _device.Dispose();
} }
_device.PlaybackStart -= _device_PlaybackStart;
_device.PlaybackProgress -= _device_PlaybackProgress;
_device.PlaybackStopped -= _device_PlaybackStopped;
_device.MediaChanged -= _device_MediaChanged;
_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
_device.OnDeviceUnavailable = null;
_device = null;
_disposed = true;
} }
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
{ {
@@ -855,13 +837,13 @@ namespace Emby.Dlna.PlayTo
if (index == -1) return request; if (index == -1) return request;
var query = url.Substring(index + 1); var query = url.Substring(index + 1);
Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString()); QueryParamCollection values = MyHttpUtility.ParseQueryString(query);
request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId"); request.DeviceProfileId = values.Get("DeviceProfileId");
request.DeviceId = values.GetValueOrDefault("DeviceId"); request.DeviceId = values.Get("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId"); request.MediaSourceId = values.Get("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId"); request.LiveStreamId = values.Get("LiveStreamId");
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase); request.IsDirectStream = string.Equals("true", values.Get("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
@@ -875,9 +857,9 @@ namespace Emby.Dlna.PlayTo
} }
} }
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name) private static int? GetIntValue(QueryParamCollection values, string name)
{ {
var value = values.GetValueOrDefault(name); var value = values.Get(name);
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{ {
@@ -887,9 +869,9 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name) private static long GetLongValue(QueryParamCollection values, string name)
{ {
var value = values.GetValueOrDefault(name); var value = values.Get(name);
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{ {

View File

@@ -1,7 +1,5 @@
using System; using System;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@@ -16,7 +14,9 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using MediaBrowser.Model.Threading;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
@@ -39,12 +39,13 @@ namespace Emby.Dlna.PlayTo
private readonly IDeviceDiscovery _deviceDiscovery; private readonly IDeviceDiscovery _deviceDiscovery;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ITimerFactory _timerFactory;
private bool _disposed; private bool _disposed;
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, ITimerFactory timerFactory)
{ {
_logger = logger; _logger = logger;
_sessionManager = sessionManager; _sessionManager = sessionManager;
@@ -60,6 +61,7 @@ namespace Emby.Dlna.PlayTo
_localization = localization; _localization = localization;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_timerFactory = timerFactory;
} }
public void Start() public void Start()
@@ -90,6 +92,11 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
if (_sessionManager.Sessions.Any(i => usn.IndexOf(i.DeviceId, StringComparison.OrdinalIgnoreCase) != -1))
{
return;
}
var cancellationToken = _disposeCancellationTokenSource.Token; var cancellationToken = _disposeCancellationTokenSource.Token;
await _sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false); await _sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -101,11 +108,6 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
if (_sessionManager.Sessions.Any(i => usn.IndexOf(i.DeviceId, StringComparison.OrdinalIgnoreCase) != -1))
{
return;
}
await AddDevice(info, location, cancellationToken).ConfigureAwait(false); await AddDevice(info, location, cancellationToken).ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -142,7 +144,7 @@ namespace Emby.Dlna.PlayTo
return usn; return usn;
} }
return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture); return usn.GetMD5().ToString("N");
} }
private async Task AddDevice(UpnpDeviceInfo info, string location, CancellationToken cancellationToken) private async Task AddDevice(UpnpDeviceInfo info, string location, CancellationToken cancellationToken)
@@ -157,23 +159,25 @@ namespace Emby.Dlna.PlayTo
} }
else else
{ {
uuid = location.GetMD5().ToString("N", CultureInfo.InvariantCulture); uuid = location.GetMD5().ToString("N");
} }
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersion, uuid, null, uri.OriginalString, null); string deviceName = null;
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersion, uuid, deviceName, uri.OriginalString, null);
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault(); var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
if (controller == null) if (controller == null)
{ {
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false); var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, _timerFactory, cancellationToken).ConfigureAwait(false);
string deviceName = device.Properties.Name; deviceName = device.Properties.Name;
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
string serverAddress; string serverAddress;
if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IPAddress.Any) || info.LocalIpAddress.Equals(IPAddress.IPv6Any)) if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IpAddressInfo.Any) || info.LocalIpAddress.Equals(IpAddressInfo.IPv6Any))
{ {
serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
} }
@@ -182,6 +186,8 @@ namespace Emby.Dlna.PlayTo
serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress); serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
} }
string accessToken = null;
controller = new PlayToController(sessionInfo, controller = new PlayToController(sessionInfo,
_sessionManager, _sessionManager,
_libraryManager, _libraryManager,
@@ -190,7 +196,7 @@ namespace Emby.Dlna.PlayTo
_userManager, _userManager,
_imageProcessor, _imageProcessor,
serverAddress, serverAddress,
null, accessToken,
_deviceDiscovery, _deviceDiscovery,
_userDataManager, _userDataManager,
_localization, _localization,

View File

@@ -9,6 +9,8 @@ namespace Emby.Dlna.PlayTo
{ {
public class PlaylistItemFactory public class PlaylistItemFactory
{ {
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public PlaylistItem Create(Photo item, DeviceProfile profile) public PlaylistItem Create(Photo item, DeviceProfile profile)
{ {
var playlistItem = new PlaylistItem var playlistItem = new PlaylistItem

View File

@@ -16,8 +16,6 @@ namespace Emby.Dlna.PlayTo
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50"; private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
private const string FriendlyName = "Jellyfin"; private const string FriendlyName = "Jellyfin";
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
@@ -27,8 +25,7 @@ namespace Emby.Dlna.PlayTo
_config = config; _config = config;
} }
public async Task<XDocument> SendCommandAsync( public async Task<XDocument> SendCommandAsync(string baseUrl,
string baseUrl,
DeviceService service, DeviceService service,
string command, string command,
string postData, string postData,
@@ -37,21 +34,16 @@ namespace Emby.Dlna.PlayTo
{ {
var cancellationToken = CancellationToken.None; var cancellationToken = CancellationToken.None;
var url = NormalizeServiceUrl(baseUrl, service.ControlUrl); using (var response = await PostSoapDataAsync(NormalizeServiceUrl(baseUrl, service.ControlUrl), "\"" + service.ServiceType + "#" + command + "\"", postData, header, logRequest, cancellationToken)
using (var response = await PostSoapDataAsync(
url,
$"\"{service.ServiceType}#{command}\"",
postData,
header,
logRequest,
cancellationToken)
.ConfigureAwait(false)) .ConfigureAwait(false))
using (var stream = response.Content)
using (var reader = new StreamReader(stream, Encoding.UTF8))
{ {
return XDocument.Parse( using (var stream = response.Content)
await reader.ReadToEndAsync().ConfigureAwait(false), {
LoadOptions.PreserveWhitespace); using (var reader = new StreamReader(stream, Encoding.UTF8))
{
return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
}
}
} }
} }
@@ -69,8 +61,9 @@ namespace Emby.Dlna.PlayTo
return baseUrl + serviceUrl; return baseUrl + serviceUrl;
} }
public async Task SubscribeAsync( private readonly CultureInfo _usCulture = new CultureInfo("en-US");
string url,
public async Task SubscribeAsync(string url,
string ip, string ip,
int port, int port,
string localIp, string localIp,
@@ -83,6 +76,9 @@ namespace Emby.Dlna.PlayTo
UserAgent = USERAGENT, UserAgent = USERAGENT,
LogErrorResponseBody = true, LogErrorResponseBody = true,
BufferContent = false, BufferContent = false,
// The periodic requests may keep some devices awake
LogRequestAsDebug = true
}; };
options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture); options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture);
@@ -105,41 +101,47 @@ namespace Emby.Dlna.PlayTo
LogErrorResponseBody = true, LogErrorResponseBody = true,
BufferContent = false, BufferContent = false,
// The periodic requests may keep some devices awake
LogRequestAsDebug = true,
CancellationToken = cancellationToken CancellationToken = cancellationToken
}; };
options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName; options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)) using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false))
using (var stream = response.Content)
using (var reader = new StreamReader(stream, Encoding.UTF8))
{ {
return XDocument.Parse( using (var stream = response.Content)
await reader.ReadToEndAsync().ConfigureAwait(false), {
LoadOptions.PreserveWhitespace); using (var reader = new StreamReader(stream, Encoding.UTF8))
{
return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
}
}
} }
} }
private Task<HttpResponseInfo> PostSoapDataAsync( private Task<HttpResponseInfo> PostSoapDataAsync(string url,
string url,
string soapAction, string soapAction,
string postData, string postData,
string header, string header,
bool logRequest, bool logRequest,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (soapAction[0] != '\"') if (!soapAction.StartsWith("\""))
{ soapAction = "\"" + soapAction + "\"";
soapAction = $"\"{soapAction}\"";
}
var options = new HttpRequestOptions var options = new HttpRequestOptions
{ {
Url = url, Url = url,
UserAgent = USERAGENT, UserAgent = USERAGENT,
LogRequest = logRequest || _config.GetDlnaConfiguration().EnableDebugLog,
LogErrorResponseBody = true, LogErrorResponseBody = true,
BufferContent = false, BufferContent = false,
// The periodic requests may keep some devices awake
LogRequestAsDebug = true,
CancellationToken = cancellationToken CancellationToken = cancellationToken
}; };
@@ -153,6 +155,7 @@ namespace Emby.Dlna.PlayTo
} }
options.RequestContentType = "text/xml"; options.RequestContentType = "text/xml";
options.AppendCharsetToMimeType = true;
options.RequestContent = postData; options.RequestContent = postData;
return _httpClient.Post(options); return _httpClient.Post(options);

View File

@@ -107,18 +107,12 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList) foreach (var arg in action.ArgumentList)
{ {
if (arg.Direction == "out") if (arg.Direction == "out")
{
continue; continue;
}
if (arg.Name == "InstanceID") if (arg.Name == "InstanceID")
{
stateString += BuildArgumentXml(arg, "0"); stateString += BuildArgumentXml(arg, "0");
}
else else
{
stateString += BuildArgumentXml(arg, null); stateString += BuildArgumentXml(arg, null);
}
} }
return string.Format(CommandBase, action.Name, xmlNamespace, stateString); return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
@@ -131,18 +125,11 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList) foreach (var arg in action.ArgumentList)
{ {
if (arg.Direction == "out") if (arg.Direction == "out")
{
continue; continue;
}
if (arg.Name == "InstanceID") if (arg.Name == "InstanceID")
{
stateString += BuildArgumentXml(arg, "0"); stateString += BuildArgumentXml(arg, "0");
}
else else
{
stateString += BuildArgumentXml(arg, value.ToString(), commandParameter); stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
}
} }
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
@@ -155,17 +142,11 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList) foreach (var arg in action.ArgumentList)
{ {
if (arg.Name == "InstanceID") if (arg.Name == "InstanceID")
{
stateString += BuildArgumentXml(arg, "0"); stateString += BuildArgumentXml(arg, "0");
}
else if (dictionary.ContainsKey(arg.Name)) else if (dictionary.ContainsKey(arg.Name))
{
stateString += BuildArgumentXml(arg, dictionary[arg.Name]); stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
}
else else
{
stateString += BuildArgumentXml(arg, value.ToString()); stateString += BuildArgumentXml(arg, value.ToString());
}
} }
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);

View File

@@ -0,0 +1,9 @@
using System;
namespace Emby.Dlna.PlayTo
{
public class TransportStateEventArgs : EventArgs
{
public TRANSPORTSTATE State { get; set; }
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Emby.Dlna.PlayTo
{
public class uParser
{
public static IList<uBaseObject> ParseBrowseXml(XDocument doc)
{
if (doc == null)
{
throw new ArgumentException("doc");
}
var list = new List<uBaseObject>();
var document = doc.Document;
if (document == null)
return list;
var item = (from result in document.Descendants("Result") select result).FirstOrDefault();
if (item == null)
return list;
var uPnpResponse = XElement.Parse((string)item);
var uObjects = from container in uPnpResponse.Elements(uPnpNamespaces.containers)
select new uParserObject { Element = container };
var uObjects2 = from container in uPnpResponse.Elements(uPnpNamespaces.items)
select new uParserObject { Element = container };
list.AddRange(uObjects.Concat(uObjects2).Select(CreateObjectFromXML).Where(uObject => uObject != null));
return list;
}
public static uBaseObject CreateObjectFromXML(uParserObject uItem)
{
return UpnpContainer.Create(uItem.Element);
}
}
}

View File

@@ -0,0 +1,9 @@
using System.Xml.Linq;
namespace Emby.Dlna.PlayTo
{
public class uParserObject
{
public XElement Element { get; set; }
}
}

View File

@@ -9,7 +9,7 @@ namespace Emby.Dlna.Profiles
{ {
Name = "Dish Hopper-Joey"; Name = "Dish Hopper-Joey";
ProtocolInfo = "http-get:*:video/mp2t:*,http-get:*:video/mpeg:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*"; ProtocolInfo = "http-get:*:video/mp2t:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*";
Identification = new DeviceIdentification Identification = new DeviceIdentification
{ {

View File

@@ -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/mp2t:http-get:*:video/mpeg:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*</ProtocolInfo> <ProtocolInfo>http-get:*:video/mp2t:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*</ProtocolInfo>
<TimelineOffsetSeconds>0</TimelineOffsetSeconds> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

View File

@@ -8,8 +8,8 @@ using System.Resources;
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyCompany("Jellyfin Project")]
[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyProduct("Jellyfin: The Free Software Media System")]
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")] [assembly: NeutralResourcesLanguage("en")]

View File

@@ -107,19 +107,19 @@ namespace Emby.Dlna.Server
'&' '&'
}; };
private static readonly string[] s_escapeStringPairs = new[] private static readonly string[] s_escapeStringPairs = new string[]
{ {
"<", "<",
"&lt;", "&lt;",
">", ">",
"&gt;", "&gt;",
"\"", "\"",
"&quot;", "&quot;",
"'", "'",
"&apos;", "&apos;",
"&", "&",
"&amp;" "&amp;"
}; };
private static string GetEscapeSequence(char c) private static string GetEscapeSequence(char c)
{ {
@@ -133,7 +133,7 @@ namespace Emby.Dlna.Server
return result; return result;
} }
} }
return c.ToString(CultureInfo.InvariantCulture); return c.ToString();
} }
/// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary> /// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
@@ -145,7 +145,6 @@ namespace Emby.Dlna.Server
{ {
return null; return null;
} }
StringBuilder stringBuilder = null; StringBuilder stringBuilder = null;
int length = str.Length; int length = str.Length;
int num = 0; int num = 0;
@@ -231,9 +230,9 @@ namespace Emby.Dlna.Server
var serverName = new string(characters); var serverName = new string(characters);
var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase); var name = (_profile.FriendlyName ?? string.Empty).Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
return name ?? string.Empty; return name;
} }
private void AppendIconList(StringBuilder builder) private void AppendIconList(StringBuilder builder)
@@ -296,62 +295,65 @@ namespace Emby.Dlna.Server
} }
private IEnumerable<DeviceIcon> GetIcons() private IEnumerable<DeviceIcon> GetIcons()
=> new[] {
var list = new List<DeviceIcon>();
list.Add(new DeviceIcon
{ {
new DeviceIcon MimeType = "image/png",
{ Depth = "24",
MimeType = "image/png", Width = 240,
Depth = "24", Height = 240,
Width = 240, Url = "icons/logo240.png"
Height = 240, });
Url = "icons/logo240.png"
},
new DeviceIcon list.Add(new DeviceIcon
{ {
MimeType = "image/jpeg", MimeType = "image/jpeg",
Depth = "24", Depth = "24",
Width = 240, Width = 240,
Height = 240, Height = 240,
Url = "icons/logo240.jpg" Url = "icons/logo240.jpg"
}, });
new DeviceIcon list.Add(new DeviceIcon
{ {
MimeType = "image/png", MimeType = "image/png",
Depth = "24", Depth = "24",
Width = 120, Width = 120,
Height = 120, Height = 120,
Url = "icons/logo120.png" Url = "icons/logo120.png"
}, });
new DeviceIcon list.Add(new DeviceIcon
{ {
MimeType = "image/jpeg", MimeType = "image/jpeg",
Depth = "24", Depth = "24",
Width = 120, Width = 120,
Height = 120, Height = 120,
Url = "icons/logo120.jpg" Url = "icons/logo120.jpg"
}, });
new DeviceIcon list.Add(new DeviceIcon
{ {
MimeType = "image/png", MimeType = "image/png",
Depth = "24", Depth = "24",
Width = 48, Width = 48,
Height = 48, Height = 48,
Url = "icons/logo48.png" Url = "icons/logo48.png"
}, });
new DeviceIcon list.Add(new DeviceIcon
{ {
MimeType = "image/jpeg", MimeType = "image/jpeg",
Depth = "24", Depth = "24",
Width = 48, Width = 48,
Height = 48, Height = 48,
Url = "icons/logo48.jpg" Url = "icons/logo48.jpg"
} });
};
return list;
}
private IEnumerable<DeviceService> GetServices() private IEnumerable<DeviceService> GetServices()
{ {

View File

@@ -7,6 +7,7 @@ using System.Xml;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Xml;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Service namespace Emby.Dlna.Service
@@ -17,11 +18,13 @@ namespace Emby.Dlna.Service
protected readonly IServerConfigurationManager Config; protected readonly IServerConfigurationManager Config;
protected readonly ILogger _logger; protected readonly ILogger _logger;
protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory;
protected BaseControlHandler(IServerConfigurationManager config, ILogger logger) protected BaseControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory)
{ {
Config = config; Config = config;
_logger = logger; _logger = logger;
XmlReaderSettingsFactory = xmlReaderSettingsFactory;
} }
public ControlResponse ProcessControlRequest(ControlRequest request) public ControlResponse ProcessControlRequest(ControlRequest request)
@@ -58,13 +61,11 @@ namespace Emby.Dlna.Service
using (var streamReader = new StreamReader(request.InputXml)) using (var streamReader = new StreamReader(request.InputXml))
{ {
var readerSettings = new XmlReaderSettings() var readerSettings = XmlReaderSettingsFactory.Create(false);
{
ValidationType = ValidationType.None, readerSettings.CheckCharacters = false;
CheckCharacters = false, readerSettings.IgnoreProcessingInstructions = true;
IgnoreProcessingInstructions = true, readerSettings.IgnoreComments = true;
IgnoreComments = true
};
using (var reader = XmlReader.Create(streamReader, readerSettings)) using (var reader = XmlReader.Create(streamReader, readerSettings))
{ {

View File

@@ -5,6 +5,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Threading;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Rssdp; using Rssdp;
using Rssdp.Infrastructure; using Rssdp.Infrastructure;
@@ -47,17 +48,20 @@ namespace Emby.Dlna.Ssdp
private SsdpDeviceLocator _deviceLocator; private SsdpDeviceLocator _deviceLocator;
private readonly ITimerFactory _timerFactory;
private readonly ISocketFactory _socketFactory; private readonly ISocketFactory _socketFactory;
private ISsdpCommunicationsServer _commsServer; private ISsdpCommunicationsServer _commsServer;
public DeviceDiscovery( public DeviceDiscovery(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IServerConfigurationManager config, IServerConfigurationManager config,
ISocketFactory socketFactory) ISocketFactory socketFactory,
ITimerFactory timerFactory)
{ {
_logger = loggerFactory.CreateLogger(nameof(DeviceDiscovery)); _logger = loggerFactory.CreateLogger(nameof(DeviceDiscovery));
_config = config; _config = config;
_socketFactory = socketFactory; _socketFactory = socketFactory;
_timerFactory = timerFactory;
} }
// Call this method from somewhere in your code to start the search. // Call this method from somewhere in your code to start the search.
@@ -74,7 +78,7 @@ namespace Emby.Dlna.Ssdp
{ {
if (_listenerCount > 0 && _deviceLocator == null) if (_listenerCount > 0 && _deviceLocator == null)
{ {
_deviceLocator = new SsdpDeviceLocator(_commsServer); _deviceLocator = new SsdpDeviceLocator(_commsServer, _timerFactory);
// (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

View File

@@ -3,10 +3,14 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.0" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.0" />
<PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
@@ -17,9 +21,4 @@
<Compile Include="..\SharedVersion.cs" /> <Compile Include="..\SharedVersion.cs" />
</ItemGroup> </ItemGroup>
<PropertyGroup>
<!-- We need at least C# 7.1 for the "default literal" feature-->
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project> </Project>

View File

@@ -18,51 +18,47 @@ using MediaBrowser.Model.Extensions;
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 SkiaSharp;
namespace Emby.Drawing namespace Emby.Drawing
{ {
/// <summary> /// <summary>
/// Class ImageProcessor. /// Class ImageProcessor
/// </summary> /// </summary>
public class ImageProcessor : IImageProcessor, IDisposable public class ImageProcessor : IImageProcessor, IDisposable
{ {
// Increment this when there's a change requiring caches to be invalidated /// <summary>
private const string Version = "3"; /// The us culture
/// </summary>
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
private static readonly HashSet<string> _transparentImageTypes /// <summary>
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; /// Gets the list of currently registered image processors
/// Image processors are specialized metadata providers that run after the normal ones
/// </summary>
/// <value>The image enhancers.</value>
public IImageEnhancer[] ImageEnhancers { get; private set; }
/// <summary> /// <summary>
/// The _logger /// The _logger
/// </summary> /// </summary>
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths; private readonly IServerApplicationPaths _appPaths;
private IImageEncoder _imageEncoder; private IImageEncoder _imageEncoder;
private readonly Func<ILibraryManager> _libraryManager; private readonly Func<ILibraryManager> _libraryManager;
private readonly Func<IMediaEncoder> _mediaEncoder; private readonly Func<IMediaEncoder> _mediaEncoder;
private readonly Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>();
private bool _disposed = false;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="appPaths"></param>
/// <param name="fileSystem"></param>
/// <param name="imageEncoder"></param>
/// <param name="libraryManager"></param>
/// <param name="mediaEncoder"></param>
public ImageProcessor( public ImageProcessor(
ILogger<ImageProcessor> logger, ILoggerFactory loggerFactory,
IServerApplicationPaths appPaths, IServerApplicationPaths appPaths,
IFileSystem fileSystem, IFileSystem fileSystem,
IImageEncoder imageEncoder, IImageEncoder imageEncoder,
Func<ILibraryManager> libraryManager, Func<ILibraryManager> libraryManager,
Func<IMediaEncoder> mediaEncoder) Func<IMediaEncoder> mediaEncoder)
{ {
_logger = logger; _logger = loggerFactory.CreateLogger(nameof(ImageProcessor));
_fileSystem = fileSystem; _fileSystem = fileSystem;
_imageEncoder = imageEncoder; _imageEncoder = imageEncoder;
_libraryManager = libraryManager; _libraryManager = libraryManager;
@@ -70,17 +66,26 @@ namespace Emby.Drawing
_appPaths = appPaths; _appPaths = appPaths;
ImageEnhancers = Array.Empty<IImageEnhancer>(); ImageEnhancers = Array.Empty<IImageEnhancer>();
ImageHelper.ImageProcessor = this; ImageHelper.ImageProcessor = this;
} }
private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); public IImageEncoder ImageEncoder
{
get => _imageEncoder;
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images"); _imageEncoder = value;
}
}
/// <inheritdoc /> public string[] SupportedInputFormats =>
public IReadOnlyCollection<string> SupportedInputFormats => new string[]
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ {
"tiff", "tiff",
"tif", "tif",
@@ -111,20 +116,18 @@ namespace Emby.Drawing
"wbmp" "wbmp"
}; };
/// <inheritdoc />
public IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; }
/// <inheritdoc />
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
/// <inheritdoc /> private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
public IImageEncoder ImageEncoder
private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images");
public void AddParts(IEnumerable<IImageEnhancer> enhancers)
{ {
get => _imageEncoder; ImageEnhancers = enhancers.ToArray();
set => _imageEncoder = value ?? throw new ArgumentNullException(nameof(value));
} }
/// <inheritdoc />
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
{ {
var file = await ProcessImage(options).ConfigureAwait(false); var file = await ProcessImage(options).ConfigureAwait(false);
@@ -135,15 +138,15 @@ namespace Emby.Drawing
} }
} }
/// <inheritdoc /> public ImageFormat[] GetSupportedImageOutputFormats()
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats() {
=> _imageEncoder.SupportedOutputFormats; return _imageEncoder.SupportedOutputFormats;
}
/// <inheritdoc /> private static readonly string[] TransparentImageTypes = new string[] { ".png", ".webp", ".gif" };
public bool SupportsTransparency(string path) public bool SupportsTransparency(string path)
=> _transparentImageTypes.Contains(Path.GetExtension(path)); => TransparentImageTypes.Contains(Path.GetExtension(path).ToLower());
/// <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) if (options == null)
@@ -165,10 +168,10 @@ namespace Emby.Drawing
string originalImagePath = originalImage.Path; string originalImagePath = originalImage.Path;
DateTime dateModified = originalImage.DateModified; DateTime dateModified = originalImage.DateModified;
ImageDimensions? originalImageSize = null; ImageSize? originalImageSize = null;
if (originalImage.Width > 0 && originalImage.Height > 0) if (originalImage.Width > 0 && originalImage.Height > 0)
{ {
originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height); originalImageSize = new ImageSize(originalImage.Width, originalImage.Height);
} }
if (!_imageEncoder.SupportsImageEncoding) if (!_imageEncoder.SupportsImageEncoding)
@@ -178,16 +181,10 @@ namespace Emby.Drawing
var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false); var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
originalImagePath = supportedImageInfo.path; originalImagePath = supportedImageInfo.path;
if (!File.Exists(originalImagePath))
{
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
dateModified = supportedImageInfo.dateModified; dateModified = supportedImageInfo.dateModified;
bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath)); bool requiresTransparency = TransparentImageTypes.Contains(Path.GetExtension(originalImagePath));
if (options.Enhancers.Count > 0) if (options.Enhancers.Length > 0)
{ {
if (item == null) if (item == null)
{ {
@@ -234,7 +231,7 @@ namespace Emby.Drawing
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
} }
ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null); ImageSize newSize = ImageHelper.GetNewImageSize(options, null);
int quality = options.Quality; int quality = options.Quality;
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency); ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
@@ -248,7 +245,7 @@ namespace Emby.Drawing
try try
{ {
if (!File.Exists(cacheFilePath)) if (!_fileSystem.FileExists(cacheFilePath))
{ {
if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath)) if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
{ {
@@ -265,10 +262,21 @@ namespace Emby.Drawing
return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
} }
catch (ArgumentOutOfRangeException ex)
{
// Decoder failed to decode it
#if DEBUG
_logger.LogError(ex, "Error encoding image");
#endif
// Just spit out the original file if all the options are default
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
catch (Exception ex) catch (Exception ex)
{ {
// If it fails for whatever reason, return the original image // If it fails for whatever reason, return the original image
_logger.LogError(ex, "Error encoding image"); _logger.LogError(ex, "Error encoding image");
// Just spit out the original file if all the options are default
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
} }
finally finally
@@ -277,7 +285,7 @@ namespace Emby.Drawing
} }
} }
private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency) private ImageFormat GetOutputFormat(ImageFormat[] clientSupportedFormats, bool requiresTransparency)
{ {
var serverFormats = GetSupportedImageOutputFormats(); var serverFormats = GetSupportedImageOutputFormats();
@@ -318,10 +326,15 @@ namespace Emby.Drawing
} }
} }
/// <summary>
/// Increment this when there's a change requiring caches to be invalidated
/// </summary>
private const string Version = "3";
/// <summary> /// <summary>
/// Gets the cache file path based on a set of parameters /// Gets the cache file path based on a set of parameters
/// </summary> /// </summary>
private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer) private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
{ {
var filename = originalPath var filename = originalPath
+ "width=" + outputSize.Width + "width=" + outputSize.Width
@@ -362,30 +375,29 @@ namespace Emby.Drawing
filename += "v=" + Version; filename += "v=" + Version;
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant()); return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLower());
} }
/// <inheritdoc /> public ImageSize GetImageSize(BaseItem item, ItemImageInfo info)
public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info) => GetImageSize(item, info, true);
=> GetImageDimensions(item, info, true);
/// <inheritdoc /> public ImageSize GetImageSize(BaseItem item, ItemImageInfo info, bool updateItem)
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;
if (height > 0 && width > 0) if (height > 0 && width > 0)
{ {
return new ImageDimensions(width, height); return new ImageSize(width, height);
} }
string path = info.Path; string path = info.Path;
_logger.LogInformation("Getting image size for item {ItemType} {Path}", item.GetType().Name, path); _logger.LogInformation("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
ImageDimensions size = GetImageDimensions(path); var size = GetImageSize(path);
info.Width = size.Width;
info.Height = size.Height; info.Height = Convert.ToInt32(size.Height);
info.Width = Convert.ToInt32(size.Width);
if (updateItem) if (updateItem)
{ {
@@ -395,19 +407,38 @@ namespace Emby.Drawing
return size; return size;
} }
/// <inheritdoc /> /// <summary>
public ImageDimensions GetImageDimensions(string path) /// Gets the size of the image.
=> _imageEncoder.GetImageSize(path); /// </summary>
public ImageSize GetImageSize(string path)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
}
/// <inheritdoc /> using (var s = new SKFileStream(path))
using (var codec = SKCodec.Create(s))
{
var info = codec.Info;
return new ImageSize(info.Width, info.Height);
}
}
/// <summary>
/// Gets the image cache tag.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="image">The image.</param>
/// <returns>Guid.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public string GetImageCacheTag(BaseItem item, ItemImageInfo image) public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
{ {
var supportedEnhancers = GetSupportedEnhancers(item, image.Type).ToArray(); var supportedEnhancers = GetSupportedEnhancers(item, image.Type);
return GetImageCacheTag(item, image, supportedEnhancers); return GetImageCacheTag(item, image, supportedEnhancers);
} }
/// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
{ {
try try
@@ -425,24 +456,31 @@ namespace Emby.Drawing
} }
} }
/// <inheritdoc /> /// <summary>
public string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers) /// Gets the image cache tag.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="image">The image.</param>
/// <param name="imageEnhancers">The image enhancers.</param>
/// <returns>Guid.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers)
{ {
string originalImagePath = image.Path; string originalImagePath = image.Path;
DateTime dateModified = image.DateModified; DateTime dateModified = image.DateModified;
ImageType imageType = image.Type; ImageType imageType = image.Type;
// Optimization // Optimization
if (imageEnhancers.Count == 0) if (imageEnhancers.Length == 0)
{ {
return (originalImagePath + dateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); return (originalImagePath + dateModified.Ticks).GetMD5().ToString("N");
} }
// Cache name is created with supported enhancers combined with the last config change so we pick up new config changes // Cache name is created with supported enhancers combined with the last config change so we pick up new config changes
var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList(); var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList();
cacheKeys.Add(originalImagePath + dateModified.Ticks); cacheKeys.Add(originalImagePath + dateModified.Ticks);
return string.Join("|", cacheKeys).GetMD5().ToString("N", CultureInfo.InvariantCulture); return string.Join("|", cacheKeys).GetMD5().ToString("N");
} }
private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
@@ -457,11 +495,11 @@ namespace Emby.Drawing
return (originalImagePath, dateModified); return (originalImagePath, dateModified);
} }
if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat, StringComparer.OrdinalIgnoreCase))
{ {
try try
{ {
string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); string filename = (originalImagePath + dateModified.Ticks.ToString(UsCulture)).GetMD5().ToString("N");
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);
@@ -488,10 +526,16 @@ namespace Emby.Drawing
return (originalImagePath, dateModified); return (originalImagePath, dateModified);
} }
/// <inheritdoc /> /// <summary>
/// Gets the enhanced image.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="imageType">Type of the image.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns>Task{System.String}.</returns>
public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex) public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex)
{ {
var enhancers = GetSupportedEnhancers(item, imageType).ToArray(); var enhancers = GetSupportedEnhancers(item, imageType);
ItemImageInfo imageInfo = item.GetImageInfo(imageType, imageIndex); ItemImageInfo imageInfo = item.GetImageInfo(imageType, imageIndex);
@@ -507,7 +551,7 @@ namespace Emby.Drawing
bool inputImageSupportsTransparency, bool inputImageSupportsTransparency,
BaseItem item, BaseItem item,
int imageIndex, int imageIndex,
IReadOnlyCollection<IImageEnhancer> enhancers, IImageEnhancer[] enhancers,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var originalImagePath = image.Path; var originalImagePath = image.Path;
@@ -548,7 +592,6 @@ namespace Emby.Drawing
/// <param name="imageIndex">Index of the image.</param> /// <param name="imageIndex">Index of the image.</param>
/// <param name="supportedEnhancers">The supported enhancers.</param> /// <param name="supportedEnhancers">The supported enhancers.</param>
/// <param name="cacheGuid">The cache unique identifier.</param> /// <param name="cacheGuid">The cache unique identifier.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;System.String&gt;.</returns> /// <returns>Task&lt;System.String&gt;.</returns>
/// <exception cref="ArgumentNullException"> /// <exception cref="ArgumentNullException">
/// originalImagePath /// originalImagePath
@@ -560,9 +603,9 @@ namespace Emby.Drawing
BaseItem item, BaseItem item,
ImageType imageType, ImageType imageType,
int imageIndex, int imageIndex,
IReadOnlyCollection<IImageEnhancer> supportedEnhancers, IImageEnhancer[] supportedEnhancers,
string cacheGuid, string cacheGuid,
CancellationToken cancellationToken = default) CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(originalImagePath)) if (string.IsNullOrEmpty(originalImagePath))
{ {
@@ -597,12 +640,12 @@ namespace Emby.Drawing
try try
{ {
// Check again in case of contention // Check again in case of contention
if (File.Exists(enhancedImagePath)) if (_fileSystem.FileExists(enhancedImagePath))
{ {
return (enhancedImagePath, treatmentRequiresTransparency); return (enhancedImagePath, treatmentRequiresTransparency);
} }
Directory.CreateDirectory(Path.GetDirectoryName(enhancedImagePath)); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(enhancedImagePath));
await ExecuteImageEnhancers(supportedEnhancers, originalImagePath, enhancedImagePath, item, imageType, imageIndex).ConfigureAwait(false); await ExecuteImageEnhancers(supportedEnhancers, originalImagePath, enhancedImagePath, item, imageType, imageIndex).ConfigureAwait(false);
@@ -656,7 +699,6 @@ namespace Emby.Drawing
{ {
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
} }
if (string.IsNullOrEmpty(uniqueName)) if (string.IsNullOrEmpty(uniqueName))
{ {
throw new ArgumentNullException(nameof(uniqueName)); throw new ArgumentNullException(nameof(uniqueName));
@@ -699,7 +741,6 @@ namespace Emby.Drawing
return Path.Combine(path, prefix, filename); return Path.Combine(path, prefix, filename);
} }
/// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options) public void CreateImageCollage(ImageCollageOptions options)
{ {
_logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath); _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
@@ -709,25 +750,38 @@ namespace Emby.Drawing
_logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath); _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
} }
/// <inheritdoc /> public IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType)
public IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType)
{ {
List<IImageEnhancer> list = null;
foreach (var i in ImageEnhancers) foreach (var i in ImageEnhancers)
{ {
if (i.Supports(item, imageType)) try
{ {
yield return i; if (i.Supports(item, imageType))
{
if (list == null)
{
list = new List<IImageEnhancer>();
}
list.Add(i);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in image enhancer: {0}", i.GetType().Name);
} }
} }
return list == null ? Array.Empty<IImageEnhancer>() : list.ToArray();
} }
private Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>();
private class LockInfo private class LockInfo
{ {
public SemaphoreSlim Lock = new SemaphoreSlim(1, 1); public SemaphoreSlim Lock = new SemaphoreSlim(1, 1);
public int Count = 1; public int Count = 1;
} }
private LockInfo GetLock(string key) private LockInfo GetLock(string key)
{ {
lock (_locks) lock (_locks)
@@ -760,7 +814,7 @@ namespace Emby.Drawing
} }
} }
/// <inheritdoc /> private bool _disposed;
public void Dispose() public void Dispose()
{ {
_disposed = true; _disposed = true;

View File

@@ -1,46 +1,45 @@
using System; using System;
using System.Collections.Generic;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
namespace Emby.Drawing namespace Emby.Drawing
{ {
/// <summary>
/// A fallback implementation of <see cref="IImageEncoder" />.
/// </summary>
public class NullImageEncoder : IImageEncoder public class NullImageEncoder : IImageEncoder
{ {
/// <inheritdoc /> public string[] SupportedInputFormats =>
public IReadOnlyCollection<string> SupportedInputFormats new[]
=> new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" }; {
"png",
"jpeg",
"jpg"
};
/// <inheritdoc /> public ImageFormat[] SupportedOutputFormats => new[] { ImageFormat.Jpg, ImageFormat.Png };
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
=> new HashSet<ImageFormat>() { ImageFormat.Jpg, ImageFormat.Png };
/// <inheritdoc /> public void CropWhiteSpace(string inputPath, string outputPath)
public string Name => "Null Image Encoder"; {
throw new NotImplementedException();
}
/// <inheritdoc />
public bool SupportsImageCollageCreation => false;
/// <inheritdoc />
public bool SupportsImageEncoding => false;
/// <inheritdoc />
public ImageDimensions GetImageSize(string path)
=> throw new NotImplementedException();
/// <inheritdoc />
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options) public void CreateImageCollage(ImageCollageOptions options)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public string Name => "Null Image Encoder";
public bool SupportsImageCollageCreation => false;
public bool SupportsImageEncoding => false;
public ImageSize GetImageSize(string path)
{
throw new NotImplementedException();
}
} }
} }

View File

@@ -2,13 +2,13 @@ using System;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using SkiaSharp; using SkiaSharp;
namespace Jellyfin.Drawing.Skia namespace Emby.Drawing
{ {
public static class PercentPlayedDrawer public static class PercentPlayedDrawer
{ {
private const int IndicatorHeight = 8; private const int IndicatorHeight = 8;
public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent) public static void Process(SKCanvas canvas, ImageSize imageSize, double percent)
{ {
using (var paint = new SKPaint()) using (var paint = new SKPaint())
{ {
@@ -23,8 +23,8 @@ namespace Jellyfin.Drawing.Skia
foregroundWidth *= percent; foregroundWidth *= percent;
foregroundWidth /= 100; foregroundWidth /= 100;
paint.Color = SKColor.Parse("#FF00A4DC"); paint.Color = SKColor.Parse("#FF52B54B");
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint); canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(Math.Round(foregroundWidth)), (float)endY), paint);
} }
} }
} }

View File

@@ -1,19 +1,19 @@
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using SkiaSharp; using SkiaSharp;
namespace Jellyfin.Drawing.Skia namespace Emby.Drawing
{ {
public static class PlayedIndicatorDrawer public static class PlayedIndicatorDrawer
{ {
private const int OffsetFromTopRightCorner = 38; private const int OffsetFromTopRightCorner = 38;
public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize) public static void DrawPlayedIndicator(SKCanvas canvas, ImageSize imageSize)
{ {
var x = imageSize.Width - OffsetFromTopRightCorner; var x = imageSize.Width - OffsetFromTopRightCorner;
using (var paint = new SKPaint()) using (var paint = new SKPaint())
{ {
paint.Color = SKColor.Parse("#CC00A4DC"); paint.Color = SKColor.Parse("#CC52B54B");
paint.Style = SKPaintStyle.Fill; paint.Style = SKPaintStyle.Fill;
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
} }

View File

@@ -8,8 +8,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyCompany("Jellyfin Project")]
[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyProduct("Jellyfin: The Free Software Media System")]
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]

View File

@@ -1,45 +1,42 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Reflection;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SkiaSharp; using SkiaSharp;
using static Jellyfin.Drawing.Skia.SkiaHelper;
namespace Jellyfin.Drawing.Skia namespace Emby.Drawing
{ {
public class SkiaEncoder : IImageEncoder public class SkiaEncoder : IImageEncoder
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths; private static IApplicationPaths _appPaths;
private readonly ILocalizationManager _localizationManager; private readonly IFileSystem _fileSystem;
private static ILocalizationManager _localizationManager;
private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
public SkiaEncoder( public SkiaEncoder(
ILogger<SkiaEncoder> logger, ILoggerFactory loggerFactory,
IApplicationPaths appPaths, IApplicationPaths appPaths,
IFileSystem fileSystem,
ILocalizationManager localizationManager) ILocalizationManager localizationManager)
{ {
_logger = logger; _logger = loggerFactory.CreateLogger("ImageEncoder");
_appPaths = appPaths; _appPaths = appPaths;
_fileSystem = fileSystem;
_localizationManager = localizationManager; _localizationManager = localizationManager;
LogVersion();
} }
public string Name => "Skia"; public string[] SupportedInputFormats =>
new[]
public bool SupportsImageCollageCreation => true;
public bool SupportsImageEncoding => true;
public IReadOnlyCollection<string> SupportedInputFormats =>
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ {
"jpeg", "jpeg",
"jpg", "jpg",
@@ -65,20 +62,26 @@ namespace Jellyfin.Drawing.Skia
"arw" "arw"
}; };
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats public ImageFormat[] SupportedOutputFormats => new[] { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
=> new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
/// <summary> private void LogVersion()
/// Test to determine if the native lib is available
/// </summary>
public static void TestSkia()
{ {
// test an operation that requires the native library // test an operation that requires the native library
SKPMColor.PreMultiply(SKColors.Black); SKPMColor.PreMultiply(SKColors.Black);
_logger.LogInformation("SkiaSharp version: " + GetVersion());
}
public static string GetVersion()
{
return typeof(SKBitmap).GetTypeInfo().Assembly.GetName().Version.ToString();
} }
private static bool IsTransparent(SKColor color) private static bool IsTransparent(SKColor color)
=> (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0; {
return (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0;
}
public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat) public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
{ {
@@ -106,7 +109,6 @@ namespace Jellyfin.Drawing.Skia
return false; return false;
} }
} }
return true; return true;
} }
@@ -119,7 +121,6 @@ namespace Jellyfin.Drawing.Skia
return false; return false;
} }
} }
return true; return true;
} }
@@ -129,51 +130,33 @@ namespace Jellyfin.Drawing.Skia
for (int row = 0; row < bitmap.Height; ++row) for (int row = 0; row < bitmap.Height; ++row)
{ {
if (IsTransparentRow(bitmap, row)) if (IsTransparentRow(bitmap, row))
{
topmost = row + 1; topmost = row + 1;
} else break;
else
{
break;
}
} }
int bottommost = bitmap.Height; int bottommost = bitmap.Height;
for (int row = bitmap.Height - 1; row >= 0; --row) for (int row = bitmap.Height - 1; row >= 0; --row)
{ {
if (IsTransparentRow(bitmap, row)) if (IsTransparentRow(bitmap, row))
{
bottommost = row; bottommost = row;
} else break;
else
{
break;
}
} }
int leftmost = 0, rightmost = bitmap.Width; int leftmost = 0, rightmost = bitmap.Width;
for (int col = 0; col < bitmap.Width; ++col) for (int col = 0; col < bitmap.Width; ++col)
{ {
if (IsTransparentColumn(bitmap, col)) if (IsTransparentColumn(bitmap, col))
{
leftmost = col + 1; leftmost = col + 1;
}
else else
{
break; break;
}
} }
for (int col = bitmap.Width - 1; col >= 0; --col) for (int col = bitmap.Width - 1; col >= 0; --col)
{ {
if (IsTransparentColumn(bitmap, col)) if (IsTransparentColumn(bitmap, col))
{
rightmost = col; rightmost = col;
}
else else
{
break; break;
}
} }
var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost); var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
@@ -185,33 +168,27 @@ namespace Jellyfin.Drawing.Skia
} }
} }
/// <inheritdoc /> public ImageSize GetImageSize(string path)
public ImageDimensions GetImageSize(string path)
{ {
if (path == null) using (var s = new SKFileStream(path))
using (var codec = SKCodec.Create(s))
{ {
throw new ArgumentNullException(nameof(path));
}
if (!File.Exists(path))
{
throw new FileNotFoundException("File not found", path);
}
using (var codec = SKCodec.Create(path, out SKCodecResult result))
{
EnsureSuccess(result);
var info = codec.Info; var info = codec.Info;
return new ImageDimensions(info.Width, info.Height); return new ImageSize
{
Width = info.Width,
Height = info.Height
};
} }
} }
private static bool HasDiacritics(string text) private static bool HasDiacritics(string text)
=> !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal); {
return !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
}
private bool RequiresSpecialCharacterHack(string path) private static bool RequiresSpecialCharacterHack(string path)
{ {
if (_localizationManager.HasUnicodeCategory(path, UnicodeCategory.OtherLetter)) if (_localizationManager.HasUnicodeCategory(path, UnicodeCategory.OtherLetter))
{ {
@@ -226,7 +203,7 @@ namespace Jellyfin.Drawing.Skia
return false; return false;
} }
private string NormalizePath(string path) private static string NormalizePath(string path, IFileSystem fileSystem)
{ {
if (!RequiresSpecialCharacterHack(path)) if (!RequiresSpecialCharacterHack(path))
{ {
@@ -235,8 +212,8 @@ namespace Jellyfin.Drawing.Skia
var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path) ?? string.Empty); var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path) ?? string.Empty);
Directory.CreateDirectory(Path.GetDirectoryName(tempPath)); fileSystem.CreateDirectory(fileSystem.GetDirectoryName(tempPath));
File.Copy(path, tempPath, true); fileSystem.CopyFile(path, tempPath, true);
return tempPath; return tempPath;
} }
@@ -269,18 +246,19 @@ namespace Jellyfin.Drawing.Skia
} }
} }
internal SKBitmap Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) private static string[] TransparentImageTypes = new string[] { ".png", ".gif", ".webp" };
internal static SKBitmap Decode(string path, bool forceCleanBitmap, IFileSystem fileSystem, ImageOrientation? orientation, out SKEncodedOrigin origin)
{ {
if (!File.Exists(path)) if (!fileSystem.FileExists(path))
{ {
throw new FileNotFoundException("File not found", path); throw new FileNotFoundException("File not found", path);
} }
var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path)); var requiresTransparencyHack = TransparentImageTypes.Contains(Path.GetExtension(path) ?? string.Empty);
if (requiresTransparencyHack || forceCleanBitmap) if (requiresTransparencyHack || forceCleanBitmap)
{ {
using (var stream = new SKFileStream(NormalizePath(path))) using (var stream = new SKFileStream(NormalizePath(path, fileSystem)))
using (var codec = SKCodec.Create(stream)) using (var codec = SKCodec.Create(stream))
{ {
if (codec == null) if (codec == null)
@@ -292,20 +270,27 @@ namespace Jellyfin.Drawing.Skia
// create the bitmap // create the bitmap
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack); var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
// decode if (bitmap != null)
_ = codec.GetPixels(bitmap.Info, bitmap.GetPixels()); {
// decode
codec.GetPixels(bitmap.Info, bitmap.GetPixels());
origin = codec.EncodedOrigin; origin = codec.EncodedOrigin;
}
else
{
origin = GetSKEncodedOrigin(orientation);
}
return bitmap; return bitmap;
} }
} }
var resultBitmap = SKBitmap.Decode(NormalizePath(path)); var resultBitmap = SKBitmap.Decode(NormalizePath(path, fileSystem));
if (resultBitmap == null) if (resultBitmap == null)
{ {
return Decode(path, true, orientation, out origin); return Decode(path, true, fileSystem, orientation, out origin);
} }
// If we have to resize these they often end up distorted // If we have to resize these they often end up distorted
@@ -313,7 +298,7 @@ namespace Jellyfin.Drawing.Skia
{ {
using (resultBitmap) using (resultBitmap)
{ {
return Decode(path, true, orientation, out origin); return Decode(path, true, fileSystem, orientation, out origin);
} }
} }
@@ -325,13 +310,13 @@ namespace Jellyfin.Drawing.Skia
{ {
if (cropWhitespace) if (cropWhitespace)
{ {
using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin)) using (var bitmap = Decode(path, forceAnalyzeBitmap, _fileSystem, orientation, out origin))
{ {
return CropWhiteSpace(bitmap); return CropWhiteSpace(bitmap);
} }
} }
return Decode(path, forceAnalyzeBitmap, orientation, out origin); return Decode(path, forceAnalyzeBitmap, _fileSystem, orientation, out origin);
} }
private SKBitmap GetBitmap(string path, bool cropWhitespace, bool autoOrient, ImageOrientation? orientation) private SKBitmap GetBitmap(string path, bool cropWhitespace, bool autoOrient, ImageOrientation? orientation)
@@ -342,11 +327,14 @@ namespace Jellyfin.Drawing.Skia
{ {
var bitmap = GetBitmap(path, cropWhitespace, true, orientation, out origin); var bitmap = GetBitmap(path, cropWhitespace, true, orientation, out origin);
if (bitmap != null && origin != SKEncodedOrigin.TopLeft) if (bitmap != null)
{ {
using (bitmap) if (origin != SKEncodedOrigin.TopLeft)
{ {
return OrientImage(bitmap, origin); using (bitmap)
{
return OrientImage(bitmap, origin);
}
} }
} }
@@ -370,6 +358,7 @@ namespace Jellyfin.Drawing.Skia
switch (origin) switch (origin)
{ {
case SKEncodedOrigin.TopRight: case SKEncodedOrigin.TopRight:
{ {
var rotated = new SKBitmap(bitmap.Width, bitmap.Height); var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
@@ -388,8 +377,11 @@ namespace Jellyfin.Drawing.Skia
var rotated = new SKBitmap(bitmap.Width, bitmap.Height); var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
using (var surface = new SKCanvas(rotated)) using (var surface = new SKCanvas(rotated))
{ {
float px = (float)bitmap.Width / 2; float px = bitmap.Width;
float py = (float)bitmap.Height / 2; px /= 2;
float py = bitmap.Height;
py /= 2;
surface.RotateDegrees(180, px, py); surface.RotateDegrees(180, px, py);
surface.DrawBitmap(bitmap, 0, 0); surface.DrawBitmap(bitmap, 0, 0);
@@ -403,9 +395,11 @@ namespace Jellyfin.Drawing.Skia
var rotated = new SKBitmap(bitmap.Width, bitmap.Height); var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
using (var surface = new SKCanvas(rotated)) using (var surface = new SKCanvas(rotated))
{ {
float px = (float)bitmap.Width / 2; float px = bitmap.Width;
px /= 2;
float py = (float)bitmap.Height / 2; float py = bitmap.Height;
py /= 2;
surface.Translate(rotated.Width, 0); surface.Translate(rotated.Width, 0);
surface.Scale(-1, 1); surface.Scale(-1, 1);
@@ -429,6 +423,7 @@ namespace Jellyfin.Drawing.Skia
surface.RotateDegrees(90); surface.RotateDegrees(90);
surface.DrawBitmap(bitmap, 0, 0); surface.DrawBitmap(bitmap, 0, 0);
} }
var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height); var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
@@ -493,7 +488,8 @@ namespace Jellyfin.Drawing.Skia
return rotated; return rotated;
} }
default: return bitmap; default:
return bitmap;
} }
} }
@@ -503,7 +499,6 @@ namespace Jellyfin.Drawing.Skia
{ {
throw new ArgumentNullException(nameof(inputPath)); throw new ArgumentNullException(nameof(inputPath));
} }
if (string.IsNullOrWhiteSpace(inputPath)) if (string.IsNullOrWhiteSpace(inputPath))
{ {
throw new ArgumentNullException(nameof(outputPath)); throw new ArgumentNullException(nameof(outputPath));
@@ -523,11 +518,11 @@ namespace Jellyfin.Drawing.Skia
throw new ArgumentOutOfRangeException(string.Format("Skia unable to read image {0}", inputPath)); throw new ArgumentOutOfRangeException(string.Format("Skia unable to read image {0}", inputPath));
} }
var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height); //_logger.LogInformation("Color type {0}", bitmap.Info.ColorType);
if (!options.CropWhiteSpace var originalImageSize = new ImageSize(bitmap.Width, bitmap.Height);
&& options.HasDefaultOptions(inputPath, originalImageSize)
&& !autoOrient) if (!options.CropWhiteSpace && options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
{ {
// Just spit out the original file if all the options are default // Just spit out the original file if all the options are default
return inputPath; return inputPath;
@@ -535,10 +530,10 @@ namespace Jellyfin.Drawing.Skia
var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize); var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
var width = newImageSize.Width; var width = Convert.ToInt32(Math.Round(newImageSize.Width));
var height = newImageSize.Height; var height = Convert.ToInt32(Math.Round(newImageSize.Height));
using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType)) using (var resizedBitmap = new SKBitmap(width, height))//, bitmap.ColorType, bitmap.AlphaType))
{ {
// scale image // scale image
bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High); bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
@@ -546,12 +541,14 @@ namespace Jellyfin.Drawing.Skia
// If all we're doing is resizing then we can stop now // If all we're doing is resizing then we can stop now
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath));
using (var outputStream = new SKFileWStream(outputPath)) using (var outputStream = new SKFileWStream(outputPath))
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()))
{ {
pixmap.Encode(outputStream, skiaOutputFormat, quality); using (var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()))
return outputPath; {
pixmap.Encode(outputStream, skiaOutputFormat, quality);
return outputPath;
}
} }
} }
@@ -598,7 +595,7 @@ namespace Jellyfin.Drawing.Skia
DrawIndicator(canvas, width, height, options); DrawIndicator(canvas, width, height, options);
} }
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath));
using (var outputStream = new SKFileWStream(outputPath)) using (var outputStream = new SKFileWStream(outputPath))
{ {
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels())) using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
@@ -614,20 +611,21 @@ namespace Jellyfin.Drawing.Skia
public void CreateImageCollage(ImageCollageOptions options) public void CreateImageCollage(ImageCollageOptions options)
{ {
double ratio = (double)options.Width / options.Height; double ratio = options.Width;
ratio /= options.Height;
if (ratio >= 1.4) if (ratio >= 1.4)
{ {
new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); new StripCollageBuilder(_appPaths, _fileSystem).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
} }
else if (ratio >= .9) else if (ratio >= .9)
{ {
new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
} }
else else
{ {
// TODO: Create Poster collage capability // @todo create Poster collage capability
new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
} }
} }
@@ -635,7 +633,7 @@ namespace Jellyfin.Drawing.Skia
{ {
try try
{ {
var currentImageSize = new ImageDimensions(imageWidth, imageHeight); var currentImageSize = new ImageSize(imageWidth, imageHeight);
if (options.AddPlayedIndicator) if (options.AddPlayedIndicator)
{ {
@@ -656,5 +654,11 @@ namespace Jellyfin.Drawing.Skia
_logger.LogError(ex, "Error drawing indicator overlay"); _logger.LogError(ex, "Error drawing indicator overlay");
} }
} }
public string Name => "Skia";
public bool SupportsImageCollageCreation => true;
public bool SupportsImageEncoding => true;
} }
} }

View File

@@ -1,17 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
using SkiaSharp; using SkiaSharp;
namespace Jellyfin.Drawing.Skia namespace Emby.Drawing
{ {
public class StripCollageBuilder public class StripCollageBuilder
{ {
private readonly SkiaEncoder _skiaEncoder; private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
public StripCollageBuilder(SkiaEncoder skiaEncoder) public StripCollageBuilder(IApplicationPaths appPaths, IFileSystem fileSystem)
{ {
_skiaEncoder = skiaEncoder; _appPaths = appPaths;
_fileSystem = fileSystem;
} }
public static SKEncodedImageFormat GetEncodedFormat(string outputPath) public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
@@ -21,50 +25,54 @@ namespace Jellyfin.Drawing.Skia
throw new ArgumentNullException(nameof(outputPath)); throw new ArgumentNullException(nameof(outputPath));
} }
var ext = Path.GetExtension(outputPath); var ext = Path.GetExtension(outputPath).ToLower();
if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase) if (ext == ".jpg" || ext == ".jpeg")
|| string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
{
return SKEncodedImageFormat.Jpeg; return SKEncodedImageFormat.Jpeg;
}
if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase)) if (ext == ".webp")
{
return SKEncodedImageFormat.Webp; return SKEncodedImageFormat.Webp;
}
if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase)) if (ext == ".gif")
{
return SKEncodedImageFormat.Gif; return SKEncodedImageFormat.Gif;
}
if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase)) if (ext == ".bmp")
{
return SKEncodedImageFormat.Bmp; return SKEncodedImageFormat.Bmp;
}
// default to png // default to png
return SKEncodedImageFormat.Png; return SKEncodedImageFormat.Png;
} }
public void BuildPosterCollage(string[] paths, string outputPath, int width, int height)
{
// @todo
}
public void BuildSquareCollage(string[] paths, string outputPath, int width, int height) public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
{ {
using (var bitmap = BuildSquareCollageBitmap(paths, width, height)) using (var bitmap = BuildSquareCollageBitmap(paths, width, height))
using (var outputStream = new SKFileWStream(outputPath))
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
{ {
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); using (var outputStream = new SKFileWStream(outputPath))
{
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
{
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
}
} }
} }
public void BuildThumbCollage(string[] paths, string outputPath, int width, int height) public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
{ {
using (var bitmap = BuildThumbCollageBitmap(paths, width, height)) using (var bitmap = BuildThumbCollageBitmap(paths, width, height))
using (var outputStream = new SKFileWStream(outputPath))
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
{ {
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); using (var outputStream = new SKFileWStream(outputPath))
{
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
{
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
}
} }
} }
@@ -76,18 +84,21 @@ namespace Jellyfin.Drawing.Skia
{ {
canvas.Clear(SKColors.Black); canvas.Clear(SKColors.Black);
// number of images used in the thumbnail
var iCount = 3;
// determine sizes for each image that will composited into the final image // determine sizes for each image that will composited into the final image
var iSlice = Convert.ToInt32(width / iCount); var iSlice = Convert.ToInt32(width * 0.23475);
int iHeight = Convert.ToInt32(height * 1.00); int iTrans = Convert.ToInt32(height * .25);
int iHeight = Convert.ToInt32(height * .70);
var horizontalImagePadding = Convert.ToInt32(width * 0.0125);
var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
int imageIndex = 0; int imageIndex = 0;
for (int i = 0; i < iCount; i++)
for (int i = 0; i < 4; i++)
{ {
using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex)) using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
{ {
imageIndex = newIndex; imageIndex = newIndex;
if (currentBitmap == null) if (currentBitmap == null)
{ {
continue; continue;
@@ -104,7 +115,44 @@ namespace Jellyfin.Drawing.Skia
using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight))) using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
{ {
// draw image onto canvas // draw image onto canvas
canvas.DrawImage(subset ?? image, iSlice * i, 0); canvas.DrawImage(subset ?? image, (horizontalImagePadding * (i + 1)) + (iSlice * i), verticalSpacing);
if (subset == null)
{
continue;
}
// create reflection of image below the drawn image
using (var croppedBitmap = SKBitmap.FromImage(subset))
using (var reflectionBitmap = new SKBitmap(croppedBitmap.Width, croppedBitmap.Height / 2, croppedBitmap.ColorType, croppedBitmap.AlphaType))
{
// resize to half height
currentBitmap.ScalePixels(reflectionBitmap, SKFilterQuality.High);
using (var flippedBitmap = new SKBitmap(reflectionBitmap.Width, reflectionBitmap.Height, reflectionBitmap.ColorType, reflectionBitmap.AlphaType))
using (var flippedCanvas = new SKCanvas(flippedBitmap))
{
// flip image vertically
var matrix = SKMatrix.MakeScale(1, -1);
matrix.SetScaleTranslate(1, -1, 0, flippedBitmap.Height);
flippedCanvas.SetMatrix(matrix);
flippedCanvas.DrawBitmap(reflectionBitmap, 0, 0);
flippedCanvas.ResetMatrix();
// create gradient to make image appear as a reflection
var remainingHeight = height - (iHeight + (2 * verticalSpacing));
flippedCanvas.ClipRect(SKRect.Create(reflectionBitmap.Width, remainingHeight));
using (var gradient = new SKPaint())
{
gradient.IsAntialias = true;
gradient.BlendMode = SKBlendMode.SrcOver;
gradient.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(0, remainingHeight), new[] { new SKColor(0, 0, 0, 128), new SKColor(0, 0, 0, 208), new SKColor(0, 0, 0, 240), new SKColor(0, 0, 0, 255) }, null, SKShaderTileMode.Clamp);
flippedCanvas.DrawPaint(gradient);
}
// finally draw reflection onto canvas
canvas.DrawBitmap(flippedBitmap, (horizontalImagePadding * (i + 1)) + (iSlice * i), iHeight + (2 * verticalSpacing));
}
}
} }
} }
} }
@@ -126,7 +174,7 @@ namespace Jellyfin.Drawing.Skia
currentIndex = 0; currentIndex = 0;
} }
bitmap = _skiaEncoder.Decode(paths[currentIndex], false, null, out var origin); bitmap = SkiaEncoder.Decode(paths[currentIndex], false, _fileSystem, null, out var origin);
imagesTested[currentIndex] = 0; imagesTested[currentIndex] = 0;
@@ -155,6 +203,7 @@ namespace Jellyfin.Drawing.Skia
{ {
for (var y = 0; y < 2; y++) for (var y = 0; y < 2; y++)
{ {
using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex)) using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
{ {
imageIndex = newIndex; imageIndex = newIndex;

View File

@@ -2,20 +2,20 @@ using System.Globalization;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using SkiaSharp; using SkiaSharp;
namespace Jellyfin.Drawing.Skia namespace Emby.Drawing
{ {
public static class UnplayedCountIndicator public static class UnplayedCountIndicator
{ {
private const int OffsetFromTopRightCorner = 38; private const int OffsetFromTopRightCorner = 38;
public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count) public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageSize imageSize, int count)
{ {
var x = imageSize.Width - OffsetFromTopRightCorner; var x = imageSize.Width - OffsetFromTopRightCorner;
var text = count.ToString(CultureInfo.InvariantCulture); var text = count.ToString(CultureInfo.InvariantCulture);
using (var paint = new SKPaint()) using (var paint = new SKPaint())
{ {
paint.Color = SKColor.Parse("#CC00A4DC"); paint.Color = SKColor.Parse("#CC52B54B");
paint.Style = SKPaintStyle.Fill; paint.Style = SKPaintStyle.Fill;
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
} }

108
Emby.IsoMounting/.gitignore vendored Normal file
View File

@@ -0,0 +1,108 @@
# Build Folders (you can keep bin if you'd like, to store dlls and pdbs)
[Bb]in/
[Oo]bj/
# mstest test results
TestResults
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
x64/
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.log
*.vspscc
*.vssscc
.builds
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*
# NCrunch
*.ncrunch*
.*crunch*.local.xml
# Installshield output folder
[Ee]xpress
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish
# Publish Web Output
*.Publish.xml
# NuGet Packages Directory
packages
# Windows Azure Build Output
csx
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
[Bb]in
[Oo]bj
sql
TestResults
[Tt]est[Rr]esult*
*.Cache
ClientBin
[Ss]tyle[Cc]op.*
~$*
*.dbmdl
Generated_Code #added for RIA/Silverlight projects
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML

View File

@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2009
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IsoMounter", "IsoMounter\IsoMounter.csproj", "{B94C929C-6552-4620-9BE5-422DD9A151BA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B94C929C-6552-4620-9BE5-422DD9A151BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B94C929C-6552-4620-9BE5-422DD9A151BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B94C929C-6552-4620-9BE5-422DD9A151BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B94C929C-6552-4620-9BE5-422DD9A151BA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C0E8EAD1-E4D7-44CD-B801-03BD12F30B1B}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,8 @@
using MediaBrowser.Model.Plugins;
namespace IsoMounter.Configuration
{
public class PluginConfiguration : BasePluginConfiguration
{
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<Compile Include="..\..\SharedVersion.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
<ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,482 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
namespace IsoMounter
{
public class LinuxIsoManager : IIsoMounter
{
[DllImport("libc", SetLastError = true)]
static extern uint getuid();
#region Private Fields
private readonly IEnvironmentInfo EnvironmentInfo;
private readonly bool ExecutablesAvailable;
private readonly IFileSystem FileSystem;
private readonly ILogger _logger;
private readonly string MountCommand;
private readonly string MountPointRoot;
private readonly IProcessFactory ProcessFactory;
private readonly string SudoCommand;
private readonly string UmountCommand;
#endregion
#region Constructor(s)
public LinuxIsoManager(ILogger logger, IFileSystem fileSystem, IEnvironmentInfo environment, IProcessFactory processFactory)
{
EnvironmentInfo = environment;
FileSystem = fileSystem;
_logger = logger;
ProcessFactory = processFactory;
MountPointRoot = FileSystem.DirectorySeparatorChar + "tmp" + FileSystem.DirectorySeparatorChar + "Emby";
_logger.LogDebug(
"[{0}] System PATH is currently set to [{1}].",
Name,
Environment.GetEnvironmentVariable("PATH") ?? ""
);
_logger.LogDebug(
"[{0}] System path separator is [{1}].",
Name,
Path.PathSeparator
);
_logger.LogDebug(
"[{0}] Mount point root is [{1}].",
Name,
MountPointRoot
);
//
// Get the location of the executables we need to support mounting/unmounting ISO images.
//
SudoCommand = GetFullPathForExecutable("sudo");
_logger.LogInformation(
"[{0}] Using version of [sudo] located at [{1}].",
Name,
SudoCommand
);
MountCommand = GetFullPathForExecutable("mount");
_logger.LogInformation(
"[{0}] Using version of [mount] located at [{1}].",
Name,
MountCommand
);
UmountCommand = GetFullPathForExecutable("umount");
_logger.LogInformation(
"[{0}] Using version of [umount] located at [{1}].",
Name,
UmountCommand
);
if (!string.IsNullOrEmpty(SudoCommand) && !string.IsNullOrEmpty(MountCommand) && !string.IsNullOrEmpty(UmountCommand))
{
ExecutablesAvailable = true;
}
else
{
ExecutablesAvailable = false;
}
}
#endregion
#region Interface Implementation for IIsoMounter
public bool IsInstalled => true;
public string Name => "LinuxMount";
public bool RequiresInstallation => false;
public bool CanMount(string path)
{
if (EnvironmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Linux)
{
return false;
}
_logger.LogInformation(
"[{0}] Checking we can attempt to mount [{1}], Extension = [{2}], Operating System = [{3}], Executables Available = [{4}].",
Name,
path,
Path.GetExtension(path),
EnvironmentInfo.OperatingSystem,
ExecutablesAvailable.ToString()
);
if (ExecutablesAvailable)
{
return string.Equals(Path.GetExtension(path), ".iso", StringComparison.OrdinalIgnoreCase);
}
else
{
return false;
}
}
public Task Install(CancellationToken cancellationToken)
{
return Task.FromResult(false);
}
public Task<IIsoMount> Mount(string isoPath, CancellationToken cancellationToken)
{
if (MountISO(isoPath, out LinuxMount mountedISO))
{
return Task.FromResult<IIsoMount>(mountedISO);
}
else
{
throw new IOException(string.Format(
"An error occurred trying to mount image [$0].",
isoPath
));
}
}
#endregion
#region Interface Implementation for IDisposable
// Flag: Has Dispose already been called?
private bool disposed = false;
public void Dispose()
{
// Dispose of unmanaged resources.
Dispose(true);
// Suppress finalization.
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
_logger.LogInformation(
"[{0}] Disposing [{1}].",
Name,
disposing.ToString()
);
if (disposing)
{
//
// Free managed objects here.
//
}
//
// Free any unmanaged objects here.
//
disposed = true;
}
#endregion
#region Private Methods
private string GetFullPathForExecutable(string name)
{
foreach (string test in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator))
{
string path = test.Trim();
if (!string.IsNullOrEmpty(path) && FileSystem.FileExists(path = Path.Combine(path, name)))
{
return FileSystem.GetFullPath(path);
}
}
return string.Empty;
}
private uint GetUID()
{
var uid = getuid();
_logger.LogDebug(
"[{0}] Our current UID is [{1}], GetUserId() returned [{2}].",
Name,
uid.ToString(),
uid
);
return uid;
}
private bool ExecuteCommand(string cmdFilename, string cmdArguments)
{
bool processFailed = false;
var process = ProcessFactory.Create(
new ProcessOptions
{
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
FileName = cmdFilename,
Arguments = cmdArguments,
IsHidden = true,
ErrorDialog = false,
EnableRaisingEvents = true
}
);
try
{
process.Start();
//StreamReader outputReader = process.StandardOutput.;
//StreamReader errorReader = process.StandardError;
_logger.LogDebug(
"[{Name}] Standard output from process is [{Error}].",
Name,
process.StandardOutput.ReadToEnd()
);
_logger.LogDebug(
"[{Name}] Standard error from process is [{Error}].",
Name,
process.StandardError.ReadToEnd()
);
}
catch (Exception ex)
{
processFailed = true;
_logger.LogDebug(ex, "[{Name}] Unhandled exception executing command.", Name);
}
if (!processFailed && process.ExitCode == 0)
{
return true;
}
else
{
return false;
}
}
private bool MountISO(string isoPath, out LinuxMount mountedISO)
{
string cmdArguments;
string cmdFilename;
string mountPoint = Path.Combine(MountPointRoot, Guid.NewGuid().ToString());
if (!string.IsNullOrEmpty(isoPath))
{
_logger.LogInformation(
"[{Name}] Attempting to mount [{Path}].",
Name,
isoPath
);
_logger.LogDebug(
"[{Name}] ISO will be mounted at [{Path}].",
Name,
mountPoint
);
}
else
{
throw new ArgumentNullException(nameof(isoPath));
}
try
{
FileSystem.CreateDirectory(mountPoint);
}
catch (UnauthorizedAccessException)
{
throw new IOException("Unable to create mount point(Permission denied) for " + isoPath);
}
catch (Exception)
{
throw new IOException("Unable to create mount point for " + isoPath);
}
if (GetUID() == 0)
{
cmdFilename = MountCommand;
cmdArguments = string.Format("\"{0}\" \"{1}\"", isoPath, mountPoint);
}
else
{
cmdFilename = SudoCommand;
cmdArguments = string.Format("\"{0}\" \"{1}\" \"{2}\"", MountCommand, isoPath, mountPoint);
}
_logger.LogDebug(
"[{0}] Mount command [{1}], mount arguments [{2}].",
Name,
cmdFilename,
cmdArguments
);
if (ExecuteCommand(cmdFilename, cmdArguments))
{
_logger.LogInformation(
"[{0}] ISO mount completed successfully.",
Name
);
mountedISO = new LinuxMount(this, isoPath, mountPoint);
}
else
{
_logger.LogInformation(
"[{0}] ISO mount completed with errors.",
Name
);
try
{
FileSystem.DeleteDirectory(mountPoint, false);
}
catch (Exception ex)
{
_logger.LogInformation(ex, "[{Name}] Unhandled exception removing mount point.", Name);
}
mountedISO = null;
}
return mountedISO != null;
}
private void UnmountISO(LinuxMount mount)
{
string cmdArguments;
string cmdFilename;
if (mount != null)
{
_logger.LogInformation(
"[{0}] Attempting to unmount ISO [{1}] mounted on [{2}].",
Name,
mount.IsoPath,
mount.MountedPath
);
}
else
{
throw new ArgumentNullException(nameof(mount));
}
if (GetUID() == 0)
{
cmdFilename = UmountCommand;
cmdArguments = string.Format("\"{0}\"", mount.MountedPath);
}
else
{
cmdFilename = SudoCommand;
cmdArguments = string.Format("\"{0}\" \"{1}\"", UmountCommand, mount.MountedPath);
}
_logger.LogDebug(
"[{0}] Umount command [{1}], umount arguments [{2}].",
Name,
cmdFilename,
cmdArguments
);
if (ExecuteCommand(cmdFilename, cmdArguments))
{
_logger.LogInformation(
"[{0}] ISO unmount completed successfully.",
Name
);
}
else
{
_logger.LogInformation(
"[{0}] ISO unmount completed with errors.",
Name
);
}
try
{
FileSystem.DeleteDirectory(mount.MountedPath, false);
}
catch (Exception ex)
{
_logger.LogInformation(ex, "[{Name}] Unhandled exception removing mount point.", Name);
}
}
#endregion
#region Internal Methods
internal void OnUnmount(LinuxMount mount)
{
UnmountISO(mount);
}
#endregion
}
}

View File

@@ -0,0 +1,83 @@
using System;
using MediaBrowser.Model.IO;
namespace IsoMounter
{
internal class LinuxMount : IIsoMount
{
#region Private Fields
private readonly LinuxIsoManager linuxIsoManager;
#endregion
#region Constructor(s)
internal LinuxMount(LinuxIsoManager isoManager, string isoPath, string mountFolder)
{
linuxIsoManager = isoManager;
IsoPath = isoPath;
MountedPath = mountFolder;
}
#endregion
#region Interface Implementation for IDisposable
// Flag: Has Dispose already been called?
private bool disposed = false;
public void Dispose()
{
// Dispose of unmanaged resources.
Dispose(true);
// Suppress finalization.
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
//
// Free managed objects here.
//
linuxIsoManager.OnUnmount(this);
}
//
// Free any unmanaged objects here.
//
disposed = true;
}
#endregion
#region Interface Implementation for IIsoMount
public string IsoPath { get; private set; }
public string MountedPath { get; private set; }
#endregion
}
}

View File

@@ -0,0 +1,30 @@
using System;
using IsoMounter.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Serialization;
namespace IsoMounter
{
public class Plugin : BasePlugin<PluginConfiguration>
{
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer)
{
}
private Guid _id = new Guid("4682DD4C-A675-4F1B-8E7C-79ADF137A8F8");
public override Guid Id => _id;
/// <summary>
/// Gets the name of the plugin
/// </summary>
/// <value>The name.</value>
public override string Name => "Iso Mounter";
/// <summary>
/// Gets the description.
/// </summary>
/// <value>The description.</value>
public override string Description => "Mount and stream ISO contents";
}
}

View File

@@ -5,12 +5,12 @@ using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following // General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information // set of attributes. Change these attribute values to modify the information
// associated with an assembly. // associated with an assembly.
[assembly: AssemblyTitle("Jellyfin.Drawing.Skia")] [assembly: AssemblyTitle("IsoMounter")]
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyCompany("Jellyfin Project")]
[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyProduct("Jellyfin: The Free Software Media System")]
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")] [assembly: NeutralResourcesLanguage("en")]

339
Emby.IsoMounting/LICENSE Normal file
View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,14 @@
# MediaBrowser.IsoMounting.Linux
This implements two core interfaces, IIsoManager, and IIsoMount.
### IIsoManager
The manager class can be used to create a mount, and also determine if the mounter is capable of mounting a given file.
### IIsoMount
IIsoMount then represents a mount instance, which will be unmounted on disposal.
***
This Linux version use sudo, mount and umount.
You need to add this to your sudo file via visudo(change the username):
Defaults:jsmith !requiretty
jsmith ALL=(root) NOPASSWD: /bin/mount
jsmith ALL=(root) NOPASSWD: /bin/umount

View File

@@ -33,29 +33,27 @@ namespace Emby.Naming.Audio
// Normalize // Normalize
// Remove whitespace // Remove whitespace
filename = filename.Replace('-', ' '); filename = filename.Replace("-", " ");
filename = filename.Replace('.', ' '); filename = filename.Replace(".", " ");
filename = filename.Replace('(', ' '); filename = filename.Replace("(", " ");
filename = filename.Replace(')', ' '); filename = filename.Replace(")", " ");
filename = Regex.Replace(filename, @"\s+", " "); filename = Regex.Replace(filename, @"\s+", " ");
filename = filename.TrimStart(); filename = filename.TrimStart();
foreach (var prefix in _options.AlbumStackingPrefixes) foreach (var prefix in _options.AlbumStackingPrefixes)
{ {
if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) != 0) if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) == 0)
{ {
continue; var tmp = filename.Substring(prefix.Length);
}
var tmp = filename.Substring(prefix.Length); tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty;
tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty; if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) result.IsMultiPart = true;
{ break;
result.IsMultiPart = true; }
break;
} }
} }

View File

@@ -7,13 +7,11 @@ namespace Emby.Naming.Audio
/// </summary> /// </summary>
/// <value>The name.</value> /// <value>The name.</value>
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// Gets or sets the part. /// Gets or sets the part.
/// </summary> /// </summary>
/// <value>The part.</value> /// <value>The part.</value>
public string Part { get; set; } public string Part { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this instance is multi part. /// Gets or sets a value indicating whether this instance is multi part.
/// </summary> /// </summary>

View File

@@ -12,56 +12,35 @@ namespace Emby.Naming.AudioBook
/// </summary> /// </summary>
/// <value>The path.</value> /// <value>The path.</value>
public string Path { get; set; } public string Path { get; set; }
/// <summary> /// <summary>
/// Gets or sets the container. /// Gets or sets the container.
/// </summary> /// </summary>
/// <value>The container.</value> /// <value>The container.</value>
public string Container { get; set; } public string Container { get; set; }
/// <summary> /// <summary>
/// Gets or sets the part number. /// Gets or sets the part number.
/// </summary> /// </summary>
/// <value>The part number.</value> /// <value>The part number.</value>
public int? PartNumber { get; set; } public int? PartNumber { get; set; }
/// <summary> /// <summary>
/// Gets or sets the chapter number. /// Gets or sets the chapter number.
/// </summary> /// </summary>
/// <value>The chapter number.</value> /// <value>The chapter number.</value>
public int? ChapterNumber { get; set; } public int? ChapterNumber { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type. /// Gets or sets the type.
/// </summary> /// </summary>
/// <value>The type.</value> /// <value>The type.</value>
public bool IsDirectory { get; set; } public bool IsDirectory { get; set; }
/// <inheritdoc/>
public int CompareTo(AudioBookFileInfo other) public int CompareTo(AudioBookFileInfo other)
{ {
if (ReferenceEquals(this, other)) if (ReferenceEquals(this, other)) return 0;
{ if (ReferenceEquals(null, other)) return 1;
return 0;
}
if (ReferenceEquals(null, other))
{
return 1;
}
var chapterNumberComparison = Nullable.Compare(ChapterNumber, other.ChapterNumber); var chapterNumberComparison = Nullable.Compare(ChapterNumber, other.ChapterNumber);
if (chapterNumberComparison != 0) if (chapterNumberComparison != 0) return chapterNumberComparison;
{
return chapterNumberComparison;
}
var partNumberComparison = Nullable.Compare(PartNumber, other.PartNumber); var partNumberComparison = Nullable.Compare(PartNumber, other.PartNumber);
if (partNumberComparison != 0) if (partNumberComparison != 0) return partNumberComparison;
{
return partNumberComparison;
}
return string.Compare(Path, other.Path, StringComparison.Ordinal); return string.Compare(Path, other.Path, StringComparison.Ordinal);
} }
} }

View File

@@ -1,4 +1,3 @@
using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -15,13 +14,14 @@ namespace Emby.Naming.AudioBook
_options = options; _options = options;
} }
public AudioBookFilePathParserResult Parse(string path) public AudioBookFilePathParserResult Parse(string path, bool IsDirectory)
{ {
if (path == null) var result = Parse(path);
{ return !result.Success ? new AudioBookFilePathParserResult() : result;
throw new ArgumentNullException(nameof(path)); }
}
private AudioBookFilePathParserResult Parse(string path)
{
var result = new AudioBookFilePathParserResult(); var result = new AudioBookFilePathParserResult();
var fileName = Path.GetFileNameWithoutExtension(path); var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions) foreach (var expression in _options.AudioBookPartsExpressions)
@@ -40,7 +40,6 @@ namespace Emby.Naming.AudioBook
} }
} }
} }
if (!result.PartNumber.HasValue) if (!result.PartNumber.HasValue)
{ {
var value = match.Groups["part"]; var value = match.Groups["part"];

View File

@@ -3,9 +3,7 @@ namespace Emby.Naming.AudioBook
public class AudioBookFilePathParserResult public class AudioBookFilePathParserResult
{ {
public int? PartNumber { get; set; } public int? PartNumber { get; set; }
public int? ChapterNumber { get; set; } public int? ChapterNumber { get; set; }
public bool Success { get; set; } public bool Success { get; set; }
} }
} }

View File

@@ -7,40 +7,33 @@ namespace Emby.Naming.AudioBook
/// </summary> /// </summary>
public class AudioBookInfo public class AudioBookInfo
{ {
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
public int? Year { get; set; }
/// <summary>
/// Gets or sets the files.
/// </summary>
/// <value>The files.</value>
public List<AudioBookFileInfo> Files { get; set; }
/// <summary>
/// Gets or sets the extras.
/// </summary>
/// <value>The extras.</value>
public List<AudioBookFileInfo> Extras { get; set; }
/// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public List<AudioBookFileInfo> AlternateVersions { get; set; }
public AudioBookInfo() public AudioBookInfo()
{ {
Files = new List<AudioBookFileInfo>(); Files = new List<AudioBookFileInfo>();
Extras = new List<AudioBookFileInfo>(); Extras = new List<AudioBookFileInfo>();
AlternateVersions = new List<AudioBookFileInfo>(); AlternateVersions = new List<AudioBookFileInfo>();
} }
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the year.
/// </summary>
public int? Year { get; set; }
/// <summary>
/// Gets or sets the files.
/// </summary>
/// <value>The files.</value>
public List<AudioBookFileInfo> Files { get; set; }
/// <summary>
/// Gets or sets the extras.
/// </summary>
/// <value>The extras.</value>
public List<AudioBookFileInfo> Extras { get; set; }
/// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public List<AudioBookFileInfo> AlternateVersions { get; set; }
} }
} }

View File

@@ -15,7 +15,7 @@ namespace Emby.Naming.AudioBook
_options = options; _options = options;
} }
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files) public IEnumerable<AudioBookInfo> Resolve(List<FileSystemMetadata> files)
{ {
var audioBookResolver = new AudioBookResolver(_options); var audioBookResolver = new AudioBookResolver(_options);

View File

@@ -24,21 +24,16 @@ namespace Emby.Naming.AudioBook
return Resolve(path, true); return Resolve(path, true);
} }
public AudioBookFileInfo Resolve(string path, bool isDirectory = false) public AudioBookFileInfo Resolve(string path, bool IsDirectory = false)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
} }
if (IsDirectory)
// TODO
if (isDirectory)
{
return null; return null;
}
var extension = Path.GetExtension(path);
var extension = Path.GetExtension(path) ?? string.Empty;
// Check supported extensions // Check supported extensions
if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{ {
@@ -47,7 +42,8 @@ namespace Emby.Naming.AudioBook
var container = extension.TrimStart('.'); var container = extension.TrimStart('.');
var parsingResult = new AudioBookFilePathParser(_options).Parse(path); var parsingResult = new AudioBookFilePathParser(_options)
.Parse(path, IsDirectory);
return new AudioBookFileInfo return new AudioBookFileInfo
{ {
@@ -55,7 +51,7 @@ namespace Emby.Naming.AudioBook
Container = container, Container = container,
PartNumber = parsingResult.PartNumber, PartNumber = parsingResult.PartNumber,
ChapterNumber = parsingResult.ChapterNumber, ChapterNumber = parsingResult.ChapterNumber,
IsDirectory = isDirectory IsDirectory = IsDirectory
}; };
} }
} }

View File

@@ -6,28 +6,17 @@ namespace Emby.Naming.Common
public class EpisodeExpression public class EpisodeExpression
{ {
private string _expression; private string _expression;
private Regex _regex; public string Expression { get => _expression;
set { _expression = value; _regex = null; } }
public string Expression
{
get => _expression;
set
{
_expression = value;
_regex = null;
}
}
public bool IsByDate { get; set; } public bool IsByDate { get; set; }
public bool IsOptimistic { get; set; } public bool IsOptimistic { get; set; }
public bool IsNamed { get; set; } public bool IsNamed { get; set; }
public bool SupportsAbsoluteEpisodeNumbers { get; set; } public bool SupportsAbsoluteEpisodeNumbers { get; set; }
public string[] DateTimeFormats { get; set; } public string[] DateTimeFormats { get; set; }
private Regex _regex;
public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled)); public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled));
public EpisodeExpression(string expression, bool byDate) public EpisodeExpression(string expression, bool byDate)

View File

@@ -6,12 +6,10 @@ namespace Emby.Naming.Common
/// 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>

View File

@@ -8,25 +8,19 @@ namespace Emby.Naming.Common
public class NamingOptions public class NamingOptions
{ {
public string[] AudioFileExtensions { get; set; } public string[] AudioFileExtensions { get; set; }
public string[] AlbumStackingPrefixes { get; set; } public string[] AlbumStackingPrefixes { get; set; }
public string[] SubtitleFileExtensions { get; set; } public string[] SubtitleFileExtensions { get; set; }
public char[] SubtitleFlagDelimiters { get; set; } public char[] SubtitleFlagDelimiters { get; set; }
public string[] SubtitleForcedFlags { get; set; } public string[] SubtitleForcedFlags { get; set; }
public string[] SubtitleDefaultFlags { get; set; } public string[] SubtitleDefaultFlags { get; set; }
public EpisodeExpression[] EpisodeExpressions { get; set; } public EpisodeExpression[] EpisodeExpressions { get; set; }
public string[] EpisodeWithoutSeasonExpressions { get; set; } public string[] EpisodeWithoutSeasonExpressions { get; set; }
public string[] EpisodeMultiPartExpressions { get; set; } public string[] EpisodeMultiPartExpressions { get; set; }
public string[] VideoFileExtensions { get; set; } public string[] VideoFileExtensions { get; set; }
public string[] StubFileExtensions { get; set; } public string[] StubFileExtensions { get; set; }
public string[] AudioBookPartsExpressions { get; set; } public string[] AudioBookPartsExpressions { get; set; }
@@ -34,22 +28,20 @@ namespace Emby.Naming.Common
public StubTypeRule[] StubTypes { get; set; } public StubTypeRule[] StubTypes { get; set; }
public char[] VideoFlagDelimiters { get; set; } public char[] VideoFlagDelimiters { get; set; }
public Format3DRule[] Format3DRules { get; set; } public Format3DRule[] Format3DRules { get; set; }
public string[] VideoFileStackingExpressions { get; set; } public string[] VideoFileStackingExpressions { get; set; }
public string[] CleanDateTimes { get; set; } public string[] CleanDateTimes { get; set; }
public string[] CleanStrings { get; set; } public string[] CleanStrings { get; set; }
public EpisodeExpression[] MultipleEpisodeExpressions { get; set; } public EpisodeExpression[] MultipleEpisodeExpressions { get; set; }
public ExtraRule[] VideoExtraRules { get; set; } public ExtraRule[] VideoExtraRules { get; set; }
public NamingOptions() public NamingOptions()
{ {
VideoFileExtensions = new[] VideoFileExtensions = new string[]
{ {
".m4v", ".m4v",
".3gp", ".3gp",
@@ -114,53 +106,53 @@ namespace Emby.Naming.Common
{ {
new StubTypeRule new StubTypeRule
{ {
StubType = "dvd", StubType = "dvd",
Token = "dvd" Token = "dvd"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "hddvd", StubType = "hddvd",
Token = "hddvd" Token = "hddvd"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "bluray", StubType = "bluray",
Token = "bluray" Token = "bluray"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "bluray", StubType = "bluray",
Token = "brrip" Token = "brrip"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "bluray", StubType = "bluray",
Token = "bd25" Token = "bd25"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "bluray", StubType = "bluray",
Token = "bd50" Token = "bd50"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "vhs", StubType = "vhs",
Token = "vhs" Token = "vhs"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "tv", StubType = "tv",
Token = "HDTV" Token = "HDTV"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "tv", StubType = "tv",
Token = "PDTV" Token = "PDTV"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "tv", StubType = "tv",
Token = "DSR" Token = "DSR"
} }
}; };
@@ -294,7 +286,7 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true) new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true)
{ {
DateTimeFormats = new[] DateTimeFormats = new []
{ {
"yyyy.MM.dd", "yyyy.MM.dd",
"yyyy-MM-dd", "yyyy-MM-dd",
@@ -303,7 +295,7 @@ namespace Emby.Naming.Common
}, },
new EpisodeExpression("([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})", true) new EpisodeExpression("([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})", true)
{ {
DateTimeFormats = new[] DateTimeFormats = new []
{ {
"dd.MM.yyyy", "dd.MM.yyyy",
"dd-MM-yyyy", "dd-MM-yyyy",
@@ -311,14 +303,6 @@ namespace Emby.Naming.Common
} }
}, },
// This isn't a Kodi naming rule, but the expression below causes false positives,
// so we make sure this one gets tested first.
// "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>(\w+\s*?)*)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
{
IsNamed = true
},
new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$") new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$")
{ {
SupportsAbsoluteEpisodeNumbers = true SupportsAbsoluteEpisodeNumbers = true
@@ -336,40 +320,37 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming // *** End Kodi Standard Naming
                // [bar] Foo - 1 [baz] new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})[^\\\/]*$")
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>(\w+\s*?)+?)[-\s_]+(?<epnumber>\d+).*$")
{
IsNamed = true
},
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d{1,4})[x,X]?[eE](?<epnumber>\d{1,3})[^\\\/]*$")
{ {
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]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
// "01.avi" // "01.avi"
new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$") new EpisodeExpression(@".*[\\\/](?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.\w+$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "1-12 episode title" // "1-12 episode title"
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>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$")
@@ -446,7 +427,7 @@ namespace Emby.Naming.Common
Token = "_trailer", Token = "_trailer",
MediaType = MediaType.Video MediaType = MediaType.Video
}, },
new ExtraRule new ExtraRule
{ {
ExtraType = "trailer", ExtraType = "trailer",
RuleType = ExtraRuleType.Suffix, RuleType = ExtraRuleType.Suffix,
@@ -481,7 +462,7 @@ namespace Emby.Naming.Common
Token = "_sample", Token = "_sample",
MediaType = MediaType.Video MediaType = MediaType.Video
}, },
new ExtraRule new ExtraRule
{ {
ExtraType = "sample", ExtraType = "sample",
RuleType = ExtraRuleType.Suffix, RuleType = ExtraRuleType.Suffix,
@@ -495,6 +476,7 @@ namespace Emby.Naming.Common
Token = "theme", Token = "theme",
MediaType = MediaType.Audio MediaType = MediaType.Audio
}, },
new ExtraRule new ExtraRule
{ {
ExtraType = "scene", ExtraType = "scene",
@@ -544,8 +526,8 @@ namespace Emby.Naming.Common
Token = "-short", Token = "-short",
MediaType = MediaType.Video MediaType = MediaType.Video
} }
};
};
Format3DRules = new[] Format3DRules = new[]
{ {
// Kodi rules: // Kodi rules:
@@ -666,9 +648,11 @@ namespace Emby.Naming.Common
@".*(\\|\/)(?<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]?\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](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$" @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$"
}.Select(i => new EpisodeExpression(i) }.Select(i => new EpisodeExpression(i)
{ {
IsNamed = true IsNamed = true
}).ToArray(); }).ToArray();
VideoFileExtensions = extensions VideoFileExtensions = extensions

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
@@ -10,7 +10,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@@ -18,19 +18,6 @@
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl> <PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -5,7 +5,6 @@ namespace Emby.Naming.Extensions
{ {
public static class StringExtensions public static class StringExtensions
{ {
// TODO: @bond remove this when moving to netstandard2.1
public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison) public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();

View File

@@ -9,8 +9,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")] [assembly: AssemblyCompany("Jellyfin Project")]
[assembly: AssemblyProduct("Jellyfin Server")] [assembly: AssemblyProduct("Jellyfin: The Free Software Media System")]
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] [assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")] [assembly: NeutralResourcesLanguage("en")]

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