diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index 4d38a906e6..e58a2bdc7e 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
- default: 3.1.100
+ default: 5.0.302
jobs:
- job: CompatibilityCheck
@@ -62,6 +62,7 @@ jobs:
- task: DownloadPipelineArtifact@2
displayName: 'Download Reference Assembly Build Artifact'
+ enabled: false
inputs:
source: "specific"
artifact: "$(NugetPackageName)"
@@ -73,6 +74,7 @@ jobs:
- task: CopyFiles@2
displayName: 'Copy Reference Assembly Build Artifact'
+ enabled: false
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: '**/*.dll'
@@ -83,6 +85,7 @@ jobs:
- task: DotNetCoreCLI@2
displayName: 'Execute ABI Compatibility Check Tool'
+ enabled: false
inputs:
command: custom
custom: compat
diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml
deleted file mode 100644
index de6bbf04ce..0000000000
--- a/.ci/azure-pipelines-api-client.yml
+++ /dev/null
@@ -1,78 +0,0 @@
-parameters:
- - name: LinuxImage
- type: string
- default: "ubuntu-latest"
- - name: GeneratorVersion
- type: string
- default: "5.0.0-beta2"
-
-jobs:
-- job: GenerateApiClients
- displayName: 'Generate Api Clients'
- dependsOn: Test
-
- pool:
- vmImage: "${{ parameters.LinuxImage }}"
-
- steps:
- - task: DownloadPipelineArtifact@2
- displayName: 'Download OpenAPI Spec Artifact'
- inputs:
- source: 'current'
- artifact: "OpenAPI Spec"
- path: "$(System.ArtifactsDirectory)/openapispec"
- runVersion: "latest"
-
- - task: CmdLine@2
- displayName: 'Download OpenApi Generator'
- inputs:
- script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
-
-## Authenticate with npm registry
- - task: npmAuthenticate@0
- inputs:
- workingFile: ./.npmrc
- customEndpoint: 'jellyfin-bot for NPM'
-
-## Generate npm api client
-# Unstable
- - task: CmdLine@2
- displayName: 'Build unstable typescript axios client'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- inputs:
- script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
-
-# Stable
- - task: CmdLine@2
- displayName: 'Build stable typescript axios client'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
-
-## Run npm install
- - task: Npm@1
- displayName: 'Install npm dependencies'
- inputs:
- command: install
- workingDir: ./apiclient/generated/typescript/axios
-
-## Publish npm packages
-# Unstable
- - task: Npm@1
- displayName: 'Publish unstable typescript axios client'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- inputs:
- command: publish
- publishRegistry: useFeed
- publishFeed: 'jellyfin/unstable'
- workingDir: ./apiclient/generated/typescript/axios
-
-# Stable
- - task: Npm@1
- displayName: 'Publish stable typescript axios client'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- command: publish
- publishRegistry: useExternalRegistry
- publishEndpoint: 'jellyfin-bot for NPM'
- workingDir: ./apiclient/generated/typescript/axios
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
index 7617f0f5a2..d2c087c14d 100644
--- a/.ci/azure-pipelines-main.yml
+++ b/.ci/azure-pipelines-main.yml
@@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
- DotNetSdkVersion: 3.1.100
+ DotNetSdkVersion: 5.0.302
jobs:
- job: Build
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 67aac45c9d..543fd7fc6d 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -22,6 +22,12 @@ jobs:
BuildConfiguration: ubuntu.armhf
Linux.amd64:
BuildConfiguration: linux.amd64
+ Linux.amd64-musl:
+ BuildConfiguration: linux.amd64-musl
+ Linux.arm64:
+ BuildConfiguration: linux.arm64
+ Linux.armhf:
+ BuildConfiguration: linux.armhf
Windows.amd64:
BuildConfiguration: windows.amd64
MacOS:
@@ -63,6 +69,7 @@ jobs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**'
+ targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: OpenAPISpec
dependsOn: Test
@@ -153,7 +160,6 @@ jobs:
dependsOn:
- BuildPackage
- BuildDocker
- condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
@@ -166,7 +172,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
- commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
+ commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
- task: SSH@0
displayName: 'Update Stable Repository'
@@ -175,28 +181,42 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
- commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
+ commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
- job: PublishNuget
displayName: 'Publish NuGet packages'
- dependsOn:
- - BuildPackage
- condition: succeeded('BuildPackage')
pool:
vmImage: 'ubuntu-latest'
+ variables:
+ - name: JellyfinVersion
+ value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
+
steps:
+ - task: UseDotNet@2
+ displayName: 'Use .NET 5.0 sdk'
+ inputs:
+ packageType: 'sdk'
+ version: '5.0.x'
+
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
- command: 'pack'
- packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
- versioningScheme: 'off'
+ command: 'custom'
+ projects: |
+ Jellyfin.Data/Jellyfin.Data.csproj
+ MediaBrowser.Common/MediaBrowser.Common.csproj
+ MediaBrowser.Controller/MediaBrowser.Controller.csproj
+ MediaBrowser.Model/MediaBrowser.Model.csproj
+ Emby.Naming/Emby.Naming.csproj
+ custom: 'pack'
+ arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
- task: DotNetCoreCLI@2
displayName: 'Build Unstable Nuget packages'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: 'custom'
projects: |
@@ -214,18 +234,12 @@ jobs:
pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: Jellyfin Nuget Packages
- - task: NuGetAuthenticate@0
- displayName: 'Authenticate to stable Nuget feed'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- nuGetServiceConnections: 'NugetOrg'
-
- task: NuGetCommand@2
displayName: 'Push Nuget packages to stable feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: 'push'
- packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
+ packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
nuGetFeedType: 'external'
publishFeedCredentials: 'NugetOrg'
allowPackageConflicts: true # This ignores an error if the version already exists
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index 6a36698b56..7ec4cdad1d 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
- default: 3.1.100
+ default: 5.0.302
jobs:
- job: Test
@@ -30,11 +30,11 @@ jobs:
# This is required for the SonarCloud analyzer
- task: UseDotNet@2
- displayName: "Install .NET Core SDK 2.1"
+ displayName: "Install .NET SDK 5.x"
condition: eq(variables['ImageName'], 'ubuntu-latest')
inputs:
packageType: sdk
- version: '2.1.805'
+ version: '5.x'
- task: UseDotNet@2
displayName: "Update DotNet"
@@ -94,5 +94,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
- targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json"
+ targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
artifactName: 'OpenAPI Spec'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 5b5a17dea2..4e8b6557b9 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -6,7 +6,7 @@ variables:
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion
- value: 3.1.100
+ value: 5.0.302
pr:
autoCancel: true
@@ -61,6 +61,3 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
-
-- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- - template: azure-pipelines-api-client.yml
diff --git a/.drone.yml b/.drone.yml
deleted file mode 100644
index 87c8e414e9..0000000000
--- a/.drone.yml
+++ /dev/null
@@ -1,30 +0,0 @@
----
-kind: pipeline
-name: build-debug
-
-steps:
-- name: submodules
- image: docker:git
- commands:
- - git submodule update --init --recursive
-
-- name: build
- image: microsoft/dotnet:2-sdk
- commands:
- - dotnet publish "Jellyfin.Server" --configuration Debug --output "../ci/ci-debug"
-
----
-kind: pipeline
-name: build-release
-
-steps:
-- name: submodules
- image: docker:git
- commands:
- - git submodule update --init --recursive
-
-- name: build
- image: microsoft/dotnet:2-sdk
- commands:
- - dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release"
-
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index e902dc7124..e1900d5833 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,3 +1,4 @@
# Joshua must review all changes to deployment and build.sh
+.ci/* @joshuaboniface
deployment/* @joshuaboniface
build.sh @joshuaboniface
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index d67e1c98bf..5a525267a9 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -17,6 +17,7 @@ assignees: ''
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
+ - Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
- Base URL: [e.g. none, yes: /example]
@@ -33,7 +34,13 @@ assignees: ''
**Expected behavior**
-**Logs**
+**Server Logs**
+
+
+**FFmpeg Logs**
+
+
+**Browser Console Logs**
**Screenshots**
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0874cae2e3..70bcd49737 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,4 +6,10 @@ updates:
interval: weekly
time: '12:00'
open-pull-requests-limit: 10
-
+
+- package-ecosystem: github-actions
+ directory: '/'
+ schedule:
+ interval: weekly
+ time: '12:00'
+ open-pull-requests-limit: 10
diff --git a/.github/stale.yml b/.github/stale.yml
index 05892c44dc..cba9c33b2a 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -17,9 +17,13 @@ staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
-
+
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
+
+# Disable automatic closing of pull requests
+pulls:
+ daysUntilClose: false
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
new file mode 100644
index 0000000000..20294843d5
--- /dev/null
+++ b/.github/workflows/automation.yml
@@ -0,0 +1,76 @@
+name: Automation
+
+on:
+ push:
+ branches:
+ - master
+ pull_request_target:
+ issue_comment:
+
+jobs:
+ label:
+ name: Labeling
+ runs-on: ubuntu-latest
+ if: ${{ github.repository == 'jellyfin/jellyfin' }}
+ steps:
+ - name: Apply label
+ uses: eps1lon/actions-label-merge-conflict@v2.0.1
+ if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
+ with:
+ dirtyLabel: 'merge conflict'
+ repoToken: ${{ secrets.JF_BOT_TOKEN }}
+
+ project:
+ name: Project board
+ runs-on: ubuntu-latest
+ if: ${{ github.repository == 'jellyfin/jellyfin' }}
+ steps:
+ - name: Remove from 'Current Release' project
+ uses: alex-page/github-project-automation-plus@v0.8.1
+ if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
+ continue-on-error: true
+ with:
+ project: Current Release
+ action: delete
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Add to 'Release Next' project
+ uses: alex-page/github-project-automation-plus@v0.8.1
+ if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
+ continue-on-error: true
+ with:
+ project: Release Next
+ column: In progress
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Add to 'Current Release' project
+ uses: alex-page/github-project-automation-plus@v0.8.1
+ if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
+ continue-on-error: true
+ with:
+ project: Current Release
+ column: In progress
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Check number of comments from the team member
+ if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
+ id: member_comments
+ run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
+
+ - name: Move issue to needs triage
+ uses: alex-page/github-project-automation-plus@v0.8.1
+ if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
+ continue-on-error: true
+ with:
+ project: Issue Triage for Main Repo
+ column: Needs triage
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Add issue to triage project
+ uses: alex-page/github-project-automation-plus@v0.8.1
+ if: github.event.issue.pull_request == '' && github.event.action == 'opened'
+ continue-on-error: true
+ with:
+ project: Issue Triage for Main Repo
+ column: Pending response
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000000..3e456f9093
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,36 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+ schedule:
+ - cron: '24 2 * * 4'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'csharp' ]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '5.0.x'
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ with:
+ languages: ${{ matrix.language }}
+ queries: +security-extended
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v1
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
new file mode 100644
index 0000000000..e0b91ecee6
--- /dev/null
+++ b/.github/workflows/commands.yml
@@ -0,0 +1,119 @@
+name: Commands
+on:
+ issue_comment:
+ types:
+ - created
+ - edited
+ pull_request_target:
+ types:
+ - labeled
+ - synchronize
+
+jobs:
+ rebase:
+ name: Rebase
+ if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify as seen
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ github.event.comment.id }}
+ reactions: '+1'
+
+ - name: Checkout the latest code
+ uses: actions/checkout@v2
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ fetch-depth: 0
+
+ - name: Automatic Rebase
+ uses: cirrus-actions/rebase@1.4
+ env:
+ GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
+
+ check-backport:
+ name: Check Backport
+ if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify as seen
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ github.event.comment.id }}
+ reactions: eyes
+
+ - name: Checkout the latest code
+ uses: actions/checkout@v2
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ fetch-depth: 0
+
+ - name: Notify as running
+ id: comment_running
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ issue-number: ${{ github.event.issue.number }}
+ body: |
+ Running backport tests...
+
+ - name: Perform test backport
+ id: run_tests
+ run: |
+ set +o errexit
+ git config --global user.name "Jellyfin Bot"
+ git config --global user.email "team@jellyfin.org"
+ CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
+ git checkout master
+ git merge --no-ff ${CURRENT_BRANCH}
+ MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
+ git fetch --all
+ CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
+ stable_branch="Current stable release branch: ${CURRENT_STABLE}"
+ echo ${stable_branch}
+ echo ::set-output name=branch::${stable_branch}
+ git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
+ git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
+ retcode=$?
+ cat output.txt | grep -v 'hint:'
+ output="$( grep -v 'hint:' output.txt )"
+ output="${output//'%'/'%25'}"
+ output="${output//$'\n'/'%0A'}"
+ output="${output//$'\r'/'%0D'}"
+ echo ::set-output name=output::$output
+ exit ${retcode}
+
+ - name: Notify with result success
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null && success() }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ steps.comment_running.outputs.comment-id }}
+ body: |
+ ${{ steps.run_tests.outputs.branch }}
+ Output from `git cherry-pick`:
+
+ ---
+
+ ${{ steps.run_tests.outputs.output }}
+ reactions: hooray
+
+ - name: Notify with result failure
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null && failure() }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ steps.comment_running.outputs.comment-id }}
+ body: |
+ ${{ steps.run_tests.outputs.branch }}
+ Output from `git cherry-pick`:
+
+ ---
+
+ ${{ steps.run_tests.outputs.output }}
+ reactions: confused
diff --git a/.gitignore b/.gitignore
index 7cd3d0068a..252210e57c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -268,6 +268,7 @@ doc/
# Deployment artifacts
dist
*.exe
+*.dll
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 05f60cfa69..e55ea22485 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 7b4772730a..c2d2aff341 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -7,6 +7,7 @@
- [anthonylavado](https://github.com/anthonylavado)
- [Artiume](https://github.com/Artiume)
- [AThomsen](https://github.com/AThomsen)
+ - [barongreenback](https://github.com/BaronGreenback)
- [barronpm](https://github.com/barronpm)
- [bilde2910](https://github.com/bilde2910)
- [bfayers](https://github.com/bfayers)
@@ -16,6 +17,7 @@
- [bugfixin](https://github.com/bugfixin)
- [chaosinnovator](https://github.com/chaosinnovator)
- [ckcr4lyf](https://github.com/ckcr4lyf)
+ - [cocool97](https://github.com/cocool97)
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
- [crankdoofus](https://github.com/crankdoofus)
- [crobibero](https://github.com/crobibero)
@@ -48,6 +50,7 @@
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
+ - [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
@@ -67,6 +70,7 @@
- [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro)
- [Matt07211](https://github.com/Matt07211)
+ - [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00)
- [mitchfizz05](https://github.com/mitchfizz05)
- [MrTimscampi](https://github.com/MrTimscampi)
@@ -78,6 +82,8 @@
- [Nickbert7](https://github.com/Nickbert7)
- [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka)
+ - [OancaAndrei](https://github.com/OancaAndrei)
+ - [obradovichv](https://github.com/obradovichv)
- [oddstr13](https://github.com/oddstr13)
- [orryverducci](https://github.com/orryverducci)
- [petermcneil](https://github.com/petermcneil)
@@ -101,9 +107,11 @@
- [shemanaev](https://github.com/shemanaev)
- [skaro13](https://github.com/skaro13)
- [sl1288](https://github.com/sl1288)
+ - [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
+ - [ssenart](https://github.com/ssenart)
- [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles)
- [SuperSandro2000](https://github.com/SuperSandro2000)
@@ -138,6 +146,8 @@
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
+ - [ianjazz246](https://github.com/ianjazz246)
+ - [peterspenler](https://github.com/peterspenler)
# Emby Contributors
@@ -202,3 +212,4 @@
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [olsh](https://github.com/olsh)
+ - [gnuyent](https://github.com/gnuyent)
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000000..b899999efb
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,14 @@
+
+
+
+
+ enable
+ true
+ $(MSBuildThisFileDirectory)/jellyfin.ruleset
+
+
+
+ AllEnabledByDefault
+
+
+
diff --git a/Dockerfile b/Dockerfile
index 69af9b77b5..0859fdc4c8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,22 +1,14 @@
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
-FROM node:alpine as web-builder
+FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
- && yarn install \
+ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# because of changes in docker and systemd we need to not build in parallel at the moment
-# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
-
-FROM debian:buster-slim
+FROM debian:buster-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -25,10 +17,14 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
+# https://github.com/intel/compute-runtime/releases
+ARG GMMLIB_VERSION=20.3.2
+ARG IGC_VERSION=1.0.5435
+ARG NEO_VERSION=20.46.18421
+ARG LEVEL_ZERO_VERSION=1.0.18421
+
# Install dependencies:
-# mesa-va-drivers: needed for AMD VAAPI
+# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
@@ -39,6 +35,20 @@ RUN apt-get update \
jellyfin-ffmpeg \
openssl \
locales \
+# Intel VAAPI Tone mapping dependencies:
+# Prefer NEO to Beignet since the latter one doesn't support Comet Lake or newer for now.
+# Do not use the intel-opencl-icd package from repo since they will not build with RELEASE_WITH_REGKEYS enabled.
+ && mkdir intel-compute-runtime \
+ && cd intel-compute-runtime \
+ && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \
+ && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
+ && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
+ && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl_${NEO_VERSION}_amd64.deb \
+ && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-ocloc_${NEO_VERSION}_amd64.deb \
+ && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
+ && dpkg -i *.deb \
+ && cd .. \
+ && rm -rf intel-compute-runtime \
&& apt-get remove gnupg wget apt-transport-https -y \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
@@ -52,6 +62,19 @@ ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# because of changes in docker and systemd we need to not build in parallel at the moment
+# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \
diff --git a/Dockerfile.arm b/Dockerfile.arm
index efeed25dff..cc0c79c94f 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -2,30 +2,19 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
-FROM node:alpine as web-builder
+FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
- && yarn install \
+ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist
-
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# Discard objs - may cause failures if exists
-RUN find . -type d -name obj | xargs -r rm -r
-# Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
-
-
FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM arm32v7/debian:buster-slim
+FROM arm32v7/debian:buster-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -61,14 +50,25 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# Discard objs - may cause failures if exists
+RUN find . -type d -name obj | xargs -r rm -r
+# Build
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 1f2c2ec36d..64367a32da 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -2,29 +2,19 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
-FROM node:alpine as web-builder
+FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
- && yarn install \
+ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist
-
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# Discard objs - may cause failures if exists
-RUN find . -type d -name obj | xargs -r rm -r
-# Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
-
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM arm64v8/debian:buster-slim
+FROM arm64v8/debian:buster-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -50,14 +40,25 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# Discard objs - may cause failures if exists
+RUN find . -type d -name obj | xargs -r rm -r
+# Build
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
index 64d041cb05..b8301e2f27 100644
--- a/DvdLib/DvdLib.csproj
+++ b/DvdLib/DvdLib.csproj
@@ -10,10 +10,11 @@
- netstandard2.1
+ net5.0
false
true
- true
+ AllDisabledByDefault
+ disable
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
index 361319625c..b4a11ed5d6 100644
--- a/DvdLib/Ifo/Dvd.cs
+++ b/DvdLib/Ifo/Dvd.cs
@@ -31,7 +31,7 @@ namespace DvdLib.Ifo
continue;
}
- var nums = ifo.Name.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
+ var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries);
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
{
ReadVTS(ifoNumber, ifo.FullName);
diff --git a/Emby.Dlna/Common/Argument.cs b/Emby.Dlna/Common/Argument.cs
index f375e6049c..e4e9c55e0d 100644
--- a/Emby.Dlna/Common/Argument.cs
+++ b/Emby.Dlna/Common/Argument.cs
@@ -1,13 +1,23 @@
-#pragma warning disable CS1591
-
namespace Emby.Dlna.Common
{
+ ///
+ /// DLNA Query parameter type, used when querying DLNA devices via SOAP.
+ ///
public class Argument
{
- public string Name { get; set; }
+ ///
+ /// Gets or sets name of the DLNA argument.
+ ///
+ public string Name { get; set; } = string.Empty;
- public string Direction { get; set; }
+ ///
+ /// Gets or sets the direction of the parameter.
+ ///
+ public string Direction { get; set; } = string.Empty;
- public string RelatedStateVariable { get; set; }
+ ///
+ /// Gets or sets the related DLNA state variable for this argument.
+ ///
+ public string RelatedStateVariable { get; set; } = string.Empty;
}
}
diff --git a/Emby.Dlna/Common/DeviceIcon.cs b/Emby.Dlna/Common/DeviceIcon.cs
index c3f7fa8aaa..f9fd1dcec6 100644
--- a/Emby.Dlna/Common/DeviceIcon.cs
+++ b/Emby.Dlna/Common/DeviceIcon.cs
@@ -1,29 +1,41 @@
-#pragma warning disable CS1591
-
using System.Globalization;
namespace Emby.Dlna.Common
{
+ ///
+ /// Defines the .
+ ///
public class DeviceIcon
{
- public string Url { get; set; }
+ ///
+ /// Gets or sets the Url.
+ ///
+ public string Url { get; set; } = string.Empty;
- public string MimeType { get; set; }
+ ///
+ /// Gets or sets the MimeType.
+ ///
+ public string MimeType { get; set; } = string.Empty;
+ ///
+ /// Gets or sets the Width.
+ ///
public int Width { get; set; }
+ ///
+ /// Gets or sets the Height.
+ ///
public int Height { get; set; }
- public string Depth { get; set; }
+ ///
+ /// Gets or sets the Depth.
+ ///
+ public string Depth { get; set; } = string.Empty;
///
public override string ToString()
{
- return string.Format(
- CultureInfo.InvariantCulture,
- "{0}x{1}",
- Height,
- Width);
+ return string.Format(CultureInfo.InvariantCulture, "{0}x{1}", Height, Width);
}
}
}
diff --git a/Emby.Dlna/Common/DeviceService.cs b/Emby.Dlna/Common/DeviceService.cs
index 44c0a0412a..c1369558ec 100644
--- a/Emby.Dlna/Common/DeviceService.cs
+++ b/Emby.Dlna/Common/DeviceService.cs
@@ -1,21 +1,36 @@
-#pragma warning disable CS1591
-
namespace Emby.Dlna.Common
{
+ ///
+ /// Defines the .
+ ///
public class DeviceService
{
- public string ServiceType { get; set; }
+ ///
+ /// Gets or sets the Service Type.
+ ///
+ public string ServiceType { get; set; } = string.Empty;
- public string ServiceId { get; set; }
+ ///
+ /// Gets or sets the Service Id.
+ ///
+ public string ServiceId { get; set; } = string.Empty;
- public string ScpdUrl { get; set; }
+ ///
+ /// Gets or sets the Scpd Url.
+ ///
+ public string ScpdUrl { get; set; } = string.Empty;
- public string ControlUrl { get; set; }
+ ///
+ /// Gets or sets the Control Url.
+ ///
+ public string ControlUrl { get; set; } = string.Empty;
- public string EventSubUrl { get; set; }
+ ///
+ /// Gets or sets the EventSubUrl.
+ ///
+ public string EventSubUrl { get; set; } = string.Empty;
///
- public override string ToString()
- => ServiceId;
+ public override string ToString() => ServiceId;
}
}
diff --git a/Emby.Dlna/Common/ServiceAction.cs b/Emby.Dlna/Common/ServiceAction.cs
index d458d7f3f6..02b81a0aa7 100644
--- a/Emby.Dlna/Common/ServiceAction.cs
+++ b/Emby.Dlna/Common/ServiceAction.cs
@@ -1,24 +1,31 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
namespace Emby.Dlna.Common
{
+ ///
+ /// Defines the .
+ ///
public class ServiceAction
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public ServiceAction()
{
ArgumentList = new List();
}
- public string Name { get; set; }
+ ///
+ /// Gets or sets the name of the action.
+ ///
+ public string Name { get; set; } = string.Empty;
+ ///
+ /// Gets the ArgumentList.
+ ///
public List ArgumentList { get; }
///
- public override string ToString()
- {
- return Name;
- }
+ public override string ToString() => Name;
}
}
diff --git a/Emby.Dlna/Common/StateVariable.cs b/Emby.Dlna/Common/StateVariable.cs
index 6daf7ab6b2..fd733e0853 100644
--- a/Emby.Dlna/Common/StateVariable.cs
+++ b/Emby.Dlna/Common/StateVariable.cs
@@ -1,27 +1,34 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
namespace Emby.Dlna.Common
{
+ ///
+ /// Defines the .
+ ///
public class StateVariable
{
- public StateVariable()
- {
- AllowedValues = Array.Empty();
- }
-
- public string Name { get; set; }
+ ///
+ /// Gets or sets the name of the state variable.
+ ///
+ public string Name { get; set; } = string.Empty;
- public string DataType { get; set; }
+ ///
+ /// Gets or sets the data type of the state variable.
+ ///
+ public string DataType { get; set; } = string.Empty;
+ ///
+ /// Gets or sets a value indicating whether it sends events.
+ ///
public bool SendsEvents { get; set; }
- public IReadOnlyList AllowedValues { get; set; }
+ ///
+ /// Gets or sets the allowed values range.
+ ///
+ public IReadOnlyList AllowedValues { get; set; } = Array.Empty();
///
- public override string ToString()
- => Name;
+ public override string ToString() => Name;
}
}
diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs
index 6dd9a445a8..91fac4bef5 100644
--- a/Emby.Dlna/Configuration/DlnaOptions.cs
+++ b/Emby.Dlna/Configuration/DlnaOptions.cs
@@ -2,8 +2,14 @@
namespace Emby.Dlna.Configuration
{
+ ///
+ /// The DlnaOptions class contains the user definable parameters for the dlna subsystems.
+ ///
public class DlnaOptions
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public DlnaOptions()
{
EnablePlayTo = true;
@@ -11,23 +17,76 @@ namespace Emby.Dlna.Configuration
BlastAliveMessages = true;
SendOnlyMatchedHost = true;
ClientDiscoveryIntervalSeconds = 60;
- BlastAliveMessageIntervalSeconds = 1800;
+ AliveMessageIntervalSeconds = 1800;
}
+ ///
+ /// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna playTo subsystem.
+ ///
public bool EnablePlayTo { get; set; }
+ ///
+ /// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna server subsystem.
+ ///
public bool EnableServer { get; set; }
+ ///
+ /// Gets or sets a value indicating whether detailed dlna server logs are sent to the console/log.
+ /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
+ ///
public bool EnableDebugLog { get; set; }
- public bool BlastAliveMessages { get; set; }
-
- public bool SendOnlyMatchedHost { get; set; }
+ ///
+ /// Gets or sets a value indicating whether whether detailed playTo debug logs are sent to the console/log.
+ /// If the setting "Emby.Dlna.PlayTo": "Debug" msut be set in logging.default.json for this property to work.
+ ///
+ public bool EnablePlayToTracing { get; set; }
+ ///
+ /// Gets or sets the ssdp client discovery interval time (in seconds).
+ /// This is the time after which the server will send a ssdp search request.
+ ///
public int ClientDiscoveryIntervalSeconds { get; set; }
- public int BlastAliveMessageIntervalSeconds { get; set; }
+ ///
+ /// Gets or sets the frequency at which ssdp alive notifications are transmitted.
+ ///
+ public int AliveMessageIntervalSeconds { get; set; }
+
+ ///
+ /// Gets or sets the frequency at which ssdp alive notifications are transmitted. MIGRATING - TO BE REMOVED ONCE WEB HAS BEEN ALTERED.
+ ///
+ public int BlastAliveMessageIntervalSeconds
+ {
+ get
+ {
+ return AliveMessageIntervalSeconds;
+ }
+
+ set
+ {
+ AliveMessageIntervalSeconds = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the default user account that the dlna server uses.
+ ///
+ public string? DefaultUserId { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether playTo device profiles should be created.
+ ///
+ public bool AutoCreatePlayToProfiles { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to blast alive messages.
+ ///
+ public bool BlastAliveMessages { get; set; } = true;
- public string DefaultUserId { get; set; }
+ ///
+ /// gets or sets a value indicating whether to send only matched host.
+ ///
+ public bool SendOnlyMatchedHost { get; set; } = true;
}
}
diff --git a/Emby.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs
index fc02e17515..3ca43052a4 100644
--- a/Emby.Dlna/ConfigurationExtension.cs
+++ b/Emby.Dlna/ConfigurationExtension.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using Emby.Dlna.Configuration;
diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
index f5a7eca720..916044a0cc 100644
--- a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
+++ b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
@@ -9,11 +9,21 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager
{
+ ///
+ /// Defines the .
+ ///
public class ConnectionManagerService : BaseService, IConnectionManager
{
private readonly IDlnaManager _dlna;
private readonly IServerConfigurationManager _config;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance..
+ /// The for use with the instance..
public ConnectionManagerService(
IDlnaManager dlna,
IServerConfigurationManager config,
@@ -28,7 +38,7 @@ namespace Emby.Dlna.ConnectionManager
///
public string GetServiceXml()
{
- return new ConnectionManagerXmlBuilder().GetXml();
+ return ConnectionManagerXmlBuilder.GetXml();
}
///
diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
index c8db5a3674..c484dac542 100644
--- a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
+++ b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
@@ -6,45 +6,57 @@ using Emby.Dlna.Service;
namespace Emby.Dlna.ConnectionManager
{
- public class ConnectionManagerXmlBuilder
+ ///
+ /// Defines the .
+ ///
+ public static class ConnectionManagerXmlBuilder
{
- public string GetXml()
+ ///
+ /// Gets the ConnectionManager:1 service template.
+ /// See http://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf.
+ ///
+ /// An XML description of this service.
+ public static string GetXml()
{
- return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), GetStateVariables());
+ return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
}
+ ///
+ /// Get the list of state variables for this invocation.
+ ///
+ /// The .
private static IEnumerable GetStateVariables()
{
- var list = new List();
-
- list.Add(new StateVariable
+ var list = new List
{
- Name = "SourceProtocolInfo",
- DataType = "string",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "SourceProtocolInfo",
+ DataType = "string",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "SinkProtocolInfo",
- DataType = "string",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "SinkProtocolInfo",
+ DataType = "string",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "CurrentConnectionIDs",
- DataType = "string",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "CurrentConnectionIDs",
+ DataType = "string",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_ConnectionStatus",
- DataType = "string",
- SendsEvents = false,
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_ConnectionStatus",
+ DataType = "string",
+ SendsEvents = false,
- AllowedValues = new[]
+ AllowedValues = new[]
{
"OK",
"ContentFormatMismatch",
@@ -52,55 +64,56 @@ namespace Emby.Dlna.ConnectionManager
"UnreliableChannel",
"Unknown"
}
- });
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_ConnectionManager",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_ConnectionManager",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Direction",
- DataType = "string",
- SendsEvents = false,
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Direction",
+ DataType = "string",
+ SendsEvents = false,
- AllowedValues = new[]
+ AllowedValues = new[]
{
"Output",
"Input"
}
- });
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_ProtocolInfo",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_ProtocolInfo",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_ConnectionID",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_ConnectionID",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_AVTransportID",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_AVTransportID",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_RcsID",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_RcsID",
+ DataType = "ui4",
+ SendsEvents = false
+ }
+ };
return list;
}
diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs
index d4cc653942..1a1790ee6a 100644
--- a/Emby.Dlna/ConnectionManager/ControlHandler.cs
+++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs
@@ -11,10 +11,19 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager
{
+ ///
+ /// Defines the .
+ ///
public class ControlHandler : BaseControlHandler
{
private readonly DeviceProfile _profile;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile)
: base(config, logger)
{
@@ -22,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
}
///
- protected override void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter)
+ protected override void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter)
{
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
{
@@ -33,6 +42,10 @@ namespace Emby.Dlna.ConnectionManager
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
}
+ ///
+ /// Builds the response to the GetProtocolInfo request.
+ ///
+ /// The .
private void HandleGetProtocolInfo(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);
diff --git a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs
index b853e7eab6..542c7bfb4b 100644
--- a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs
+++ b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs
@@ -5,9 +5,16 @@ using Emby.Dlna.Common;
namespace Emby.Dlna.ConnectionManager
{
- public class ServiceActionListBuilder
+ ///
+ /// Defines the .
+ ///
+ public static class ServiceActionListBuilder
{
- public IEnumerable GetActions()
+ ///
+ /// Returns an enumerable of the ConnectionManagar:1 DLNA actions.
+ ///
+ /// An .
+ public static IEnumerable GetActions()
{
var list = new List
{
@@ -21,6 +28,10 @@ namespace Emby.Dlna.ConnectionManager
return list;
}
+ ///
+ /// Returns the action details for "PrepareForConnection".
+ ///
+ /// The .
private static ServiceAction PrepareForConnection()
{
var action = new ServiceAction
@@ -80,6 +91,10 @@ namespace Emby.Dlna.ConnectionManager
return action;
}
+ ///
+ /// Returns the action details for "GetCurrentConnectionInfo".
+ ///
+ /// The .
private static ServiceAction GetCurrentConnectionInfo()
{
var action = new ServiceAction
@@ -146,7 +161,11 @@ namespace Emby.Dlna.ConnectionManager
return action;
}
- private ServiceAction GetProtocolInfo()
+ ///
+ /// Returns the action details for "GetProtocolInfo".
+ ///
+ /// The .
+ private static ServiceAction GetProtocolInfo()
{
var action = new ServiceAction
{
@@ -170,7 +189,11 @@ namespace Emby.Dlna.ConnectionManager
return action;
}
- private ServiceAction GetCurrentConnectionIDs()
+ ///
+ /// Returns the action details for "GetCurrentConnectionIDs".
+ ///
+ /// The .
+ private static ServiceAction GetCurrentConnectionIDs()
{
var action = new ServiceAction
{
@@ -187,7 +210,11 @@ namespace Emby.Dlna.ConnectionManager
return action;
}
- private ServiceAction ConnectionComplete()
+ ///
+ /// Returns the action details for "ConnectionComplete".
+ ///
+ /// The .
+ private static ServiceAction ConnectionComplete()
{
var action = new ServiceAction
{
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
index 5760f260cf..9020dea994 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
@@ -19,6 +19,9 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ContentDirectory
{
+ ///
+ /// Defines the .
+ ///
public class ContentDirectoryService : BaseService, IContentDirectory
{
private readonly ILibraryManager _libraryManager;
@@ -33,6 +36,22 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaEncoder _mediaEncoder;
private readonly ITVSeriesManager _tvSeriesManager;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
+ /// The to use in the instance.
public ContentDirectoryService(
IDlnaManager dlna,
IUserDataManager userDataManager,
@@ -62,7 +81,10 @@ namespace Emby.Dlna.ContentDirectory
_tvSeriesManager = tvSeriesManager;
}
- private int SystemUpdateId
+ ///
+ /// Gets the system id. (A unique id which changes on when our definition changes.)
+ ///
+ private static int SystemUpdateId
{
get
{
@@ -75,14 +97,18 @@ namespace Emby.Dlna.ContentDirectory
///
public string GetServiceXml()
{
- return new ContentDirectoryXmlBuilder().GetXml();
+ return ContentDirectoryXmlBuilder.GetXml();
}
///
public Task ProcessControlRequestAsync(ControlRequest request)
{
- var profile = _dlna.GetProfile(request.Headers) ??
- _dlna.GetDefaultProfile();
+ if (request == null)
+ {
+ throw new ArgumentNullException(nameof(request));
+ }
+
+ var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile();
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
@@ -107,7 +133,12 @@ namespace Emby.Dlna.ContentDirectory
.ProcessControlRequestAsync(request);
}
- private User GetUser(DeviceProfile profile)
+ ///
+ /// Get the user stored in the device profile.
+ ///
+ /// The .
+ /// The .
+ private User? GetUser(DeviceProfile profile)
{
if (!string.IsNullOrEmpty(profile.UserId))
{
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
index 743dcc5161..3edaabb70e 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
@@ -6,143 +6,154 @@ using Emby.Dlna.Service;
namespace Emby.Dlna.ContentDirectory
{
- public class ContentDirectoryXmlBuilder
+ ///
+ /// Defines the .
+ ///
+ public static class ContentDirectoryXmlBuilder
{
- public string GetXml()
+ ///
+ /// Gets the ContentDirectory:1 service template.
+ /// See http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf.
+ ///
+ /// An XML description of this service.
+ public static string GetXml()
{
- return new ServiceXmlBuilder().GetXml(
- new ServiceActionListBuilder().GetActions(),
- GetStateVariables());
+ return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
}
+ ///
+ /// Get the list of state variables for this invocation.
+ ///
+ /// The .
private static IEnumerable GetStateVariables()
{
- var list = new List();
-
- list.Add(new StateVariable
+ var list = new List
{
- Name = "A_ARG_TYPE_Filter",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Filter",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_SortCriteria",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_SortCriteria",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Index",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Index",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Count",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Count",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_UpdateID",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_UpdateID",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "SearchCapabilities",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "SearchCapabilities",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "SortCapabilities",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "SortCapabilities",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "SystemUpdateID",
- DataType = "ui4",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "SystemUpdateID",
+ DataType = "ui4",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_SearchCriteria",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_SearchCriteria",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Result",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Result",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_ObjectID",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_ObjectID",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_BrowseFlag",
- DataType = "string",
- SendsEvents = false,
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_BrowseFlag",
+ DataType = "string",
+ SendsEvents = false,
- AllowedValues = new[]
+ AllowedValues = new[]
{
"BrowseMetadata",
"BrowseDirectChildren"
}
- });
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_BrowseLetter",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_BrowseLetter",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_CategoryType",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_CategoryType",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_RID",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_RID",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_PosSec",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_PosSec",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Featurelist",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Featurelist",
+ DataType = "string",
+ SendsEvents = false
+ }
+ };
return list;
}
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 4b108b89ea..ac336e5dcc 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#nullable disable
using System;
using System.Collections.Generic;
@@ -38,6 +38,9 @@ using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Dlna.ContentDirectory
{
+ ///
+ /// Defines the .
+ ///
public class ControlHandler : BaseControlHandler
{
private const string NsDc = "http://purl.org/dc/elements/1.1/";
@@ -58,6 +61,24 @@ namespace Emby.Dlna.ContentDirectory
private readonly DeviceProfile _profile;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The server address to use in this instance> for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The system id for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
public ControlHandler(
ILogger logger,
ILibraryManager libraryManager,
@@ -100,8 +121,18 @@ namespace Emby.Dlna.ContentDirectory
}
///
- protected override void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter)
+ protected override void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter)
{
+ if (xmlWriter == null)
+ {
+ throw new ArgumentNullException(nameof(xmlWriter));
+ }
+
+ if (methodParams == null)
+ {
+ throw new ArgumentNullException(nameof(methodParams));
+ }
+
const string DeviceId = "test";
if (string.Equals(methodName, "GetSearchCapabilities", StringComparison.OrdinalIgnoreCase))
@@ -167,7 +198,11 @@ namespace Emby.Dlna.ContentDirectory
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
}
- private void HandleXSetBookmark(IDictionary sparams)
+ ///
+ /// Adds a "XSetBookmark" element to the xml document.
+ ///
+ /// The method parameters.
+ private void HandleXSetBookmark(IReadOnlyDictionary sparams)
{
var id = sparams["ObjectID"];
@@ -189,75 +224,92 @@ namespace Emby.Dlna.ContentDirectory
CancellationToken.None);
}
- private void HandleGetSearchCapabilities(XmlWriter xmlWriter)
+ ///
+ /// Adds the "SearchCaps" element to the xml document.
+ ///
+ /// The .
+ private static void HandleGetSearchCapabilities(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString(
"SearchCaps",
"res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords");
}
- private void HandleGetSortCapabilities(XmlWriter xmlWriter)
+ ///
+ /// Adds the "SortCaps" element to the xml document.
+ ///
+ /// The .
+ private static void HandleGetSortCapabilities(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString(
"SortCaps",
"res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating");
}
- private void HandleGetSortExtensionCapabilities(XmlWriter xmlWriter)
+ ///
+ /// Adds the "SortExtensionCaps" element to the xml document.
+ ///
+ /// The .
+ private static void HandleGetSortExtensionCapabilities(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString(
"SortExtensionCaps",
"res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating");
}
+ ///
+ /// Adds the "Id" element to the xml document.
+ ///
+ /// The .
private void HandleGetSystemUpdateID(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString("Id", _systemUpdateId.ToString(CultureInfo.InvariantCulture));
}
- private void HandleGetFeatureList(XmlWriter xmlWriter)
+ ///
+ /// Adds the "FeatureList" element to the xml document.
+ ///
+ /// The .
+ private static void HandleGetFeatureList(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString("FeatureList", WriteFeatureListXml());
}
- private void HandleXGetFeatureList(XmlWriter xmlWriter)
+ ///
+ /// Adds the "FeatureList" element to the xml document.
+ ///
+ /// The .
+ private static void HandleXGetFeatureList(XmlWriter xmlWriter)
=> HandleGetFeatureList(xmlWriter);
- private string WriteFeatureListXml()
- {
- // TODO: clean this up
- var builder = new StringBuilder();
-
- builder.Append("");
- builder.Append("");
-
- builder.Append("");
- builder.Append("");
- builder.Append("");
- builder.Append("");
- builder.Append("");
-
- builder.Append("");
-
- return builder.ToString();
- }
-
- public string GetValueOrDefault(IDictionary sparams, string key, string defaultValue)
+ ///
+ /// Builds a static feature list.
+ ///
+ /// The xml feature list.
+ private static string WriteFeatureListXml()
{
- if (sparams.TryGetValue(key, out string val))
- {
- return val;
- }
-
- return defaultValue;
+ return ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "";
}
- private void HandleBrowse(XmlWriter xmlWriter, IDictionary sparams, string deviceId)
+ ///
+ /// Builds the "Browse" xml response.
+ ///
+ /// The .
+ /// The method parameters.
+ /// The device Id to use.
+ private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary sparams, string deviceId)
{
var id = sparams["ObjectID"];
var flag = sparams["BrowseFlag"];
- var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
- var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
+ var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
+ var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
var provided = 0;
@@ -313,7 +365,6 @@ namespace Emby.Dlna.ContentDirectory
}
else
{
- var dlnaOptions = _config.GetDlnaConfiguration();
_didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter);
}
@@ -326,7 +377,6 @@ namespace Emby.Dlna.ContentDirectory
provided = childrenResult.Items.Count;
- var dlnaOptions = _config.GetDlnaConfiguration();
foreach (var i in childrenResult.Items)
{
var childItem = i.Item;
@@ -357,17 +407,29 @@ namespace Emby.Dlna.ContentDirectory
xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture));
}
- private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary sparams, string deviceId)
+ ///
+ /// Builds the response to the "X_BrowseByLetter request.
+ ///
+ /// The .
+ /// The method parameters.
+ /// The device id.
+ private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary sparams, string deviceId)
{
// TODO: Implement this method
HandleSearch(xmlWriter, sparams, deviceId);
}
- private void HandleSearch(XmlWriter xmlWriter, IDictionary sparams, string deviceId)
+ ///
+ /// Builds a response to the "Search" request.
+ ///
+ /// The xmlWriter.
+ /// The method parameters.
+ /// The deviceId.
+ private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary sparams, string deviceId)
{
- var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
- var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
- var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
+ var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty));
+ var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
+ var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
// sort example: dc:title, dc:date
@@ -442,7 +504,17 @@ namespace Emby.Dlna.ContentDirectory
xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture));
}
- private QueryResult GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
+ ///
+ /// Returns the child items meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// The .
+ private static QueryResult GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
{
var folder = (Folder)item;
@@ -487,18 +559,32 @@ namespace Emby.Dlna.ContentDirectory
User = user,
Recursive = true,
IsMissing = false,
- ExcludeItemTypes = new[] { typeof(Book).Name },
+ ExcludeItemTypes = new[] { nameof(Book) },
IsFolder = isFolder,
MediaTypes = mediaTypes,
DtoOptions = GetDtoOptions()
});
}
- private DtoOptions GetDtoOptions()
+ ///
+ /// Returns a new DtoOptions object.
+ ///
+ /// The .
+ private static DtoOptions GetDtoOptions()
{
return new DtoOptions(true);
}
+ ///
+ /// Returns the User items meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// The .
private QueryResult GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit)
{
if (item is MusicGenre)
@@ -556,7 +642,7 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit,
StartIndex = startIndex,
IsVirtualItem = false,
- ExcludeItemTypes = new[] { typeof(Book).Name },
+ ExcludeItemTypes = new[] { nameof(Book) },
IsPlaceHolder = false,
DtoOptions = GetDtoOptions()
};
@@ -568,6 +654,14 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(queryResult);
}
+ ///
+ /// Returns the Live Tv Channels meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// The .
private QueryResult GetLiveTvChannels(User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -575,7 +669,7 @@ namespace Emby.Dlna.ContentDirectory
StartIndex = startIndex,
Limit = limit,
};
- query.IncludeItemTypes = new[] { typeof(LiveTvChannel).Name };
+ query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
SetSorting(query, sort, false);
@@ -584,6 +678,16 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the music folders meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// The .
private QueryResult GetMusicFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -643,57 +747,58 @@ namespace Emby.Dlna.ContentDirectory
return GetMusicGenres(item, user, query);
}
- var list = new List();
-
- list.Add(new ServerItem(item)
+ var list = new List
{
- StubType = StubType.Latest
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Latest
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Playlists
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Playlists
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Albums
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Albums
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.AlbumArtists
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.AlbumArtists
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Artists
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Artists
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Songs
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Songs
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Genres
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Genres
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.FavoriteArtists
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.FavoriteArtists
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.FavoriteAlbums
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.FavoriteAlbums
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.FavoriteSongs
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.FavoriteSongs
+ }
+ };
return new QueryResult
{
@@ -702,6 +807,16 @@ namespace Emby.Dlna.ContentDirectory
};
}
+ ///
+ /// Returns the movie folders meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// The .
private QueryResult GetMovieFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -776,6 +891,13 @@ namespace Emby.Dlna.ContentDirectory
};
}
+ ///
+ /// Returns the folders meeting the criteria.
+ ///
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// The .
private QueryResult GetFolders(User user, int? startIndex, int? limit)
{
var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true)
@@ -796,6 +918,16 @@ namespace Emby.Dlna.ContentDirectory
limit);
}
+ ///
+ /// Returns the TV folders meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// The .
private QueryResult GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -840,42 +972,43 @@ namespace Emby.Dlna.ContentDirectory
return GetGenres(item, user, query);
}
- var list = new List();
-
- list.Add(new ServerItem(item)
+ var list = new List
{
- StubType = StubType.ContinueWatching
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.ContinueWatching
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.NextUp
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.NextUp
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Latest
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Latest
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Series
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Series
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.FavoriteSeries
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.FavoriteSeries
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.FavoriteEpisodes
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.FavoriteEpisodes
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Genres
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Genres
+ }
+ };
return new QueryResult
{
@@ -884,6 +1017,13 @@ namespace Emby.Dlna.ContentDirectory
};
}
+ ///
+ /// Returns the Movies that are part watched that meet the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetMovieContinueWatching(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -904,136 +1044,213 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the series meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetSeries(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
+ ///
+ /// Returns the Movie folders meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetMovieMovies(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
+ ///
+ /// Returns the Movie collections meeting the criteria.
+ ///
+ /// The see cref="User"/>.
+ /// The see cref="InternalItemsQuery"/>.
+ /// The .
private QueryResult GetMovieCollections(User user, InternalItemsQuery query)
{
query.Recursive = true;
// query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
+ query.IncludeItemTypes = new[] { nameof(BoxSet) };
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
+ ///
+ /// Returns the Music albums meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetMusicAlbums(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
+ query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
+ ///
+ /// Returns the Music songs meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetMusicSongs(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Audio).Name };
+ query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
+ ///
+ /// Returns the songs tagged as favourite that meet the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetFavoriteSongs(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Audio).Name };
+ query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
+ ///
+ /// Returns the series tagged as favourite that meet the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetFavoriteSeries(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
+ ///
+ /// Returns the episodes tagged as favourite that meet the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetFavoriteEpisodes(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
+ ///
+ /// Returns the movies tagged as favourite that meet the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetMovieFavorites(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
+ ///
+ /// /// Returns the albums tagged as favourite that meet the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetFavoriteAlbums(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
+ query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
+ ///
+ /// Returns the genres meeting the criteria.
+ /// The GetGenres.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetGenres(BaseItem parent, User user, InternalItemsQuery query)
{
var genresResult = _libraryManager.GetGenres(new InternalItemsQuery(user)
@@ -1052,6 +1269,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the music genres meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetMusicGenres(BaseItem parent, User user, InternalItemsQuery query)
{
var genresResult = _libraryManager.GetMusicGenres(new InternalItemsQuery(user)
@@ -1070,6 +1294,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the music albums by artist that meet the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetMusicAlbumArtists(BaseItem parent, User user, InternalItemsQuery query)
{
var artists = _libraryManager.GetAlbumArtists(new InternalItemsQuery(user)
@@ -1088,6 +1319,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the music artists meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetMusicArtists(BaseItem parent, User user, InternalItemsQuery query)
{
var artists = _libraryManager.GetArtists(new InternalItemsQuery(user)
@@ -1106,6 +1344,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the artists tagged as favourite that meet the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetFavoriteArtists(BaseItem parent, User user, InternalItemsQuery query)
{
var artists = _libraryManager.GetArtists(new InternalItemsQuery(user)
@@ -1125,6 +1370,12 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the music playlists meeting the criteria.
+ ///
+ /// The user.
+ /// The query.
+ /// The .
private QueryResult GetMusicPlaylists(User user, InternalItemsQuery query)
{
query.Parent = null;
@@ -1137,6 +1388,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the latest music meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetMusicLatest(BaseItem parent, User user, InternalItemsQuery query)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
@@ -1155,6 +1413,12 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(items);
}
+ ///
+ /// Returns the next up item meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetNextUp(BaseItem parent, InternalItemsQuery query)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
@@ -1172,6 +1436,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the latest tv meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetTvLatest(BaseItem parent, User user, InternalItemsQuery query)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
@@ -1181,7 +1452,7 @@ namespace Emby.Dlna.ContentDirectory
{
UserId = user.Id,
Limit = 50,
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false
},
@@ -1190,6 +1461,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(items);
}
+ ///
+ /// Returns the latest movies meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
private QueryResult GetMovieLatest(BaseItem parent, User user, InternalItemsQuery query)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
@@ -1208,6 +1486,16 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(items);
}
+ ///
+ /// Returns music artist items that meet the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// The .
private QueryResult GetMusicArtistItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -1215,7 +1503,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true,
ParentId = parentId,
ArtistIds = new[] { item.Id },
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions()
@@ -1228,6 +1516,16 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the genre items meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// The .
private QueryResult GetGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -1252,6 +1550,16 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ ///
+ /// Returns the music genre items meeting the criteria.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// The .
private QueryResult GetMusicGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -1259,7 +1567,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true,
ParentId = parentId,
GenreIds = new[] { item.Id },
- IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions()
@@ -1272,7 +1580,12 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
- private QueryResult ToResult(BaseItem[] result)
+ ///
+ /// Converts a array into a .
+ ///
+ /// An array of .
+ /// A .
+ private static QueryResult ToResult(BaseItem[] result)
{
var serverItems = result
.Select(i => new ServerItem(i))
@@ -1285,7 +1598,12 @@ namespace Emby.Dlna.ContentDirectory
};
}
- private QueryResult ToResult(QueryResult result)
+ ///
+ /// Converts a to a .
+ ///
+ /// A .
+ /// The .
+ private static QueryResult ToResult(QueryResult result)
{
var serverItems = result
.Items
@@ -1299,7 +1617,13 @@ namespace Emby.Dlna.ContentDirectory
};
}
- private void SetSorting(InternalItemsQuery query, SortCriteria sort, bool isPreSorted)
+ ///
+ /// Sets the sorting method on a query.
+ ///
+ /// The .
+ /// The .
+ /// True if pre-sorted.
+ private static void SetSorting(InternalItemsQuery query, SortCriteria sort, bool isPreSorted)
{
if (isPreSorted)
{
@@ -1311,21 +1635,37 @@ namespace Emby.Dlna.ContentDirectory
}
}
- private QueryResult ApplyPaging(QueryResult result, int? startIndex, int? limit)
+ ///
+ /// Apply paging to a query.
+ ///
+ /// The .
+ /// The start index.
+ /// The maximum number to return.
+ /// A .
+ private static QueryResult ApplyPaging(QueryResult result, int? startIndex, int? limit)
{
result.Items = result.Items.Skip(startIndex ?? 0).Take(limit ?? int.MaxValue).ToArray();
return result;
}
+ ///
+ /// Retrieves the ServerItem id.
+ ///
+ /// The id.
+ /// The .
private ServerItem GetItemFromObjectId(string id)
{
return DidlBuilder.IsIdRoot(id)
-
? new ServerItem(_libraryManager.GetUserRootFolder())
: ParseItemId(id);
}
+ ///
+ /// Parses the item id into a .
+ ///
+ /// The .
+ /// The corresponding .
private ServerItem ParseItemId(string id)
{
StubType? stubType = null;
@@ -1346,8 +1686,8 @@ namespace Emby.Dlna.ContentDirectory
{
if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
{
- stubType = (StubType)Enum.Parse(typeof(StubType), name, true);
- id = id.Split(new[] { '_' }, 2)[1];
+ stubType = Enum.Parse(name, true);
+ id = id.Split('_', 2)[1];
break;
}
diff --git a/Emby.Dlna/ContentDirectory/ServerItem.cs b/Emby.Dlna/ContentDirectory/ServerItem.cs
index e406054149..34244000c1 100644
--- a/Emby.Dlna/ContentDirectory/ServerItem.cs
+++ b/Emby.Dlna/ContentDirectory/ServerItem.cs
@@ -4,8 +4,15 @@ using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory
{
+ ///
+ /// Defines the .
+ ///
internal class ServerItem
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
public ServerItem(BaseItem item)
{
Item = item;
@@ -16,8 +23,14 @@ namespace Emby.Dlna.ContentDirectory
}
}
+ ///
+ /// Gets or sets the underlying base item.
+ ///
public BaseItem Item { get; set; }
+ ///
+ /// Gets or sets the DLNA item type.
+ ///
public StubType? StubType { get; set; }
}
}
diff --git a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs
index 921b14e394..7e3db46519 100644
--- a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs
+++ b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs
@@ -1,13 +1,18 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using Emby.Dlna.Common;
namespace Emby.Dlna.ContentDirectory
{
- public class ServiceActionListBuilder
+ ///
+ /// Defines the .
+ ///
+ public static class ServiceActionListBuilder
{
- public IEnumerable GetActions()
+ ///
+ /// Returns a list of services that this instance provides.
+ ///
+ /// An .
+ public static IEnumerable GetActions()
{
return new[]
{
@@ -22,6 +27,10 @@ namespace Emby.Dlna.ContentDirectory
};
}
+ ///
+ /// Returns the action details for "GetSystemUpdateID".
+ ///
+ /// The .
private static ServiceAction GetGetSystemUpdateIDAction()
{
var action = new ServiceAction
@@ -39,6 +48,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
+ ///
+ /// Returns the action details for "GetSearchCapabilities".
+ ///
+ /// The .
private static ServiceAction GetSearchCapabilitiesAction()
{
var action = new ServiceAction
@@ -56,6 +69,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
+ ///
+ /// Returns the action details for "GetSortCapabilities".
+ ///
+ /// The .
private static ServiceAction GetSortCapabilitiesAction()
{
var action = new ServiceAction
@@ -73,6 +90,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
+ ///
+ /// Returns the action details for "X_GetFeatureList".
+ ///
+ /// The .
private static ServiceAction GetX_GetFeatureListAction()
{
var action = new ServiceAction
@@ -90,6 +111,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
+ ///
+ /// Returns the action details for "Search".
+ ///
+ /// The .
private static ServiceAction GetSearchAction()
{
var action = new ServiceAction
@@ -170,7 +195,11 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
- private ServiceAction GetBrowseAction()
+ ///
+ /// Returns the action details for "Browse".
+ ///
+ /// The .
+ private static ServiceAction GetBrowseAction()
{
var action = new ServiceAction
{
@@ -250,7 +279,11 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
- private ServiceAction GetBrowseByLetterAction()
+ ///
+ /// Returns the action details for "X_BrowseByLetter".
+ ///
+ /// The .
+ private static ServiceAction GetBrowseByLetterAction()
{
var action = new ServiceAction
{
@@ -337,7 +370,11 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
- private ServiceAction GetXSetBookmarkAction()
+ ///
+ /// Returns the action details for "X_SetBookmark".
+ ///
+ /// The .
+ private static ServiceAction GetXSetBookmarkAction()
{
var action = new ServiceAction
{
diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs
index eee405d3e7..187dc1d75a 100644
--- a/Emby.Dlna/ContentDirectory/StubType.cs
+++ b/Emby.Dlna/ContentDirectory/StubType.cs
@@ -1,8 +1,10 @@
#pragma warning disable CS1591
-#pragma warning disable SA1602
namespace Emby.Dlna.ContentDirectory
{
+ ///
+ /// Defines the DLNA item types.
+ ///
public enum StubType
{
Folder = 0,
diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs
index 4ea4e4e48c..8ee6325e9e 100644
--- a/Emby.Dlna/ControlRequest.cs
+++ b/Emby.Dlna/ControlRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs
index d827eef26c..8b09588424 100644
--- a/Emby.Dlna/ControlResponse.cs
+++ b/Emby.Dlna/ControlResponse.cs
@@ -6,9 +6,11 @@ namespace Emby.Dlna
{
public class ControlResponse
{
- public ControlResponse()
+ public ControlResponse(string xml, bool isSuccessful)
{
Headers = new Dictionary();
+ Xml = xml;
+ IsSuccessful = isSuccessful;
}
public IDictionary Headers { get; }
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 5b8a89d8f3..2982ce97e1 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -96,6 +98,7 @@ namespace Emby.Dlna.Didl
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
{
+ // If this using are changed to single lines, then write.Flush needs to be appended before the return.
using (var writer = XmlWriter.Create(builder, settings))
{
// writer.WriteStartDocument();
@@ -123,7 +126,7 @@ namespace Emby.Dlna.Didl
{
foreach (var att in profile.XmlRootAttributes)
{
- var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
+ var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
writer.WriteAttributeString(parts[0], parts[1], null, att.Value);
@@ -207,7 +210,8 @@ namespace Emby.Dlna.Didl
var targetWidth = streamInfo.TargetWidth;
var targetHeight = streamInfo.TargetHeight;
- var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
+ var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
+ _profile,
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(),
@@ -598,7 +602,8 @@ namespace Emby.Dlna.Didl
? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType;
- var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
+ var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
+ _profile,
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
targetAudioBitrate,
@@ -973,15 +978,28 @@ namespace Emby.Dlna.Didl
return;
}
- var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
+ // TODO: Remove these default values
+ var albumArtUrlInfo = GetImageUrl(
+ imageInfo,
+ _profile.MaxAlbumArtWidth ?? 10000,
+ _profile.MaxAlbumArtHeight ?? 10000,
+ "jpg");
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
- writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
- writer.WriteString(albumartUrlInfo.url);
+ if (!string.IsNullOrEmpty(_profile.AlbumArtPn))
+ {
+ writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
+ }
+
+ writer.WriteString(albumArtUrlInfo.url);
writer.WriteFullEndElement();
- // TOOD: Remove these default values
- var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
+ // TODO: Remove these default values
+ var iconUrlInfo = GetImageUrl(
+ imageInfo,
+ _profile.MaxIconWidth ?? 48,
+ _profile.MaxIconHeight ?? 48,
+ "jpg");
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
if (!_profile.EnableAlbumArtInDidl)
@@ -1032,8 +1050,7 @@ namespace Emby.Dlna.Didl
var width = albumartUrlInfo.width ?? maxWidth;
var height = albumartUrlInfo.height ?? maxHeight;
- var contentFeatures = new ContentFeatureBuilder(_profile)
- .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
+ var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
writer.WriteAttributeString(
"protocolInfo",
@@ -1205,8 +1222,7 @@ namespace Emby.Dlna.Didl
if (width.HasValue && height.HasValue)
{
- var newSize = DrawingUtils.Resize(
- new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
+ var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
width = newSize.Width;
height = newSize.Height;
diff --git a/Emby.Dlna/Didl/Filter.cs b/Emby.Dlna/Didl/Filter.cs
index b58fdff2c9..d703f043eb 100644
--- a/Emby.Dlna/Didl/Filter.cs
+++ b/Emby.Dlna/Didl/Filter.cs
@@ -18,7 +18,7 @@ namespace Emby.Dlna.Didl
{
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
- _fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ _fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
}
public bool Contains(string field)
diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs
index 2b86ea333f..b66f53ece2 100644
--- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs
+++ b/Emby.Dlna/Didl/StringWriterWithEncoding.cs
@@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
{
public class StringWriterWithEncoding : StringWriter
{
- private readonly Encoding _encoding;
+ private readonly Encoding? _encoding;
public StringWriterWithEncoding()
{
diff --git a/Emby.Dlna/DlnaConfigurationFactory.cs b/Emby.Dlna/DlnaConfigurationFactory.cs
index 4c6ca869aa..6cc6b73a0c 100644
--- a/Emby.Dlna/DlnaConfigurationFactory.cs
+++ b/Emby.Dlna/DlnaConfigurationFactory.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 1807ac6a13..af70793ccf 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -7,10 +6,12 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
+using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Emby.Dlna.Profiles;
using Emby.Dlna.Server;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -32,9 +33,9 @@ namespace Emby.Dlna
private readonly IXmlSerializer _xmlSerializer;
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly Dictionary> _profiles = new Dictionary>(StringComparer.Ordinal);
@@ -43,14 +44,12 @@ namespace Emby.Dlna
IFileSystem fileSystem,
IApplicationPaths appPaths,
ILoggerFactory loggerFactory,
- IJsonSerializer jsonSerializer,
IServerApplicationHost appHost)
{
_xmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
_appPaths = appPaths;
_logger = loggerFactory.CreateLogger();
- _jsonSerializer = jsonSerializer;
_appHost = appHost;
}
@@ -94,12 +93,14 @@ namespace Emby.Dlna
}
}
+ ///
public DeviceProfile GetDefaultProfile()
{
return new DefaultProfile();
}
- public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
+ ///
+ public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
{
if (deviceInfo == null)
{
@@ -109,13 +110,13 @@ namespace Emby.Dlna
var profile = GetProfiles()
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
- if (profile != null)
+ if (profile == null)
{
- _logger.LogDebug("Found matching device profile: {0}", profile.Name);
+ LogUnmatchedProfile(deviceInfo);
}
else
{
- LogUnmatchedProfile(deviceInfo);
+ _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
}
return profile;
@@ -126,92 +127,57 @@ namespace Emby.Dlna
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
- builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
- builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
- builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
- builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
- builder.Append("ModelName:").AppendLine(profile.ModelName);
- builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
- builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
- builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
+ builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
+ builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
+ builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
+ builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
+ builder.Append("ModelName: ").AppendLine(profile.ModelName);
+ builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
+ builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
+ builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString());
}
- private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
+ ///
+ /// Attempts to match a device with a profile.
+ /// Rules:
+ /// - If the profile field has no value, the field matches irregardless of its contents.
+ /// - the profile field can be an exact match, or a reg exp.
+ ///
+ /// The of the device.
+ /// The of the profile.
+ /// True if they match.
+ public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
{
- if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
- {
- if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
- {
- if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
- {
- if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
- {
- if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.ModelName))
- {
- if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
- {
- if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
- {
- return false;
- }
- }
+ return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
+ && IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
+ && IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
+ && IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
+ && IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
+ && IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
+ && IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
+ && IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
+ }
- if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
+ private bool IsRegexOrSubstringMatch(string input, string pattern)
+ {
+ if (string.IsNullOrEmpty(pattern))
{
- if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
- {
- return false;
- }
+ // In profile identification: An empty pattern matches anything.
+ return true;
}
- if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
+ if (string.IsNullOrEmpty(input))
{
- if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
- {
- return false;
- }
+ // The profile contains a value, and the device doesn't.
+ return false;
}
- return true;
- }
-
- private bool IsRegexOrSubstringMatch(string input, string pattern)
- {
try
{
- return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
+ return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
+ || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
catch (ArgumentException ex)
{
@@ -220,7 +186,8 @@ namespace Emby.Dlna
}
}
- public DeviceProfile GetProfile(IHeaderDictionary headers)
+ ///
+ public DeviceProfile? GetProfile(IHeaderDictionary headers)
{
if (headers == null)
{
@@ -228,15 +195,13 @@ namespace Emby.Dlna
}
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
-
- if (profile != null)
+ if (profile == null)
{
- _logger.LogDebug("Found matching device profile: {0}", profile.Name);
+ _logger.LogDebug("No matching device profile found. {@Headers}", headers);
}
else
{
- var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
- _logger.LogDebug("No matching device profile found. {0}", headerString);
+ _logger.LogDebug("Found matching device profile: {0}", profile.Name);
}
return profile;
@@ -286,19 +251,19 @@ namespace Emby.Dlna
return xmlFies
.Select(i => ParseProfileFile(i, type))
.Where(i => i != null)
- .ToList();
+ .ToList()!; // We just filtered out all the nulls
}
catch (IOException)
{
- return new List();
+ return Array.Empty();
}
}
- private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
+ private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
{
lock (_profiles)
{
- if (_profiles.TryGetValue(path, out Tuple profileTuple))
+ if (_profiles.TryGetValue(path, out Tuple? profileTuple))
{
return profileTuple.Item2;
}
@@ -326,14 +291,20 @@ namespace Emby.Dlna
}
}
- public DeviceProfile GetProfile(string id)
+ ///
+ public DeviceProfile? GetProfile(string id)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
- var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
+ var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
+
+ if (info == null)
+ {
+ return null;
+ }
return ParseProfileFile(info.Path, info.Info.Type);
}
@@ -350,6 +321,7 @@ namespace Emby.Dlna
}
}
+ ///
public IEnumerable GetProfileInfos()
{
return GetProfileInfosInternal().Select(i => i.Info);
@@ -357,17 +329,14 @@ namespace Emby.Dlna
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
{
- return new InternalProfileInfo
- {
- Path = file.FullName,
-
- Info = new DeviceProfileInfo
+ return new InternalProfileInfo(
+ new DeviceProfileInfo
{
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
Name = _fileSystem.GetFileNameWithoutExtension(file),
Type = type
- }
- };
+ },
+ file.FullName);
}
private async Task ExtractSystemProfilesAsync()
@@ -383,11 +352,12 @@ namespace Emby.Dlna
continue;
}
- var filename = Path.GetFileName(name).Substring(namespaceName.Length);
-
- var path = Path.Combine(systemProfilesPath, filename);
+ var path = Path.Join(
+ systemProfilesPath,
+ Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
- using (var stream = _assembly.GetManifestResourceStream(name))
+ // The stream should exist as we just got its name from GetManifestResourceNames
+ using (var stream = _assembly.GetManifestResourceStream(name)!)
{
var fileInfo = _fileSystem.GetFileInfo(path);
@@ -395,7 +365,8 @@ namespace Emby.Dlna
{
Directory.CreateDirectory(systemProfilesPath);
- using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
@@ -407,6 +378,7 @@ namespace Emby.Dlna
Directory.CreateDirectory(UserProfilesPath);
}
+ ///
public void DeleteProfile(string id)
{
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
@@ -424,6 +396,7 @@ namespace Emby.Dlna
}
}
+ ///
public void CreateProfile(DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@@ -439,6 +412,7 @@ namespace Emby.Dlna
SaveProfile(profile, path, DeviceProfileType.User);
}
+ ///
public void UpdateProfile(DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@@ -484,10 +458,10 @@ namespace Emby.Dlna
///
/// Recreates the object using serialization, to ensure it's not a subclass.
- /// If it's a subclass it may not serlialize properly to xml (different root element tag name).
+ /// If it's a subclass it may not serialize properly to xml (different root element tag name).
///
/// The device profile.
- /// The reserialized device profile.
+ /// The re-serialized device profile.
private DeviceProfile ReserializeProfile(DeviceProfile profile)
{
if (profile.GetType() == typeof(DeviceProfile))
@@ -495,11 +469,13 @@ namespace Emby.Dlna
return profile;
}
- var json = _jsonSerializer.SerializeToString(profile);
+ var json = JsonSerializer.Serialize(profile, _jsonOptions);
- return _jsonSerializer.DeserializeFromString(json);
+ // Output can't be null if the input isn't null
+ return JsonSerializer.Deserialize(json, _jsonOptions)!;
}
+ ///
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{
var profile = GetDefaultProfile();
@@ -509,6 +485,7 @@ namespace Emby.Dlna
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
}
+ ///
public ImageStream GetIcon(string filename)
{
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
@@ -526,9 +503,15 @@ namespace Emby.Dlna
private class InternalProfileInfo
{
- internal DeviceProfileInfo Info { get; set; }
+ internal InternalProfileInfo(DeviceProfileInfo info, string path)
+ {
+ Info = info;
+ Path = path;
+ }
+
+ internal DeviceProfileInfo Info { get; }
- internal string Path { get; set; }
+ internal string Path { get; }
}
}
@@ -553,7 +536,7 @@ namespace Emby.Dlna
private void DumpProfiles()
{
- DeviceProfile[] list = new []
+ DeviceProfile[] list = new[]
{
new SamsungSmartTvProfile(),
new XboxOneProfile(),
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 6ed49944c0..970c16d2e6 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -17,24 +17,19 @@
- netstandard2.1
+ net5.0
false
true
- true
+ AllDisabledByDefault
-
-
- ../jellyfin.ruleset
-
-
@@ -78,9 +73,7 @@
-
-
-
+
diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs
index 1b1bd426c5..635d2c47a1 100644
--- a/Emby.Dlna/EventSubscriptionResponse.cs
+++ b/Emby.Dlna/EventSubscriptionResponse.cs
@@ -6,8 +6,10 @@ namespace Emby.Dlna
{
public class EventSubscriptionResponse
{
- public EventSubscriptionResponse()
+ public EventSubscriptionResponse(string content, string contentType)
{
+ Content = content;
+ ContentType = contentType;
Headers = new Dictionary();
}
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index 7d8da86ef9..3c91360904 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -49,11 +51,7 @@ namespace Emby.Dlna.Eventing
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
}
- return new EventSubscriptionResponse
- {
- Content = string.Empty,
- ContentType = "text/plain"
- };
+ return new EventSubscriptionResponse(string.Empty, "text/plain");
}
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
@@ -72,7 +70,8 @@ namespace Emby.Dlna.Eventing
Id = id,
CallbackUrl = callbackUrl,
SubscriptionTime = DateTime.UtcNow,
- TimeoutSeconds = timeout
+ TimeoutSeconds = timeout,
+ NotificationType = notificationType
});
return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout);
@@ -83,7 +82,7 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header))
{
// Starts with SECOND-
- header = header.Split('-').Last();
+ header = header.Split('-')[^1];
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
{
@@ -100,20 +99,12 @@ namespace Emby.Dlna.Eventing
_subscriptions.TryRemove(subscriptionId, out _);
- return new EventSubscriptionResponse
- {
- Content = string.Empty,
- ContentType = "text/plain"
- };
+ return new EventSubscriptionResponse(string.Empty, "text/plain");
}
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
{
- var response = new EventSubscriptionResponse
- {
- Content = string.Empty,
- ContentType = "text/plain"
- };
+ var response = new EventSubscriptionResponse(string.Empty, "text/plain");
response.Headers["SID"] = subscriptionId;
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
@@ -168,7 +159,7 @@ namespace Emby.Dlna.Eventing
builder.Append("");
- using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
+ using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
diff --git a/Emby.Dlna/Eventing/EventSubscription.cs b/Emby.Dlna/Eventing/EventSubscription.cs
index 40d73ee0e5..4fd7f81695 100644
--- a/Emby.Dlna/Eventing/EventSubscription.cs
+++ b/Emby.Dlna/Eventing/EventSubscription.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 40c2cc0e0a..5d252d8dc4 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -1,13 +1,17 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Globalization;
+using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
-using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
+using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -23,11 +27,9 @@ using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
-using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
using Rssdp;
using Rssdp.Infrastructure;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Dlna.Main
{
@@ -50,6 +52,8 @@ namespace Emby.Dlna.Main
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
+ private readonly NetworkConfiguration _netConfig;
+ private readonly bool _disabled;
private PlayToManager _manager;
private SsdpDevicePublisher _publisher;
@@ -120,10 +124,23 @@ namespace Emby.Dlna.Main
httpClientFactory,
config);
Current = this;
+
+ _netConfig = config.GetConfiguration("network");
+ _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
+
+ if (_disabled && _config.GetDlnaConfiguration().EnableServer)
+ {
+ _logger.LogError("The DLNA specification does not support HTTPS.");
+ }
}
public static DlnaEntryPoint Current { get; private set; }
+ ///
+ /// Gets a value indicating whether the dlna server is enabled.
+ ///
+ public static bool Enabled { get; private set; }
+
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
@@ -134,28 +151,35 @@ namespace Emby.Dlna.Main
{
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
- await ReloadComponents().ConfigureAwait(false);
+ if (_disabled)
+ {
+ // No use starting as dlna won't work, as we're running purely on HTTPS.
+ return;
+ }
+
+ ReloadComponents();
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
}
- private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+ private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
{
- await ReloadComponents().ConfigureAwait(false);
+ ReloadComponents();
}
}
- private async Task ReloadComponents()
+ private void ReloadComponents()
{
var options = _config.GetDlnaConfiguration();
+ Enabled = options.EnableServer;
StartSsdpHandler();
if (options.EnableServer)
{
- await StartDevicePublisher(options).ConfigureAwait(false);
+ StartDevicePublisher(options);
}
else
{
@@ -178,8 +202,8 @@ namespace Emby.Dlna.Main
{
if (_communicationsServer == null)
{
- var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
- OperatingSystem.Id == OperatingSystemId.Linux;
+ var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
+ OperatingSystem.IsLinux();
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{
@@ -204,7 +228,10 @@ namespace Emby.Dlna.Main
{
try
{
- ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+ if (communicationsServer != null)
+ {
+ ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+ }
}
catch (Exception ex)
{
@@ -225,7 +252,7 @@ namespace Emby.Dlna.Main
}
}
- public async Task StartDevicePublisher(Configuration.DlnaOptions options)
+ public void StartDevicePublisher(Configuration.DlnaOptions options)
{
if (!options.BlastAliveMessages)
{
@@ -239,13 +266,18 @@ namespace Emby.Dlna.Main
try
{
- _publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
+ _publisher = new SsdpDevicePublisher(
+ _communicationsServer,
+ _networkManager,
+ MediaBrowser.Common.System.OperatingSystem.Name,
+ Environment.OSVersion.VersionString,
+ _config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
LogFunction = LogMessage,
SupportPnpRootDevice = false
};
- await RegisterServerEndpoints().ConfigureAwait(false);
+ RegisterServerEndpoints();
_publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
}
@@ -255,13 +287,22 @@ namespace Emby.Dlna.Main
}
}
- private async Task RegisterServerEndpoints()
+ private void RegisterServerEndpoints()
{
- var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
-
var udn = CreateUuid(_appHost.SystemId);
+ var descriptorUri = "/dlna/" + udn + "/description.xml";
- foreach (var address in addresses)
+ var bindAddresses = NetworkManager.CreateCollection(
+ _networkManager.GetInternalBindAddresses()
+ .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)));
+
+ if (bindAddresses.Count == 0)
+ {
+ // No interfaces returned, so use loopback.
+ bindAddresses = _networkManager.GetLoopbacks();
+ }
+
+ foreach (IPNetAddress address in bindAddresses)
{
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
@@ -270,7 +311,7 @@ namespace Emby.Dlna.Main
}
// Limit to LAN addresses only
- if (!_networkManager.IsAddressInSubnets(address, true, true))
+ if (!_networkManager.IsInLocalNetwork(address))
{
continue;
}
@@ -279,15 +320,20 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
- var descriptorUri = "/dlna/" + udn + "/description.xml";
- var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
+ var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
+ if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
+ {
+ // DLNA will only work over http, so we must reset to http:// : {port}.
+ uri.Scheme = "http";
+ uri.Port = _netConfig.HttpServerPortNumber;
+ }
var device = new SsdpRootDevice
{
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
- Location = uri, // Must point to the URL that serves your devices UPnP description document.
- Address = address,
- SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
+ Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
+ Address = address.Address,
+ PrefixLength = address.PrefixLength,
FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server",
diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
index 464f71a6f1..d8fb127420 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
@@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
}
///
- protected override void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter)
+ protected override void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter)
{
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
{
diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
index 37840cd096..f3789a791c 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using Emby.Dlna.Common;
using Emby.Dlna.Service;
-using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{
diff --git a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
index 1dc9c79c14..56788ae223 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using Emby.Dlna.Common;
-using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index c97acdb026..11fcd81cff 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -12,8 +14,6 @@ using System.Xml;
using System.Xml.Linq;
using Emby.Dlna.Common;
using Emby.Dlna.Ssdp;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo
@@ -221,7 +221,7 @@ namespace Emby.Dlna.PlayTo
{
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
+ var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
if (command == null)
{
return false;
@@ -237,7 +237,13 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("Setting mute");
var value = mute ? 1 : 0;
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ rendererCommands.BuildPost(command, service.ServiceType, value),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
IsMuted = mute;
@@ -255,7 +261,7 @@ namespace Emby.Dlna.PlayTo
{
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
+ var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
if (command == null)
{
return;
@@ -272,7 +278,13 @@ namespace Emby.Dlna.PlayTo
// Remote control will perform better
Volume = value;
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ rendererCommands.BuildPost(command, service.ServiceType, value),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
@@ -280,7 +292,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
+ var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
if (command == null)
{
return;
@@ -293,7 +305,13 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service");
}
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
RestartTimer(true);
@@ -307,7 +325,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
+ var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
if (command == null)
{
return;
@@ -327,14 +345,21 @@ namespace Emby.Dlna.PlayTo
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ post,
+ header: header,
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
- await Task.Delay(50).ConfigureAwait(false);
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
try
{
- await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
+ await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
}
catch
{
@@ -345,7 +370,43 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true);
}
- private string CreateDidlMeta(string value)
+ /*
+ * SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
+ * Without that information, the next track command on the device does not work.
+ */
+ public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
+ {
+ var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
+
+ url = url.Replace("&", "&", StringComparison.Ordinal);
+
+ _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
+
+ var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
+ if (command == null)
+ {
+ return;
+ }
+
+ var dictionary = new Dictionary
+ {
+ { "NextURI", url },
+ { "NextURIMetaData", CreateDidlMeta(metaData) }
+ };
+
+ var service = GetAvTransportService();
+
+ if (service == null)
+ {
+ throw new InvalidOperationException("Unable to find service");
+ }
+
+ var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
+ await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ private static string CreateDidlMeta(string value)
{
if (string.IsNullOrEmpty(value))
{
@@ -380,6 +441,10 @@ namespace Emby.Dlna.PlayTo
public async Task SetPlay(CancellationToken cancellationToken)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
+ if (avCommands == null)
+ {
+ return;
+ }
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
@@ -390,7 +455,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
+ var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
if (command == null)
{
return;
@@ -398,7 +463,13 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ avCommands.BuildPost(command, service.ServiceType, 1),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
RestartTimer(true);
@@ -408,7 +479,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
+ var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
if (command == null)
{
return;
@@ -416,7 +487,13 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ avCommands.BuildPost(command, service.ServiceType, 1),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
TransportState = TransportState.Paused;
@@ -480,7 +557,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
+ // If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive
if (transportState.Value == TransportState.Stopped)
{
RestartTimerInactive();
@@ -530,7 +607,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
+ var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
if (command == null)
{
return;
@@ -580,7 +657,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
+ var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
if (command == null)
{
return;
@@ -667,6 +744,10 @@ namespace Emby.Dlna.PlayTo
}
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
+ if (rendererCommands == null)
+ {
+ return null;
+ }
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
@@ -735,6 +816,11 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
+ if (rendererCommands == null)
+ {
+ return (false, null);
+ }
+
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
@@ -775,7 +861,7 @@ namespace Emby.Dlna.PlayTo
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);
}
@@ -812,7 +898,7 @@ namespace Emby.Dlna.PlayTo
private XElement ParseResponse(string xml)
{
- // Handle different variations sent back by devices
+ // Handle different variations sent back by devices.
try
{
return XElement.Parse(xml);
@@ -821,7 +907,7 @@ namespace Emby.Dlna.PlayTo
{
}
- // first try to add a root node with a dlna namesapce
+ // first try to add a root node with a dlna namespace.
try
{
return XElement.Parse("" + xml + "")
@@ -916,6 +1002,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory);
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
+ if (document == null)
+ {
+ return null;
+ }
AvCommands = TransportCommands.Create(document);
return AvCommands;
@@ -944,6 +1034,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory);
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
+ if (document == null)
+ {
+ return null;
+ }
RendererCommands = TransportCommands.Create(document);
return RendererCommands;
@@ -962,7 +1056,7 @@ namespace Emby.Dlna.PlayTo
url = "/dmr/" + url;
}
- if (!url.StartsWith("/", StringComparison.Ordinal))
+ if (!url.StartsWith('/'))
{
url = "/" + url;
}
@@ -975,6 +1069,10 @@ namespace Emby.Dlna.PlayTo
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
+ if (document == null)
+ {
+ return null;
+ }
var friendlyNames = new List();
@@ -992,7 +1090,7 @@ namespace Emby.Dlna.PlayTo
var deviceProperties = new DeviceInfo()
{
- Name = string.Join(" ", friendlyNames),
+ Name = string.Join(' ', friendlyNames),
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
};
@@ -1162,10 +1260,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
- {
- MediaInfo = mediaInfo
- });
+ PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
}
private void OnPlaybackProgress(UBaseObject mediaInfo)
@@ -1175,27 +1270,17 @@ namespace Emby.Dlna.PlayTo
return;
}
- PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
- {
- MediaInfo = mediaInfo
- });
+ PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo));
}
private void OnPlaybackStop(UBaseObject mediaInfo)
{
- PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
- {
- MediaInfo = mediaInfo
- });
+ PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo));
}
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
{
- MediaChanged?.Invoke(this, new MediaChangedEventArgs
- {
- OldMediaInfo = old,
- NewMediaInfo = newMedia
- });
+ MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia));
}
///
diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs
index d3daab9e0a..2acfff4eb6 100644
--- a/Emby.Dlna/PlayTo/DeviceInfo.cs
+++ b/Emby.Dlna/PlayTo/DeviceInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
index dabd079afd..0f7a524d62 100644
--- a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
+++ b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using System;
@@ -6,6 +6,12 @@ namespace Emby.Dlna.PlayTo
{
public class MediaChangedEventArgs : EventArgs
{
+ public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
+ {
+ OldMediaInfo = oldMediaInfo;
+ NewMediaInfo = newMediaInfo;
+ }
+
public UBaseObject OldMediaInfo { get; set; }
public UBaseObject NewMediaInfo { get; set; }
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index a5b8e2b3ce..0e49fd2c02 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -9,7 +11,6 @@ using System.Threading.Tasks;
using Emby.Dlna.Didl;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@@ -41,7 +42,6 @@ namespace Emby.Dlna.PlayTo
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IConfigurationManager _config;
private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery;
@@ -68,7 +68,6 @@ namespace Emby.Dlna.PlayTo
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
- IConfigurationManager config,
IMediaEncoder mediaEncoder)
{
_session = session;
@@ -84,7 +83,6 @@ namespace Emby.Dlna.PlayTo
_userDataManager = userDataManager;
_localization = localization;
_mediaSourceManager = mediaSourceManager;
- _config = config;
_mediaEncoder = mediaEncoder;
}
@@ -106,6 +104,22 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
}
+ /*
+ * Send a message to the DLNA device to notify what is the next track in the playlist.
+ */
+ private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
+ {
+ if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
+ {
+ // The current playing item is indeed in the play list and we are not yet at the end of the playlist.
+ var nextItemIndex = currentPlayListItemIndex + 1;
+ var nextItem = _playlist[nextItemIndex];
+
+ // Send the SetNextAvTransport message.
+ await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
private void OnDeviceUnavailable()
{
try
@@ -136,7 +150,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{
- if (_disposed)
+ if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
return;
}
@@ -160,6 +174,15 @@ namespace Emby.Dlna.PlayTo
var newItemProgress = GetProgressInfo(streamInfo);
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
+
+ // Send a message to the DLNA device to notify what is the next track in the playlist.
+ var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
+ if (currentItemIndex >= 0)
+ {
+ _currentPlaylistIndex = currentItemIndex;
+ }
+
+ await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
}
catch (Exception ex)
{
@@ -326,7 +349,7 @@ namespace Emby.Dlna.PlayTo
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
{
- _logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
+ _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
@@ -337,25 +360,26 @@ namespace Emby.Dlna.PlayTo
}
var startIndex = command.StartIndex ?? 0;
+ int len = items.Count - startIndex;
if (startIndex > 0)
{
- items = items.Skip(startIndex).ToList();
+ items = items.GetRange(startIndex, len);
}
- var playlist = new List();
- var isFirst = true;
+ var playlist = new PlaylistItem[len];
+
+ // Not nullable enabled - so this is required.
+ playlist[0] = CreatePlaylistItem(
+ items[0],
+ user,
+ command.StartPositionTicks ?? 0,
+ command.MediaSourceId ?? string.Empty,
+ command.AudioStreamIndex,
+ command.SubtitleStreamIndex);
- foreach (var item in items)
+ for (int i = 1; i < len; i++)
{
- if (isFirst && command.StartPositionTicks.HasValue)
- {
- playlist.Add(CreatePlaylistItem(item, user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex));
- isFirst = false;
- }
- else
- {
- playlist.Add(CreatePlaylistItem(item, user, 0, null, null, null));
- }
+ playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
}
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
@@ -428,6 +452,11 @@ namespace Emby.Dlna.PlayTo
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
+
+ // Send a message to the DLNA device to notify what is the next track in the play list.
+ var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+ await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
return;
}
@@ -468,8 +497,8 @@ namespace Emby.Dlna.PlayTo
_dlnaManager.GetDefaultProfile();
var mediaSources = item is IHasMediaSources
- ? _mediaSourceManager.GetStaticMediaSources(item, true, user)
- : new List();
+ ? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray()
+ : Array.Empty();
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
@@ -502,8 +531,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Audio)
{
- return new ContentFeatureBuilder(profile)
- .BuildAudioHeader(
+ return ContentFeatureBuilder.BuildAudioHeader(
+ profile,
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate,
@@ -517,8 +546,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Video)
{
- var list = new ContentFeatureBuilder(profile)
- .BuildVideoHeader(
+ var list = ContentFeatureBuilder.BuildVideoHeader(
+ profile,
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(),
@@ -548,7 +577,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private PlaylistItem GetPlaylistItem(BaseItem item, List mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
+ private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
{
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
@@ -557,7 +586,7 @@ namespace Emby.Dlna.PlayTo
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
{
ItemId = item.Id,
- MediaSources = mediaSources.ToArray(),
+ MediaSources = mediaSources,
Profile = profile,
DeviceId = deviceId,
MaxBitrate = profile.MaxStreamingBitrate,
@@ -577,7 +606,7 @@ namespace Emby.Dlna.PlayTo
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
{
ItemId = item.Id,
- MediaSources = mediaSources.ToArray(),
+ MediaSources = mediaSources,
Profile = profile,
DeviceId = deviceId,
MaxBitrate = profile.MaxStreamingBitrate,
@@ -590,7 +619,7 @@ namespace Emby.Dlna.PlayTo
if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
{
- return new PlaylistItemFactory().Create((Photo)item, profile);
+ return PlaylistItemFactory.Create((Photo)item, profile);
}
throw new ArgumentException("Unrecognized item type.");
@@ -626,6 +655,9 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
+ // Send a message to the DLNA device to notify what is the next track in the play list.
+ await SendNextTrackMessage(index, cancellationToken);
+
var streamInfo = currentitem.StreamInfo;
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
{
@@ -739,6 +771,10 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
+ // Send a message to the DLNA device to notify what is the next track in the play list.
+ var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+ await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
if (EnableClientSideSeek(newItem.StreamInfo))
{
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
@@ -764,6 +800,10 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
+ // Send a message to the DLNA device to notify what is the next track in the play list.
+ var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+ await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
{
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
@@ -774,13 +814,14 @@ namespace Emby.Dlna.PlayTo
private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken)
{
- const int maxWait = 15000000;
- const int interval = 500;
+ const int MaxWait = 15000000;
+ const int Interval = 500;
+
var currentWait = 0;
- while (_device.TransportState != TransportState.Playing && currentWait < maxWait)
+ while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
{
- await Task.Delay(interval).ConfigureAwait(false);
- currentWait += interval;
+ await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
+ currentWait += Interval;
}
await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
@@ -828,7 +869,7 @@ namespace Emby.Dlna.PlayTo
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
- if (name == SessionMessageType.PlayState)
+ if (name == SessionMessageType.Playstate)
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
@@ -898,16 +939,16 @@ namespace Emby.Dlna.PlayTo
var parts = url.Split('/');
- for (var i = 0; i < parts.Length; i++)
+ for (var i = 0; i < parts.Length - 1; i++)
{
var part = parts[i];
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
{
- if (parts.Length > i + 1)
+ if (Guid.TryParse(parts[i + 1], out var result))
{
- return Guid.Parse(parts[i + 1]);
+ return result;
}
}
}
@@ -946,7 +987,6 @@ namespace Emby.Dlna.PlayTo
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
-
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index e93aef3043..35bf5927c4 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -1,15 +1,15 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.Linq;
-using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
@@ -89,13 +89,10 @@ namespace Emby.Dlna.PlayTo
nt = string.Empty;
}
- string location = info.Location.ToString();
-
// It has to report that it's a media renderer
- if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
- nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
+ if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)
+ && !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase))
{
- // _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
return;
}
@@ -115,7 +112,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- await AddDevice(info, location, cancellationToken).ConfigureAwait(false);
+ await AddDevice(info, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -130,39 +127,50 @@ namespace Emby.Dlna.PlayTo
}
}
- private static string GetUuid(string usn)
+ internal static string GetUuid(string usn)
{
const string UuidStr = "uuid:";
const string UuidColonStr = "::";
var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
+ if (index == -1)
+ {
+ return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ }
+
+ ReadOnlySpan tmp = usn.AsSpan()[(index + UuidStr.Length)..];
+
+ index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- return usn.Substring(index + UuidStr.Length);
+ tmp = tmp[..index];
}
- index = usn.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
+ index = tmp.IndexOf('{');
if (index != -1)
{
- usn = usn.Substring(0, index + UuidColonStr.Length);
+ int endIndex = tmp.IndexOf('}');
+ if (endIndex != -1)
+ {
+ tmp = tmp[(index + 1)..endIndex];
+ }
}
- return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ return tmp.ToString();
}
- private async Task AddDevice(UpnpDeviceInfo info, string location, CancellationToken cancellationToken)
+ private async Task AddDevice(UpnpDeviceInfo info, CancellationToken cancellationToken)
{
var uri = info.Location;
- _logger.LogDebug("Attempting to create PlayToController from location {0}", location);
+ _logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
- _logger.LogDebug("Logging session activity from location {0}", location);
if (info.Headers.TryGetValue("USN", out string uuid))
{
uuid = GetUuid(uuid);
}
else
{
- uuid = location.GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
@@ -172,20 +180,17 @@ namespace Emby.Dlna.PlayTo
if (controller == null)
{
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
+ if (device == null)
+ {
+ _logger.LogError("Ignoring device as xml response is invalid.");
+ return;
+ }
string deviceName = device.Properties.Name;
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
- string serverAddress;
- if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IPAddress.Any) || info.LocalIpAddress.Equals(IPAddress.IPv6Any))
- {
- serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
- }
- else
- {
- serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
- }
+ string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
controller = new PlayToController(
sessionInfo,
@@ -201,7 +206,6 @@ namespace Emby.Dlna.PlayTo
_userDataManager,
_localization,
_mediaSourceManager,
- _config,
_mediaEncoder);
sessionInfo.AddController(controller);
diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
index d14617c8a0..c95d8b1e84 100644
--- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
@@ -6,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackProgressEventArgs : EventArgs
{
+ public PlaybackProgressEventArgs(UBaseObject mediaInfo)
+ {
+ MediaInfo = mediaInfo;
+ }
+
public UBaseObject MediaInfo { get; set; }
}
}
diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
index 3f8d552636..619c861ed9 100644
--- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
@@ -6,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStartEventArgs : EventArgs
{
+ public PlaybackStartEventArgs(UBaseObject mediaInfo)
+ {
+ MediaInfo = mediaInfo;
+ }
+
public UBaseObject MediaInfo { get; set; }
}
}
diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
index deeb47918d..d0ec250591 100644
--- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
@@ -6,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStoppedEventArgs : EventArgs
{
+ public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
+ {
+ MediaInfo = mediaInfo;
+ }
+
public UBaseObject MediaInfo { get; set; }
}
}
diff --git a/Emby.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs
index 85846166cf..5056e69ae7 100644
--- a/Emby.Dlna/PlayTo/PlaylistItem.cs
+++ b/Emby.Dlna/PlayTo/PlaylistItem.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.Dlna;
diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
index bedc8b9ad3..6574913032 100644
--- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
+++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
@@ -8,9 +10,9 @@ using MediaBrowser.Model.Session;
namespace Emby.Dlna.PlayTo
{
- public class PlaylistItemFactory
+ public static class PlaylistItemFactory
{
- public PlaylistItem Create(Photo item, DeviceProfile profile)
+ public static PlaylistItem Create(Photo item, DeviceProfile profile)
{
var playlistItem = new PlaylistItem
{
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index c8c36fc972..f14f73bb6f 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -1,10 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Globalization;
-using System.IO;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Threading;
@@ -45,11 +45,11 @@ namespace Emby.Dlna.PlayTo
header,
cancellationToken)
.ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- using var reader = new StreamReader(stream, Encoding.UTF8);
- return XDocument.Parse(
- await reader.ReadToEndAsync().ConfigureAwait(false),
- LoadOptions.PreserveWhitespace);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ return await XDocument.LoadAsync(
+ stream,
+ LoadOptions.PreserveWhitespace,
+ cancellationToken).ConfigureAwait(false);
}
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@@ -60,7 +60,7 @@ namespace Emby.Dlna.PlayTo
return serviceUrl;
}
- if (!serviceUrl.StartsWith("/", StringComparison.Ordinal))
+ if (!serviceUrl.StartsWith('/'))
{
serviceUrl = "/" + serviceUrl;
}
@@ -94,11 +94,18 @@ namespace Emby.Dlna.PlayTo
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- using var reader = new StreamReader(stream, Encoding.UTF8);
- return XDocument.Parse(
- await reader.ReadToEndAsync().ConfigureAwait(false),
- LoadOptions.PreserveWhitespace);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ return await XDocument.LoadAsync(
+ stream,
+ LoadOptions.PreserveWhitespace,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ return null;
+ }
}
private async Task PostSoapDataAsync(
diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs
index fda17a8b41..b58669355d 100644
--- a/Emby.Dlna/PlayTo/TransportCommands.cs
+++ b/Emby.Dlna/PlayTo/TransportCommands.cs
@@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
public class TransportCommands
{
private const string CommandBase = "\r\n" + "" + "" + "" + "{2}" + "" + "";
- private List _stateVariables = new List();
- private List _serviceActions = new List();
- public List StateVariables => _stateVariables;
+ public List StateVariables { get; } = new List();
- public List ServiceActions => _serviceActions;
+ public List ServiceActions { get; } = new List();
public static TransportCommands Create(XDocument document)
{
@@ -48,7 +46,7 @@ namespace Emby.Dlna.PlayTo
{
var serviceAction = new ServiceAction
{
- Name = container.GetValue(UPnpNamespaces.Svc + "name"),
+ Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
};
var argumentList = serviceAction.ArgumentList;
@@ -70,15 +68,15 @@ namespace Emby.Dlna.PlayTo
return new Argument
{
- Name = container.GetValue(UPnpNamespaces.Svc + "name"),
- Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
- RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
+ Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
+ Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
+ RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
};
}
private static StateVariable FromXml(XElement container)
{
- var allowedValues = new List();
+ var allowedValues = Array.Empty();
var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
.FirstOrDefault();
@@ -86,14 +84,14 @@ namespace Emby.Dlna.PlayTo
{
var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
- allowedValues.AddRange(values.Select(child => child.Value));
+ allowedValues = values.Select(child => child.Value).ToArray();
}
return new StateVariable
{
- Name = container.GetValue(UPnpNamespaces.Svc + "name"),
- DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
- AllowedValues = allowedValues.ToArray()
+ Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
+ DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
+ AllowedValues = allowedValues
};
}
@@ -103,12 +101,12 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList)
{
- if (arg.Direction == "out")
+ if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
{
continue;
}
- if (arg.Name == "InstanceID")
+ if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
{
stateString += BuildArgumentXml(arg, "0");
}
@@ -127,12 +125,12 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList)
{
- if (arg.Direction == "out")
+ if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
{
continue;
}
- if (arg.Name == "InstanceID")
+ if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
{
stateString += BuildArgumentXml(arg, "0");
}
@@ -151,7 +149,7 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList)
{
- if (arg.Name == "InstanceID")
+ if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
{
stateString += BuildArgumentXml(arg, "0");
}
@@ -168,7 +166,7 @@ namespace Emby.Dlna.PlayTo
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
}
- private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
+ private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
{
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs
index 7068a5d24d..2058e9dc79 100644
--- a/Emby.Dlna/PlayTo/TransportState.cs
+++ b/Emby.Dlna/PlayTo/TransportState.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1602
namespace Emby.Dlna.PlayTo
{
diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs
index 0d9478e42e..02d2da58d6 100644
--- a/Emby.Dlna/PlayTo/uBaseObject.cs
+++ b/Emby.Dlna/PlayTo/uBaseObject.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs
index d4af72b626..8eaf12ba9a 100644
--- a/Emby.Dlna/Profiles/DefaultProfile.cs
+++ b/Emby.Dlna/Profiles/DefaultProfile.cs
@@ -1,5 +1,7 @@
#pragma warning disable CS1591
+using System;
+using System.Globalization;
using System.Linq;
using MediaBrowser.Model.Dlna;
@@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
{
public DefaultProfile()
{
+ Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
Name = "Generic Device";
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
diff --git a/Emby.Dlna/Profiles/SonyBravia2010Profile.cs b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs
index 8ab4acd1be..9f0d82b8f8 100644
--- a/Emby.Dlna/Profiles/SonyBravia2010Profile.cs
+++ b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
Identification = new DeviceIdentification
{
- FriendlyName = @"KDL-\d{2}[EHLNPB]X\d[01]\d.*",
+ FriendlyName = @"KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*",
Manufacturer = "Sony",
Headers = new[]
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
new HttpHeaderInfo
{
Name = "X-AV-Client-Info",
- Value = @".*KDL-\d{2}[EHLNPB]X\d[01]\d.*",
+ Value = @".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*",
Match = HeaderMatchType.Regex
}
}
diff --git a/Emby.Dlna/Profiles/SonyBravia2011Profile.cs b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs
index 42d253394c..dfb91817ac 100644
--- a/Emby.Dlna/Profiles/SonyBravia2011Profile.cs
+++ b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
Identification = new DeviceIdentification
{
- FriendlyName = @"KDL-\d{2}([A-Z]X\d2\d|CX400).*",
+ FriendlyName = @"KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*",
Manufacturer = "Sony",
Headers = new[]
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
new HttpHeaderInfo
{
Name = "X-AV-Client-Info",
- Value = @".*KDL-\d{2}([A-Z]X\d2\d|CX400).*",
+ Value = @".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*",
Match = HeaderMatchType.Regex
}
}
diff --git a/Emby.Dlna/Profiles/SonyBravia2012Profile.cs b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs
index 0598e83422..d59ee38d74 100644
--- a/Emby.Dlna/Profiles/SonyBravia2012Profile.cs
+++ b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
Identification = new DeviceIdentification
{
- FriendlyName = @"KDL-\d{2}[A-Z]X\d5(\d|G).*",
+ FriendlyName = @"KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*",
Manufacturer = "Sony",
Headers = new[]
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
new HttpHeaderInfo
{
Name = "X-AV-Client-Info",
- Value = @".*KDL-\d{2}[A-Z]X\d5(\d|G).*",
+ Value = @".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*",
Match = HeaderMatchType.Regex
}
}
diff --git a/Emby.Dlna/Profiles/SonyBravia2013Profile.cs b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs
index 3d90a1e72d..73b0fd67eb 100644
--- a/Emby.Dlna/Profiles/SonyBravia2013Profile.cs
+++ b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
Identification = new DeviceIdentification
{
- FriendlyName = @"KDL-\d{2}[WR][5689]\d{2}A.*",
+ FriendlyName = @"KDL-[0-9]{2}[WR][5689][0-9]{2}A.*",
Manufacturer = "Sony",
Headers = new[]
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
new HttpHeaderInfo
{
Name = "X-AV-Client-Info",
- Value = @".*KDL-\d{2}[WR][5689]\d{2}A.*",
+ Value = @".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*",
Match = HeaderMatchType.Regex
}
}
diff --git a/Emby.Dlna/Profiles/SonyBravia2014Profile.cs b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs
index 9188f73ef1..db8ee5750f 100644
--- a/Emby.Dlna/Profiles/SonyBravia2014Profile.cs
+++ b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
Identification = new DeviceIdentification
{
- FriendlyName = @"(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*",
+ FriendlyName = @"(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*",
Manufacturer = "Sony",
Headers = new[]
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
new HttpHeaderInfo
{
Name = "X-AV-Client-Info",
- Value = @".*(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*",
+ Value = @".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*",
Match = HeaderMatchType.Regex
}
}
diff --git a/Emby.Dlna/Profiles/SonyPs3Profile.cs b/Emby.Dlna/Profiles/SonyPs3Profile.cs
index d56b1df507..e4a7a3a596 100644
--- a/Emby.Dlna/Profiles/SonyPs3Profile.cs
+++ b/Emby.Dlna/Profiles/SonyPs3Profile.cs
@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts",
Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264",
- AudioCodec = "ac3,mp2,mp3,aac"
+ AudioCodec = "aac,ac3,mp2"
},
new DirectPlayProfile
{
@@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles
{
Container = "ts",
VideoCodec = "h264",
- AudioCodec = "ac3,aac,mp3",
+ AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video
},
new TranscodingProfile
diff --git a/Emby.Dlna/Profiles/SonyPs4Profile.cs b/Emby.Dlna/Profiles/SonyPs4Profile.cs
index db56094e2b..985df0c9a5 100644
--- a/Emby.Dlna/Profiles/SonyPs4Profile.cs
+++ b/Emby.Dlna/Profiles/SonyPs4Profile.cs
@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts",
Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264",
- AudioCodec = "ac3,mp2,mp3,aac"
+ AudioCodec = "aac,ac3,mp2"
},
new DirectPlayProfile
{
@@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles
{
Container = "ts",
VideoCodec = "h264",
- AudioCodec = "mp3",
+ AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video
},
new TranscodingProfile
diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml
index f20e9fcb6f..1461db3117 100644
--- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml
+++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml
@@ -3,10 +3,10 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
Sony Bravia (2010)
- KDL-\d{2}[EHLNPB]X\d[01]\d.*
+ KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*
Sony
-
+
Microsoft Corporation
diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml
index e516ff512d..7c5f2b1817 100644
--- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml
+++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml
@@ -3,10 +3,10 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
Sony Bravia (2011)
- KDL-\d{2}([A-Z]X\d2\d|CX400).*
+ KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*
Sony
-
+
Microsoft Corporation
diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml
index 88bd1c2f53..842a8fba33 100644
--- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml
+++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml
@@ -3,10 +3,10 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
Sony Bravia (2012)
- KDL-\d{2}[A-Z]X\d5(\d|G).*
+ KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*
Sony
-
+
Microsoft Corporation
diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml
index 3ca9893cdc..f1135c3fe3 100644
--- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml
+++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml
@@ -3,10 +3,10 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
Sony Bravia (2013)
- KDL-\d{2}[WR][5689]\d{2}A.*
+ KDL-[0-9]{2}[WR][5689][0-9]{2}A.*
Sony
-
+
Microsoft Corporation
diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml
index 8804a75dfa..85c7868c66 100644
--- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml
+++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml
@@ -3,10 +3,10 @@
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
Sony Bravia (2014)
- (KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*
+ (KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*
Sony
-
+
Microsoft Corporation
diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
index bafa44b828..129b188e2a 100644
--- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
+++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
@@ -38,7 +38,7 @@
-
+
@@ -46,7 +46,7 @@
-
+
diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
index eb8e645b31..592119305e 100644
--- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
+++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
@@ -38,7 +38,7 @@
-
+
@@ -46,7 +46,7 @@
-
+
diff --git a/Emby.Dlna/Properties/AssemblyInfo.cs b/Emby.Dlna/Properties/AssemblyInfo.cs
index a2c1e0db8b..606ffcf4fd 100644
--- a/Emby.Dlna/Properties/AssemblyInfo.cs
+++ b/Emby.Dlna/Properties/AssemblyInfo.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
@@ -13,6 +14,7 @@ using System.Resources;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("Jellyfin.Dlna.Tests")]
// Version information for an assembly consists of the following four values:
//
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index bca9e81cd0..3f3dfccd3a 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -40,8 +40,6 @@ namespace Emby.Dlna.Server
_serverId = serverId;
}
- private static bool EnableAbsoluteUrls => false;
-
public string GetXml()
{
var builder = new StringBuilder();
@@ -75,13 +73,6 @@ namespace Emby.Dlna.Server
builder.Append("0");
builder.Append("");
- if (!EnableAbsoluteUrls)
- {
- builder.Append("")
- .Append(SecurityElement.Escape(_serverAddress))
- .Append("");
- }
-
AppendDeviceInfo(builder);
builder.Append("");
@@ -257,16 +248,10 @@ namespace Emby.Dlna.Server
return string.Empty;
}
- url = url.TrimStart('/');
-
- url = "/dlna/" + _serverUdn + "/" + url;
-
- if (EnableAbsoluteUrls)
- {
- url = _serverAddress.TrimEnd('/') + url;
- }
+ url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
- return SecurityElement.Escape(url);
+ // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
+ return SecurityElement.Escape(url) ?? string.Empty;
}
private IEnumerable GetIcons()
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 4b0bbba969..581e4a2861 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -6,9 +6,9 @@ using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
+using Diacritics.Extensions;
using Emby.Dlna.Didl;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Extensions;
using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Service
@@ -47,9 +47,9 @@ namespace Emby.Dlna.Service
private async Task ProcessControlRequestInternalAsync(ControlRequest request)
{
- ControlRequestInfo requestInfo = null;
+ ControlRequestInfo? requestInfo = null;
- using (var streamReader = new StreamReader(request.InputXml))
+ using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
{
var readerSettings = new XmlReaderSettings()
{
@@ -95,11 +95,7 @@ namespace Emby.Dlna.Service
var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
- var controlResponse = new ControlResponse
- {
- Xml = xml,
- IsSuccessful = true
- };
+ var controlResponse = new ControlResponse(xml, true);
controlResponse.Headers.Add("EXT", string.Empty);
@@ -151,7 +147,7 @@ namespace Emby.Dlna.Service
private async Task ParseBodyTagAsync(XmlReader reader)
{
- string namespaceURI = null, localName = null;
+ string? namespaceURI = null, localName = null;
await reader.MoveToContentAsync().ConfigureAwait(false);
await reader.ReadAsync().ConfigureAwait(false);
@@ -169,6 +165,7 @@ namespace Emby.Dlna.Service
var result = new ControlRequestInfo(localName, namespaceURI);
using var subReader = reader.ReadSubtree();
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
+ return result;
}
else
{
@@ -209,7 +206,7 @@ namespace Emby.Dlna.Service
}
}
- protected abstract void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter);
+ protected abstract void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter);
private void LogRequest(ControlRequest request)
{
diff --git a/Emby.Dlna/Service/ControlErrorHandler.cs b/Emby.Dlna/Service/ControlErrorHandler.cs
index f2b5dd9ca8..3e2cd6d2e4 100644
--- a/Emby.Dlna/Service/ControlErrorHandler.cs
+++ b/Emby.Dlna/Service/ControlErrorHandler.cs
@@ -46,11 +46,7 @@ namespace Emby.Dlna.Service
writer.WriteEndDocument();
}
- return new ControlResponse
- {
- Xml = builder.ToString(),
- IsSuccessful = false
- };
+ return new ControlResponse(builder.ToString(), false);
}
}
}
diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
index 8c7d961f3e..391dda1479 100644
--- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs
+++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -69,7 +71,7 @@ namespace Emby.Dlna.Ssdp
{
lock (_syncLock)
{
- if (_listenerCount > 0 && _deviceLocator == null)
+ if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
{
_deviceLocator = new SsdpDeviceLocator(_commsServer);
@@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp
{
Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers,
- LocalIpAddress = e.LocalIpAddress
+ RemoteIpAddress = e.RemoteIpAddress
});
DeviceDiscoveredInternal?.Invoke(this, args);
diff --git a/Emby.Dlna/Ssdp/SsdpExtensions.cs b/Emby.Dlna/Ssdp/SsdpExtensions.cs
index e7a52f168f..d00eb02b46 100644
--- a/Emby.Dlna/Ssdp/SsdpExtensions.cs
+++ b/Emby.Dlna/Ssdp/SsdpExtensions.cs
@@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp
{
public static class SsdpExtensions
{
- public static string GetValue(this XElement container, XName name)
+ public static string? GetValue(this XElement container, XName name)
{
var node = container.Element(name);
return node?.Value;
}
- public static string GetAttributeValue(this XElement container, XName name)
+ public static string? GetAttributeValue(this XElement container, XName name)
{
var node = container.Attribute(name);
return node?.Value;
}
- public static string GetDescendantValue(this XElement container, XName name)
+ public static string? GetDescendantValue(this XElement container, XName name)
=> container.Descendants(name).FirstOrDefault()?.Value;
}
}
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj
index 092f8580a6..baf350c6f1 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -6,11 +6,10 @@
- netstandard2.1
+ net5.0
false
true
- true
- enable
+ AllDisabledByDefault
@@ -25,14 +24,9 @@
-
-
- ../jellyfin.ruleset
-
-
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 8a2301d2d6..7d952aa23b 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
@@ -171,21 +172,31 @@ namespace Emby.Drawing
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
- ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
int quality = options.Quality;
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
- string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
+ string cacheFilePath = GetCacheFilePath(
+ originalImagePath,
+ options.Width,
+ options.Height,
+ options.MaxWidth,
+ options.MaxHeight,
+ options.FillWidth,
+ options.FillHeight,
+ quality,
+ dateModified,
+ outputFormat,
+ options.AddPlayedIndicator,
+ options.PercentPlayed,
+ options.UnplayedCount,
+ options.Blur,
+ options.BackgroundColor,
+ options.ForegroundLayer);
try
{
if (!File.Exists(cacheFilePath))
{
- if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
- {
- options.CropWhiteSpace = false;
- }
-
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
@@ -246,48 +257,111 @@ namespace Emby.Drawing
///
/// Gets the cache file path based on a set of parameters.
///
- 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,
+ int? width,
+ int? height,
+ int? maxWidth,
+ int? maxHeight,
+ int? fillWidth,
+ int? fillHeight,
+ int quality,
+ DateTime dateModified,
+ ImageFormat format,
+ bool addPlayedIndicator,
+ double percentPlayed,
+ int? unwatchedCount,
+ int? blur,
+ string backgroundColor,
+ string foregroundLayer)
{
- var filename = originalPath
- + "width=" + outputSize.Width
- + "height=" + outputSize.Height
- + "quality=" + quality
- + "datemodified=" + dateModified.Ticks
- + "f=" + format;
+ var filename = new StringBuilder(256);
+ filename.Append(originalPath);
+
+ filename.Append(",quality=");
+ filename.Append(quality);
+
+ filename.Append(",datemodified=");
+ filename.Append(dateModified.Ticks);
+
+ filename.Append(",f=");
+ filename.Append(format);
+
+ if (width.HasValue)
+ {
+ filename.Append(",width=");
+ filename.Append(width.Value);
+ }
+
+ if (height.HasValue)
+ {
+ filename.Append(",height=");
+ filename.Append(height.Value);
+ }
+
+ if (maxWidth.HasValue)
+ {
+ filename.Append(",maxwidth=");
+ filename.Append(maxWidth.Value);
+ }
+
+ if (maxHeight.HasValue)
+ {
+ filename.Append(",maxheight=");
+ filename.Append(maxHeight.Value);
+ }
+
+ if (fillWidth.HasValue)
+ {
+ filename.Append(",fillwidth=");
+ filename.Append(fillWidth.Value);
+ }
+
+ if (fillHeight.HasValue)
+ {
+ filename.Append(",fillheight=");
+ filename.Append(fillHeight.Value);
+ }
if (addPlayedIndicator)
{
- filename += "pl=true";
+ filename.Append(",pl=true");
}
if (percentPlayed > 0)
{
- filename += "p=" + percentPlayed;
+ filename.Append(",p=");
+ filename.Append(percentPlayed);
}
if (unwatchedCount.HasValue)
{
- filename += "p=" + unwatchedCount.Value;
+ filename.Append(",p=");
+ filename.Append(unwatchedCount.Value);
}
if (blur.HasValue)
{
- filename += "blur=" + blur.Value;
+ filename.Append(",blur=");
+ filename.Append(blur.Value);
}
if (!string.IsNullOrEmpty(backgroundColor))
{
- filename += "b=" + backgroundColor;
+ filename.Append(",b=");
+ filename.Append(backgroundColor);
}
if (!string.IsNullOrEmpty(foregroundLayer))
{
- filename += "fl=" + foregroundLayer;
+ filename.Append(",fl=");
+ filename.Append(foregroundLayer);
}
- filename += "v=" + Version;
+ filename.Append(",v=");
+ filename.Append(Version);
- return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant());
+ return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
}
///
@@ -352,8 +426,13 @@ namespace Emby.Drawing
}
///
- public string GetImageCacheTag(User user)
+ public string? GetImageCacheTag(User user)
{
+ if (user.ProfileImage == null)
+ {
+ return null;
+ }
+
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
.ToString("N", CultureInfo.InvariantCulture);
}
diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs
index 2a1cfd3da5..1c05aa9161 100644
--- a/Emby.Drawing/NullImageEncoder.cs
+++ b/Emby.Drawing/NullImageEncoder.cs
@@ -32,7 +32,7 @@ namespace Emby.Drawing
=> throw new NotImplementedException();
///
- 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 outputFormat)
{
throw new NotImplementedException();
}
diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs
index b63be3a647..bbfdccc902 100644
--- a/Emby.Naming/Audio/AlbumParser.cs
+++ b/Emby.Naming/Audio/AlbumParser.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System;
using System.Globalization;
using System.IO;
@@ -9,15 +6,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.Audio
{
+ ///
+ /// Helper class to determine if Album is multipart.
+ ///
public class AlbumParser
{
private readonly NamingOptions _options;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Naming options containing AlbumStackingPrefixes.
public AlbumParser(NamingOptions options)
{
_options = options;
}
+ ///
+ /// Function that determines if album is multipart.
+ ///
+ /// Path to file.
+ /// True if album is multipart.
public bool IsMultiPart(string path)
{
var filename = Path.GetFileName(path);
diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs
index 6b2f4be93e..2b610ec796 100644
--- a/Emby.Naming/Audio/AudioFileParser.cs
+++ b/Emby.Naming/Audio/AudioFileParser.cs
@@ -1,19 +1,25 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Audio
{
+ ///
+ /// Static helper class to determine if file at path is audio file.
+ ///
public static class AudioFileParser
{
+ ///
+ /// Static helper method to determine if file at path is audio file.
+ ///
+ /// Path to file.
+ /// containing AudioFileExtensions.
+ /// True if file at path is audio file.
public static bool IsAudioFile(string path, NamingOptions options)
{
- var extension = Path.GetExtension(path);
- return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ var extension = Path.GetExtension(path.AsSpan());
+ return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookFileInfo.cs b/Emby.Naming/AudioBook/AudioBookFileInfo.cs
index c4863b50ab..862e396677 100644
--- a/Emby.Naming/AudioBook/AudioBookFileInfo.cs
+++ b/Emby.Naming/AudioBook/AudioBookFileInfo.cs
@@ -7,6 +7,21 @@ namespace Emby.Naming.AudioBook
///
public class AudioBookFileInfo : IComparable
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Path to audiobook file.
+ /// File type.
+ /// Number of part this file represents.
+ /// Number of chapter this file represents.
+ public AudioBookFileInfo(string path, string container, int? partNumber = default, int? chapterNumber = default)
+ {
+ Path = path;
+ Container = container;
+ PartNumber = partNumber;
+ ChapterNumber = chapterNumber;
+ }
+
///
/// Gets or sets the path.
///
@@ -31,14 +46,8 @@ namespace Emby.Naming.AudioBook
/// The chapter number.
public int? ChapterNumber { get; set; }
- ///
- /// Gets or sets a value indicating whether this instance is a directory.
- ///
- /// The type.
- public bool IsDirectory { get; set; }
-
///
- public int CompareTo(AudioBookFileInfo other)
+ public int CompareTo(AudioBookFileInfo? other)
{
if (ReferenceEquals(this, other))
{
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
index 14edd64926..7b4429ab15 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
@@ -8,15 +5,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.AudioBook
{
+ ///
+ /// Parser class to extract part and/or chapter number from audiobook filename.
+ ///
public class AudioBookFilePathParser
{
private readonly NamingOptions _options;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Naming options containing AudioBookPartsExpressions.
public AudioBookFilePathParser(NamingOptions options)
{
_options = options;
}
+ ///
+ /// Based on regex determines if filename includes part/chapter number.
+ ///
+ /// Path to audiobook file.
+ /// Returns object.
public AudioBookFilePathParserResult Parse(string path)
{
AudioBookFilePathParserResult result = default;
@@ -52,8 +61,6 @@ namespace Emby.Naming.AudioBook
}
}
- result.Success = result.ChapterNumber.HasValue || result.PartNumber.HasValue;
-
return result;
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
index 7bfc4479d2..48ab8b57dc 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
@@ -1,14 +1,18 @@
-#nullable enable
-#pragma warning disable CS1591
-
namespace Emby.Naming.AudioBook
{
+ ///
+ /// Data object for passing result of audiobook part/chapter extraction.
+ ///
public struct AudioBookFilePathParserResult
{
+ ///
+ /// Gets or sets optional number of path extracted from audiobook filename.
+ ///
public int? PartNumber { get; set; }
+ ///
+ /// Gets or sets optional number of chapter extracted from audiobook filename.
+ ///
public int? ChapterNumber { get; set; }
-
- public bool Success { get; set; }
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs
index b0b5bd881f..acd8905af6 100644
--- a/Emby.Naming/AudioBook/AudioBookInfo.cs
+++ b/Emby.Naming/AudioBook/AudioBookInfo.cs
@@ -10,11 +10,18 @@ namespace Emby.Naming.AudioBook
///
/// Initializes a new instance of the class.
///
- public AudioBookInfo()
+ /// Name of audiobook.
+ /// Year of audiobook release.
+ /// List of files composing the actual audiobook.
+ /// List of extra files.
+ /// Alternative version of files.
+ public AudioBookInfo(string name, int? year, IReadOnlyList files, IReadOnlyList extras, IReadOnlyList alternateVersions)
{
- Files = new List();
- Extras = new List();
- AlternateVersions = new List();
+ Name = name;
+ Year = year;
+ Files = files;
+ Extras = extras;
+ AlternateVersions = alternateVersions;
}
///
@@ -32,18 +39,18 @@ namespace Emby.Naming.AudioBook
/// Gets or sets the files.
///
/// The files.
- public List Files { get; set; }
+ public IReadOnlyList Files { get; set; }
///
/// Gets or sets the extras.
///
/// The extras.
- public List Extras { get; set; }
+ public IReadOnlyList Extras { get; set; }
///
/// Gets or sets the alternate versions.
///
/// The alternate versions.
- public List AlternateVersions { get; set; }
+ public IReadOnlyList AlternateVersions { get; set; }
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index f4ba11a0d1..1e4a8d2edc 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -1,6 +1,6 @@
-#pragma warning disable CS1591
-
+using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
@@ -8,40 +8,145 @@ using MediaBrowser.Model.IO;
namespace Emby.Naming.AudioBook
{
+ ///
+ /// Class used to resolve Name, Year, alternative files and extras from stack of files.
+ ///
public class AudioBookListResolver
{
private readonly NamingOptions _options;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Naming options passed along to and .
public AudioBookListResolver(NamingOptions options)
{
_options = options;
}
+ ///
+ /// Resolves Name, Year and differentiate alternative files and extras from regular audiobook files.
+ ///
+ /// List of files related to audiobook.
+ /// Returns IEnumerable of .
public IEnumerable Resolve(IEnumerable files)
{
var audioBookResolver = new AudioBookResolver(_options);
+ // File with empty fullname will be sorted out here.
var audiobookFileInfos = files
- .Select(i => audioBookResolver.Resolve(i.FullName, i.IsDirectory))
- .Where(i => i != null)
+ .Select(i => audioBookResolver.Resolve(i.FullName))
+ .OfType()
.ToList();
- // Filter out all extras, otherwise they could cause stacks to not be resolved
- // See the unit test TestStackedWithTrailer
- var metadata = audiobookFileInfos
- .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
-
var stackResult = new StackResolver(_options)
- .ResolveAudioBooks(metadata);
+ .ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
- var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i, stack.IsDirectoryStack)).ToList();
+ var stackFiles = stack.Files
+ .Select(i => audioBookResolver.Resolve(i))
+ .OfType()
+ .ToList();
+
stackFiles.Sort();
- var info = new AudioBookInfo { Files = stackFiles, Name = stack.Name };
+
+ var nameParserResult = new AudioBookNameParser(_options).Parse(stack.Name);
+
+ FindExtraAndAlternativeFiles(ref stackFiles, out var extras, out var alternativeVersions, nameParserResult);
+
+ var info = new AudioBookInfo(
+ nameParserResult.Name,
+ nameParserResult.Year,
+ stackFiles,
+ extras,
+ alternativeVersions);
yield return info;
}
}
+
+ private void FindExtraAndAlternativeFiles(ref List stackFiles, out List extras, out List alternativeVersions, AudioBookNameParserResult nameParserResult)
+ {
+ extras = new List();
+ alternativeVersions = new List();
+
+ var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
+ var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
+ var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.');
+
+ foreach (var group in groupedBy)
+ {
+ if (group.Key.ChapterNumber == null && group.Key.PartNumber == null)
+ {
+ if (group.Count() > 1 || haveChaptersOrPages)
+ {
+ var ex = new List();
+ var alt = new List();
+
+ foreach (var audioFile in group)
+ {
+ var name = Path.GetFileNameWithoutExtension(audioFile.Path);
+ if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
+ name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
+ name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
+ {
+ alt.Add(audioFile);
+ }
+ else
+ {
+ ex.Add(audioFile);
+ }
+ }
+
+ if (ex.Count > 0)
+ {
+ var extra = ex
+ .OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .ToList();
+
+ stackFiles = stackFiles.Except(extra).ToList();
+ extras.AddRange(extra);
+ }
+
+ if (alt.Count > 0)
+ {
+ var alternatives = alt
+ .OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .ToList();
+
+ var main = FindMainAudioBookFile(alternatives, nameParserResult.Name);
+ alternatives.Remove(main);
+ stackFiles = stackFiles.Except(alternatives).ToList();
+ alternativeVersions.AddRange(alternatives);
+ }
+ }
+ }
+ else if (group.Count() > 1)
+ {
+ var alternatives = group
+ .OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .Skip(1)
+ .ToList();
+
+ stackFiles = stackFiles.Except(alternatives).ToList();
+ alternativeVersions.AddRange(alternatives);
+ }
+ }
+ }
+
+ private AudioBookFileInfo FindMainAudioBookFile(List files, string name)
+ {
+ var main = files.Find(x => Path.GetFileNameWithoutExtension(x.Path).Equals(name, StringComparison.OrdinalIgnoreCase));
+ main ??= files.FirstOrDefault(x => Path.GetFileNameWithoutExtension(x.Path).Equals("audiobook", StringComparison.OrdinalIgnoreCase));
+ main ??= files.OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .First();
+
+ return main;
+ }
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs
new file mode 100644
index 0000000000..120482bc2c
--- /dev/null
+++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs
@@ -0,0 +1,67 @@
+using System.Globalization;
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.AudioBook
+{
+ ///
+ /// Helper class to retrieve name and year from audiobook previously retrieved name.
+ ///
+ public class AudioBookNameParser
+ {
+ private readonly NamingOptions _options;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Naming options containing AudioBookNamesExpressions.
+ public AudioBookNameParser(NamingOptions options)
+ {
+ _options = options;
+ }
+
+ ///
+ /// Parse name and year from previously determined name of audiobook.
+ ///
+ /// Name of the audiobook.
+ /// Returns object.
+ public AudioBookNameParserResult Parse(string name)
+ {
+ AudioBookNameParserResult result = default;
+ foreach (var expression in _options.AudioBookNamesExpressions)
+ {
+ var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name);
+ if (match.Success)
+ {
+ if (result.Name == null)
+ {
+ var value = match.Groups["name"];
+ if (value.Success)
+ {
+ result.Name = value.Value;
+ }
+ }
+
+ if (!result.Year.HasValue)
+ {
+ var value = match.Groups["year"];
+ if (value.Success)
+ {
+ if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ {
+ result.Year = intValue;
+ }
+ }
+ }
+ }
+ }
+
+ if (string.IsNullOrEmpty(result.Name))
+ {
+ result.Name = name;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/Emby.Naming/AudioBook/AudioBookNameParserResult.cs b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs
new file mode 100644
index 0000000000..3f2d7b2b0b
--- /dev/null
+++ b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs
@@ -0,0 +1,18 @@
+namespace Emby.Naming.AudioBook
+{
+ ///
+ /// Data object used to pass result of name and year parsing.
+ ///
+ public struct AudioBookNameParserResult
+ {
+ ///
+ /// Gets or sets name of audiobook.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets optional year of release.
+ ///
+ public int? Year { get; set; }
+ }
+}
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index 5807d4688c..f6ad3601d7 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System;
using System.IO;
using System.Linq;
@@ -8,25 +5,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.AudioBook
{
+ ///
+ /// Resolve specifics (path, container, partNumber, chapterNumber) about audiobook file.
+ ///
public class AudioBookResolver
{
private readonly NamingOptions _options;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// containing AudioFileExtensions and also used to pass to AudioBookFilePathParser.
public AudioBookResolver(NamingOptions options)
{
_options = options;
}
- public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
+ ///
+ /// Resolve specifics (path, container, partNumber, chapterNumber) about audiobook file.
+ ///
+ /// Path to audiobook file.
+ /// Returns object.
+ public AudioBookFileInfo? Resolve(string path)
{
- if (path.Length == 0)
- {
- throw new ArgumentException("String can't be empty.", nameof(path));
- }
-
- // TODO
- if (isDirectory)
+ if (path.Length == 0 || Path.GetFileNameWithoutExtension(path).Length == 0)
{
+ // Return null to indicate this path will not be used, instead of stopping whole process with exception
return null;
}
@@ -42,14 +46,11 @@ namespace Emby.Naming.AudioBook
var parsingResult = new AudioBookFilePathParser(_options).Parse(path);
- return new AudioBookFileInfo
- {
- Path = path,
- Container = container,
- ChapterNumber = parsingResult.ChapterNumber,
- PartNumber = parsingResult.PartNumber,
- IsDirectory = isDirectory
- };
+ return new AudioBookFileInfo(
+ path,
+ container,
+ chapterNumber: parsingResult.ChapterNumber,
+ partNumber: parsingResult.PartNumber);
}
}
}
diff --git a/Emby.Naming/Common/EpisodeExpression.cs b/Emby.Naming/Common/EpisodeExpression.cs
index ed6ba8881c..19d3c7aab0 100644
--- a/Emby.Naming/Common/EpisodeExpression.cs
+++ b/Emby.Naming/Common/EpisodeExpression.cs
@@ -1,28 +1,32 @@
-#pragma warning disable CS1591
-
using System;
using System.Text.RegularExpressions;
namespace Emby.Naming.Common
{
+ ///
+ /// Regular expressions for parsing TV Episodes.
+ ///
public class EpisodeExpression
{
private string _expression;
- private Regex _regex;
+ private Regex? _regex;
- public EpisodeExpression(string expression, bool byDate)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Regular expressions.
+ /// True if date is expected.
+ public EpisodeExpression(string expression, bool byDate = false)
{
- Expression = expression;
+ _expression = expression;
IsByDate = byDate;
DateTimeFormats = Array.Empty();
SupportsAbsoluteEpisodeNumbers = true;
}
- public EpisodeExpression(string expression)
- : this(expression, false)
- {
- }
-
+ ///
+ /// Gets or sets raw expressions string.
+ ///
public string Expression
{
get => _expression;
@@ -33,16 +37,34 @@ namespace Emby.Naming.Common
}
}
+ ///
+ /// Gets or sets a value indicating whether gets or sets property indicating if date can be find in expression.
+ ///
public bool IsByDate { get; set; }
+ ///
+ /// Gets or sets a value indicating whether gets or sets property indicating if expression is optimistic.
+ ///
public bool IsOptimistic { get; set; }
+ ///
+ /// Gets or sets a value indicating whether gets or sets property indicating if expression is named.
+ ///
public bool IsNamed { get; set; }
+ ///
+ /// Gets or sets a value indicating whether gets or sets property indicating if expression supports episodes with absolute numbers.
+ ///
public bool SupportsAbsoluteEpisodeNumbers { get; set; }
+ ///
+ /// Gets or sets optional list of date formats used for date parsing.
+ ///
public string[] DateTimeFormats { get; set; }
+ ///
+ /// Gets a expressions objects (creates it if null).
+ ///
public Regex Regex => _regex ??= new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
}
diff --git a/Emby.Naming/Common/MediaType.cs b/Emby.Naming/Common/MediaType.cs
index 148833765f..dc9784c6da 100644
--- a/Emby.Naming/Common/MediaType.cs
+++ b/Emby.Naming/Common/MediaType.cs
@@ -1,7 +1,8 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.Common
{
+ ///
+ /// Type of audiovisual media.
+ ///
public enum MediaType
{
///
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index fd4244f64d..915ce42cc9 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -1,15 +1,21 @@
-#pragma warning disable CS1591
-
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
+// ReSharper disable StringLiteralTypo
+
namespace Emby.Naming.Common
{
+ ///
+ /// Big ugly class containing lot of different naming options that should be split and injected instead of passes everywhere.
+ ///
public class NamingOptions
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public NamingOptions()
{
VideoFileExtensions = new[]
@@ -75,63 +81,52 @@ namespace Emby.Naming.Common
StubTypes = new[]
{
- new StubTypeRule
- {
- StubType = "dvd",
- Token = "dvd"
- },
- new StubTypeRule
- {
- StubType = "hddvd",
- Token = "hddvd"
- },
- new StubTypeRule
- {
- StubType = "bluray",
- Token = "bluray"
- },
- new StubTypeRule
- {
- StubType = "bluray",
- Token = "brrip"
- },
- new StubTypeRule
- {
- StubType = "bluray",
- Token = "bd25"
- },
- new StubTypeRule
- {
- StubType = "bluray",
- Token = "bd50"
- },
- new StubTypeRule
- {
- StubType = "vhs",
- Token = "vhs"
- },
- new StubTypeRule
- {
- StubType = "tv",
- Token = "HDTV"
- },
- new StubTypeRule
- {
- StubType = "tv",
- Token = "PDTV"
- },
- new StubTypeRule
- {
- StubType = "tv",
- Token = "DSR"
- }
+ new StubTypeRule(
+ stubType: "dvd",
+ token: "dvd"),
+
+ new StubTypeRule(
+ stubType: "hddvd",
+ token: "hddvd"),
+
+ new StubTypeRule(
+ stubType: "bluray",
+ token: "bluray"),
+
+ new StubTypeRule(
+ stubType: "bluray",
+ token: "brrip"),
+
+ new StubTypeRule(
+ stubType: "bluray",
+ token: "bd25"),
+
+ new StubTypeRule(
+ stubType: "bluray",
+ token: "bd50"),
+
+ new StubTypeRule(
+ stubType: "vhs",
+ token: "vhs"),
+
+ new StubTypeRule(
+ stubType: "tv",
+ token: "HDTV"),
+
+ new StubTypeRule(
+ stubType: "tv",
+ token: "PDTV"),
+
+ new StubTypeRule(
+ stubType: "tv",
+ token: "DSR")
};
VideoFileStackingExpressions = new[]
{
- "(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(.*?)(\\.[^.]+)$",
- "(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(.*?)(\\.[^.]+)$",
- "(.*?)([ ._-]*[a-d])(.*?)(\\.[^.]+)$"
+ "(?.*?)(?[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?.*?)(?\\.[^.]+)$",
+ "(?.*?)(?[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?.*?)(?\\.[^.]+)$",
+ "(?.*?)(?[ ._-]*[a-d])(?.*?)(?\\.[^.]+)$"
};
CleanDateTimes = new[]
@@ -142,7 +137,7 @@ namespace Emby.Naming.Common
CleanStrings = new[]
{
- @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+ @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"(\[.*\])"
};
@@ -255,7 +250,7 @@ namespace Emby.Naming.Common
},
//
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[]
{
@@ -264,7 +259,7 @@ namespace Emby.Naming.Common
"yyyy_MM_dd"
}
},
- 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[]
{
@@ -282,11 +277,22 @@ namespace Emby.Naming.Common
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
},
- new EpisodeExpression(@"[\\\\/\\._ -](?(?![0-9]+[0-9][0-9])([^\\\/])*)[\\\\/\\._ -](?[0-9]+)(?[0-9][0-9](?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([\\._ -][^\\\\/]*)$")
+
+ // Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
+ // [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
+ new EpisodeExpression(@".*[\\\/]?.*?(\[.*?\])+.*?(?[-\w\s]+?)[\s_]*-[\s_]*(?[0-9]+).*$")
+ {
+ IsNamed = true
+ },
+
+ // /server/anything_102.mp4
+ // /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
+ // /server/anything_1996.11.14.mp4
+ new EpisodeExpression(@"[\\/._ -](?(?![0-9]+[0-9][0-9])([^\\\/_])*)[\\\/._ -](?[0-9]+)(?[0-9][0-9](?:(?:[a-i]|\.[1-9])(?![0-9]))?)([._ -][^\\\/]*)$")
{
IsOptimistic = true,
IsNamed = true,
@@ -299,11 +305,12 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming
- // [bar] Foo - 1 [baz]
- new EpisodeExpression(@".*?(\[.*?\])+.*?(?[\w\s]+?)[-\s_]+(?[0-9]+).*$")
+ // "Episode 16", "Episode 16 - Title"
+ new EpisodeExpression(@"[Ee]pisode (?[0-9]+)(-(?[0-9]+))?[^\\\/]*$")
{
IsNamed = true
},
+
new EpisodeExpression(@".*(\\|\/)[sS]?(?[0-9]+)[xX](?[0-9]+)[^\\\/]*$")
{
IsNamed = true
@@ -361,12 +368,6 @@ namespace Emby.Naming.Common
IsOptimistic = true,
IsNamed = true
},
- // "Episode 16", "Episode 16 - Title"
- new EpisodeExpression(@".*[\\\/][^\\\/]* (?[0-9]{1,3})(-(?[0-9]{2,3}))*[^\\\/]*$")
- {
- IsOptimistic = true,
- IsNamed = true
- }
};
EpisodeWithoutSeasonExpressions = new[]
@@ -381,247 +382,193 @@ namespace Emby.Naming.Common
VideoExtraRules = new[]
{
- new ExtraRule
- {
- ExtraType = ExtraType.Trailer,
- RuleType = ExtraRuleType.Filename,
- Token = "trailer",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Trailer,
- RuleType = ExtraRuleType.Suffix,
- Token = "-trailer",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Trailer,
- RuleType = ExtraRuleType.Suffix,
- Token = ".trailer",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Trailer,
- RuleType = ExtraRuleType.Suffix,
- Token = "_trailer",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Trailer,
- RuleType = ExtraRuleType.Suffix,
- Token = " trailer",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.Filename,
- Token = "sample",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.Suffix,
- Token = "-sample",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.Suffix,
- Token = ".sample",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.Suffix,
- Token = "_sample",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.Suffix,
- Token = " sample",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.ThemeSong,
- RuleType = ExtraRuleType.Filename,
- Token = "theme",
- MediaType = MediaType.Audio
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Scene,
- RuleType = ExtraRuleType.Suffix,
- Token = "-scene",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Clip,
- RuleType = ExtraRuleType.Suffix,
- Token = "-clip",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Interview,
- RuleType = ExtraRuleType.Suffix,
- Token = "-interview",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.BehindTheScenes,
- RuleType = ExtraRuleType.Suffix,
- Token = "-behindthescenes",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.DeletedScene,
- RuleType = ExtraRuleType.Suffix,
- Token = "-deleted",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Clip,
- RuleType = ExtraRuleType.Suffix,
- Token = "-featurette",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Clip,
- RuleType = ExtraRuleType.Suffix,
- Token = "-short",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.BehindTheScenes,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "behind the scenes",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.DeletedScene,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "deleted scenes",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Interview,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "interviews",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Scene,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "scenes",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "samples",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Clip,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "shorts",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Clip,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "featurettes",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Unknown,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "extras",
- MediaType = MediaType.Video,
- },
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.Filename,
+ "trailer",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.Suffix,
+ "-trailer",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.Suffix,
+ ".trailer",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.Suffix,
+ "_trailer",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.Suffix,
+ " trailer",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Filename,
+ "sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Suffix,
+ "-sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Suffix,
+ ".sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Suffix,
+ "_sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Suffix,
+ " sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.ThemeSong,
+ ExtraRuleType.Filename,
+ "theme",
+ MediaType.Audio),
+
+ new ExtraRule(
+ ExtraType.Scene,
+ ExtraRuleType.Suffix,
+ "-scene",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.Suffix,
+ "-clip",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Interview,
+ ExtraRuleType.Suffix,
+ "-interview",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.BehindTheScenes,
+ ExtraRuleType.Suffix,
+ "-behindthescenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.Suffix,
+ "-deleted",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.Suffix,
+ "-featurette",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.Suffix,
+ "-short",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.BehindTheScenes,
+ ExtraRuleType.DirectoryName,
+ "behind the scenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.DirectoryName,
+ "deleted scenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Interview,
+ ExtraRuleType.DirectoryName,
+ "interviews",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Scene,
+ ExtraRuleType.DirectoryName,
+ "scenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.DirectoryName,
+ "samples",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.DirectoryName,
+ "shorts",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.DirectoryName,
+ "featurettes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Unknown,
+ ExtraRuleType.DirectoryName,
+ "extras",
+ MediaType.Video),
};
Format3DRules = new[]
{
// Kodi rules:
- new Format3DRule
- {
- PreceedingToken = "3d",
- Token = "hsbs"
- },
- new Format3DRule
- {
- PreceedingToken = "3d",
- Token = "sbs"
- },
- new Format3DRule
- {
- PreceedingToken = "3d",
- Token = "htab"
- },
- new Format3DRule
- {
- PreceedingToken = "3d",
- Token = "tab"
- },
- // Media Browser rules:
- new Format3DRule
- {
- Token = "fsbs"
- },
- new Format3DRule
- {
- Token = "hsbs"
- },
- new Format3DRule
- {
- Token = "sbs"
- },
- new Format3DRule
- {
- Token = "ftab"
- },
- new Format3DRule
- {
- Token = "htab"
- },
- new Format3DRule
- {
- Token = "tab"
- },
- new Format3DRule
- {
- Token = "sbs3d"
- },
- new Format3DRule
- {
- Token = "mvc"
- }
+ new Format3DRule(
+ precedingToken: "3d",
+ token: "hsbs"),
+
+ new Format3DRule(
+ precedingToken: "3d",
+ token: "sbs"),
+
+ new Format3DRule(
+ precedingToken: "3d",
+ token: "htab"),
+
+ new Format3DRule(
+ precedingToken: "3d",
+ token: "tab"),
+
+ // Media Browser rules:
+ new Format3DRule("fsbs"),
+ new Format3DRule("hsbs"),
+ new Format3DRule("sbs"),
+ new Format3DRule("ftab"),
+ new Format3DRule("htab"),
+ new Format3DRule("tab"),
+ new Format3DRule("sbs3d"),
+ new Format3DRule("mvc")
};
+
AudioBookPartsExpressions = new[]
{
// Detect specified chapters, like CH 01
@@ -631,13 +578,20 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename
"^(?[0-9]+)",
// Part if often ending of filename
- "(?[0-9]+)$",
+ @"(?[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part)
"(?[0-9]+)_(?[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?[0-9]+)"
};
+ AudioBookNamesExpressions = new[]
+ {
+ // Detect year usually in brackets after name Batman (2020)
+ @"^(?.+?)\s*\(\s*(?[0-9]{4})\s*\)\s*$",
+ @"^\s*(?[^ ].*?)\s*$"
+ };
+
var extensions = VideoFileExtensions.ToList();
extensions.AddRange(new[]
@@ -673,7 +627,7 @@ namespace Emby.Naming.Common
".mxf"
});
- MultipleEpisodeExpressions = new string[]
+ MultipleEpisodeExpressions = new[]
{
@".*(\\|\/)[sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?[0-9]{1,3}))+[^\\\/]*$",
@@ -697,56 +651,139 @@ namespace Emby.Naming.Common
Compile();
}
+ ///
+ /// Gets or sets list of audio file extensions.
+ ///
public string[] AudioFileExtensions { get; set; }
+ ///
+ /// Gets or sets list of album stacking prefixes.
+ ///
public string[] AlbumStackingPrefixes { get; set; }
+ ///
+ /// Gets or sets list of subtitle file extensions.
+ ///
public string[] SubtitleFileExtensions { get; set; }
+ ///
+ /// Gets or sets list of subtitles flag delimiters.
+ ///
public char[] SubtitleFlagDelimiters { get; set; }
+ ///
+ /// Gets or sets list of subtitle forced flags.
+ ///
public string[] SubtitleForcedFlags { get; set; }
+ ///
+ /// Gets or sets list of subtitle default flags.
+ ///
public string[] SubtitleDefaultFlags { get; set; }
+ ///
+ /// Gets or sets list of episode regular expressions.
+ ///
public EpisodeExpression[] EpisodeExpressions { get; set; }
+ ///
+ /// Gets or sets list of raw episode without season regular expressions strings.
+ ///
public string[] EpisodeWithoutSeasonExpressions { get; set; }
+ ///
+ /// Gets or sets list of raw multi-part episodes regular expressions strings.
+ ///
public string[] EpisodeMultiPartExpressions { get; set; }
+ ///
+ /// Gets or sets list of video file extensions.
+ ///
public string[] VideoFileExtensions { get; set; }
+ ///
+ /// Gets or sets list of video stub file extensions.
+ ///
public string[] StubFileExtensions { get; set; }
+ ///
+ /// Gets or sets list of raw audiobook parts regular expressions strings.
+ ///
public string[] AudioBookPartsExpressions { get; set; }
+ ///
+ /// Gets or sets list of raw audiobook names regular expressions strings.
+ ///
+ public string[] AudioBookNamesExpressions { get; set; }
+
+ ///
+ /// Gets or sets list of stub type rules.
+ ///
public StubTypeRule[] StubTypes { get; set; }
+ ///
+ /// Gets or sets list of video flag delimiters.
+ ///
public char[] VideoFlagDelimiters { get; set; }
+ ///
+ /// Gets or sets list of 3D Format rules.
+ ///
public Format3DRule[] Format3DRules { get; set; }
+ ///
+ /// Gets or sets list of raw video file-stacking expressions strings.
+ ///
public string[] VideoFileStackingExpressions { get; set; }
+ ///
+ /// Gets or sets list of raw clean DateTimes regular expressions strings.
+ ///
public string[] CleanDateTimes { get; set; }
+ ///
+ /// Gets or sets list of raw clean strings regular expressions strings.
+ ///
public string[] CleanStrings { get; set; }
+ ///
+ /// Gets or sets list of multi-episode regular expressions.
+ ///
public EpisodeExpression[] MultipleEpisodeExpressions { get; set; }
+ ///
+ /// Gets or sets list of extra rules for videos.
+ ///
public ExtraRule[] VideoExtraRules { get; set; }
- public Regex[] VideoFileStackingRegexes { get; private set; }
-
- public Regex[] CleanDateTimeRegexes { get; private set; }
-
- public Regex[] CleanStringRegexes { get; private set; }
-
- public Regex[] EpisodeWithoutSeasonRegexes { get; private set; }
-
- public Regex[] EpisodeMultiPartRegexes { get; private set; }
-
+ ///
+ /// Gets list of video file-stack regular expressions.
+ ///
+ public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty();
+
+ ///
+ /// Gets list of clean datetime regular expressions.
+ ///
+ public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty();
+
+ ///
+ /// Gets list of clean string regular expressions.
+ ///
+ public Regex[] CleanStringRegexes { get; private set; } = Array.Empty();
+
+ ///
+ /// Gets list of episode without season regular expressions.
+ ///
+ public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty();
+
+ ///
+ /// Gets list of multi-part episode regular expressions.
+ ///
+ public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty();
+
+ ///
+ /// Compiles raw regex strings into regexes.
+ ///
public void Compile()
{
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 6857f9952c..07d879e96a 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -1,4 +1,4 @@
-
+
@@ -6,14 +6,14 @@
- netstandard2.1
+ net5.0
false
true
- true
true
true
true
snupkg
+ AllDisabledByDefault
@@ -22,35 +22,31 @@
-
+
-
+
+
Jellyfin Contributors
Jellyfin.Naming
- 10.7.0
+ 10.8.0
https://github.com/jellyfin/jellyfin
GPL-3.0-only
-
+
-
-
- ../jellyfin.ruleset
-
-
diff --git a/Emby.Naming/Subtitles/SubtitleInfo.cs b/Emby.Naming/Subtitles/SubtitleInfo.cs
index f39c496b7a..1fb2e0dc89 100644
--- a/Emby.Naming/Subtitles/SubtitleInfo.cs
+++ b/Emby.Naming/Subtitles/SubtitleInfo.cs
@@ -1,9 +1,23 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.Subtitles
{
+ ///
+ /// Class holding information about subtitle.
+ ///
public class SubtitleInfo
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Path to file.
+ /// Is subtitle default.
+ /// Is subtitle forced.
+ public SubtitleInfo(string path, bool isDefault, bool isForced)
+ {
+ Path = path;
+ IsDefault = isDefault;
+ IsForced = isForced;
+ }
+
///
/// Gets or sets the path.
///
@@ -14,7 +28,7 @@ namespace Emby.Naming.Subtitles
/// Gets or sets the language.
///
/// The language.
- public string Language { get; set; }
+ public string? Language { get; set; }
///
/// Gets or sets a value indicating whether this instance is default.
diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs
index 24e59f90a3..a19340ef69 100644
--- a/Emby.Naming/Subtitles/SubtitleParser.cs
+++ b/Emby.Naming/Subtitles/SubtitleParser.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System;
using System.IO;
using System.Linq;
@@ -8,20 +5,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.Subtitles
{
+ ///
+ /// Subtitle Parser class.
+ ///
public class SubtitleParser
{
private readonly NamingOptions _options;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.
public SubtitleParser(NamingOptions options)
{
_options = options;
}
+ ///
+ /// Parse file to determine if is subtitle and .
+ ///
+ /// Path to file.
+ /// Returns null or object if parsing is successful.
public SubtitleInfo? ParseFile(string path)
{
if (path.Length == 0)
{
- throw new ArgumentException("File path can't be empty.", nameof(path));
+ return null;
}
var extension = Path.GetExtension(path);
@@ -31,12 +40,10 @@ namespace Emby.Naming.Subtitles
}
var flags = GetFlags(path);
- var info = new SubtitleInfo
- {
- Path = path,
- IsDefault = _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
- IsForced = _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase))
- };
+ var info = new SubtitleInfo(
+ path,
+ _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
+ _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
&& !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
@@ -53,7 +60,7 @@ namespace Emby.Naming.Subtitles
private string[] GetFlags(string path)
{
- // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
+ // Note: the tags need be surrounded be either a space ( ), hyphen -, dot . or underscore _.
var file = Path.GetFileName(path);
diff --git a/Emby.Naming/TV/EpisodeInfo.cs b/Emby.Naming/TV/EpisodeInfo.cs
index 250df4e2d3..a8920b36ae 100644
--- a/Emby.Naming/TV/EpisodeInfo.cs
+++ b/Emby.Naming/TV/EpisodeInfo.cs
@@ -1,9 +1,19 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.TV
{
+ ///
+ /// Holder object for Episode information.
+ ///
public class EpisodeInfo
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Path to the file.
+ public EpisodeInfo(string path)
+ {
+ Path = path;
+ }
+
///
/// Gets or sets the path.
///
@@ -14,19 +24,19 @@ namespace Emby.Naming.TV
/// Gets or sets the container.
///
/// The container.
- public string Container { get; set; }
+ public string? Container { get; set; }
///
/// Gets or sets the name of the series.
///
/// The name of the series.
- public string SeriesName { get; set; }
+ public string? SeriesName { get; set; }
///
/// Gets or sets the format3 d.
///
/// The format3 d.
- public string Format3D { get; set; }
+ public string? Format3D { get; set; }
///
/// Gets or sets a value indicating whether [is3 d].
@@ -44,20 +54,41 @@ namespace Emby.Naming.TV
/// Gets or sets the type of the stub.
///
/// The type of the stub.
- public string StubType { get; set; }
+ public string? StubType { get; set; }
+ ///
+ /// Gets or sets optional season number.
+ ///
public int? SeasonNumber { get; set; }
+ ///
+ /// Gets or sets optional episode number.
+ ///
public int? EpisodeNumber { get; set; }
- public int? EndingEpsiodeNumber { get; set; }
+ ///
+ /// Gets or sets optional ending episode number. For multi-episode files 1-13.
+ ///
+ public int? EndingEpisodeNumber { get; set; }
+ ///
+ /// Gets or sets optional year of release.
+ ///
public int? Year { get; set; }
+ ///
+ /// Gets or sets optional year of release.
+ ///
public int? Month { get; set; }
+ ///
+ /// Gets or sets optional day of release.
+ ///
public int? Day { get; set; }
+ ///
+ /// Gets or sets a value indicating whether by date expression was used.
+ ///
public bool IsByDate { get; set; }
}
}
diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs
index a6af689c72..6d0597356b 100644
--- a/Emby.Naming/TV/EpisodePathParser.cs
+++ b/Emby.Naming/TV/EpisodePathParser.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -9,15 +6,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.TV
{
+ ///
+ /// Used to parse information about episode from path.
+ ///
public class EpisodePathParser
{
private readonly NamingOptions _options;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// object containing EpisodeExpressions and MultipleEpisodeExpressions.
public EpisodePathParser(NamingOptions options)
{
_options = options;
}
+ ///
+ /// Parses information about episode from path.
+ ///
+ /// Path.
+ /// Is path for a directory or file.
+ /// Do we want to use IsNamed expressions.
+ /// Do we want to use Optimistic expressions.
+ /// Do we want to use expressions supporting absolute episode numbers.
+ /// Should we attempt to retrieve extended information.
+ /// Returns object.
public EpisodePathParserResult Parse(
string path,
bool isDirectory,
@@ -146,7 +160,7 @@ namespace Emby.Naming.TV
{
if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
- result.EndingEpsiodeNumber = num;
+ result.EndingEpisodeNumber = num;
}
}
}
@@ -186,7 +200,7 @@ namespace Emby.Naming.TV
private void FillAdditional(string path, EpisodePathParserResult info)
{
- var expressions = _options.MultipleEpisodeExpressions.ToList();
+ var expressions = _options.MultipleEpisodeExpressions.Where(i => i.IsNamed).ToList();
if (string.IsNullOrEmpty(info.SeriesName))
{
@@ -200,11 +214,6 @@ namespace Emby.Naming.TV
{
foreach (var i in expressions)
{
- if (!i.IsNamed)
- {
- continue;
- }
-
var result = Parse(path, i);
if (!result.Success)
@@ -217,13 +226,13 @@ namespace Emby.Naming.TV
info.SeriesName = result.SeriesName;
}
- if (!info.EndingEpsiodeNumber.HasValue && info.EpisodeNumber.HasValue)
+ if (!info.EndingEpisodeNumber.HasValue && info.EpisodeNumber.HasValue)
{
- info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
+ info.EndingEpisodeNumber = result.EndingEpisodeNumber;
}
if (!string.IsNullOrEmpty(info.SeriesName)
- && (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue))
+ && (!info.EpisodeNumber.HasValue || info.EndingEpisodeNumber.HasValue))
{
break;
}
diff --git a/Emby.Naming/TV/EpisodePathParserResult.cs b/Emby.Naming/TV/EpisodePathParserResult.cs
index 05f921edc9..233d5a4f6c 100644
--- a/Emby.Naming/TV/EpisodePathParserResult.cs
+++ b/Emby.Naming/TV/EpisodePathParserResult.cs
@@ -1,25 +1,54 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.TV
{
+ ///
+ /// Holder object for result.
+ ///
public class EpisodePathParserResult
{
+ ///
+ /// Gets or sets optional season number.
+ ///
public int? SeasonNumber { get; set; }
+ ///
+ /// Gets or sets optional episode number.
+ ///
public int? EpisodeNumber { get; set; }
- public int? EndingEpsiodeNumber { get; set; }
+ ///
+ /// Gets or sets optional ending episode number. For multi-episode files 1-13.
+ ///
+ public int? EndingEpisodeNumber { get; set; }
- public string SeriesName { get; set; }
+ ///
+ /// Gets or sets the name of the series.
+ ///
+ /// The name of the series.
+ public string? SeriesName { get; set; }
+ ///
+ /// Gets or sets a value indicating whether parsing was successful.
+ ///
public bool Success { get; set; }
+ ///
+ /// Gets or sets a value indicating whether by date expression was used.
+ ///
public bool IsByDate { get; set; }
+ ///
+ /// Gets or sets optional year of release.
+ ///
public int? Year { get; set; }
+ ///
+ /// Gets or sets optional year of release.
+ ///
public int? Month { get; set; }
+ ///
+ /// Gets or sets optional day of release.
+ ///
public int? Day { get; set; }
}
}
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs
index 6994f69fc4..5e952e47b7 100644
--- a/Emby.Naming/TV/EpisodeResolver.cs
+++ b/Emby.Naming/TV/EpisodeResolver.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.IO;
using System.Linq;
@@ -9,15 +6,32 @@ using Emby.Naming.Video;
namespace Emby.Naming.TV
{
+ ///
+ /// Used to resolve information about episode from path.
+ ///
public class EpisodeResolver
{
private readonly NamingOptions _options;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// object containing VideoFileExtensions and passed to , and .
public EpisodeResolver(NamingOptions options)
{
_options = options;
}
+ ///
+ /// Resolve information about episode from path.
+ ///
+ /// Path.
+ /// Is path for a directory or file.
+ /// Do we want to use IsNamed expressions.
+ /// Do we want to use Optimistic expressions.
+ /// Do we want to use expressions supporting absolute episode numbers.
+ /// Should we attempt to retrieve extended information.
+ /// Returns null or object if successful.
public EpisodeInfo? Resolve(
string path,
bool isDirectory,
@@ -48,18 +62,21 @@ namespace Emby.Naming.TV
container = extension.TrimStart('.');
}
- var flags = new FlagParser(_options).GetFlags(path);
- var format3DResult = new Format3DParser(_options).Parse(flags);
+ var format3DResult = Format3DParser.Parse(path, _options);
var parsingResult = new EpisodePathParser(_options)
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
- return new EpisodeInfo
+ if (!parsingResult.Success && !isStub)
+ {
+ return null;
+ }
+
+ return new EpisodeInfo(path)
{
- Path = path,
Container = container,
IsStub = isStub,
- EndingEpsiodeNumber = parsingResult.EndingEpsiodeNumber,
+ EndingEpisodeNumber = parsingResult.EndingEpisodeNumber,
EpisodeNumber = parsingResult.EpisodeNumber,
SeasonNumber = parsingResult.SeasonNumber,
SeriesName = parsingResult.SeriesName,
diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index d2e324dda5..6236f86c43 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -1,11 +1,12 @@
-#pragma warning disable CS1591
-
using System;
using System.Globalization;
using System.IO;
namespace Emby.Naming.TV
{
+ ///
+ /// Class to parse season paths.
+ ///
public static class SeasonPathParser
{
///
@@ -23,6 +24,13 @@ namespace Emby.Naming.TV
"stagione"
};
+ ///
+ /// Attempts to parse season number from path.
+ ///
+ /// Path to season.
+ /// Support special aliases when parsing.
+ /// Support numeric season folders when parsing.
+ /// Returns object.
public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
{
var result = new SeasonPathParserResult();
@@ -52,7 +60,7 @@ namespace Emby.Naming.TV
bool supportSpecialAliases,
bool supportNumericSeasonFolders)
{
- var filename = Path.GetFileName(path) ?? string.Empty;
+ string filename = Path.GetFileName(path);
if (supportSpecialAliases)
{
@@ -101,9 +109,9 @@ namespace Emby.Naming.TV
}
var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
- for (int i = 0; i < parts.Length; i++)
+ foreach (var part in parts)
{
- if (TryGetSeasonNumberFromPart(parts[i], out int seasonNumber))
+ if (TryGetSeasonNumberFromPart(part, out int seasonNumber))
{
return (seasonNumber, true);
}
@@ -139,7 +147,7 @@ namespace Emby.Naming.TV
var numericStart = -1;
var length = 0;
- var hasOpenParenth = false;
+ var hasOpenParenthesis = false;
var isSeasonFolder = true;
// Find out where the numbers start, and then keep going until they end
@@ -147,7 +155,7 @@ namespace Emby.Naming.TV
{
if (char.IsNumber(path[i]))
{
- if (!hasOpenParenth)
+ if (!hasOpenParenthesis)
{
if (numericStart == -1)
{
@@ -167,11 +175,11 @@ namespace Emby.Naming.TV
var currentChar = path[i];
if (currentChar == '(')
{
- hasOpenParenth = true;
+ hasOpenParenthesis = true;
}
else if (currentChar == ')')
{
- hasOpenParenth = false;
+ hasOpenParenthesis = false;
}
}
diff --git a/Emby.Naming/TV/SeasonPathParserResult.cs b/Emby.Naming/TV/SeasonPathParserResult.cs
index a142fafea0..b4b6f236a7 100644
--- a/Emby.Naming/TV/SeasonPathParserResult.cs
+++ b/Emby.Naming/TV/SeasonPathParserResult.cs
@@ -1,7 +1,8 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.TV
{
+ ///
+ /// Data object to pass result of .
+ ///
public class SeasonPathParserResult
{
///
@@ -16,6 +17,10 @@ namespace Emby.Naming.TV
/// true if success; otherwise, false.
public bool Success { get; set; }
+ ///
+ /// Gets or sets a value indicating whether "Is season folder".
+ /// Seems redundant and barely used.
+ ///
public bool IsSeasonFolder { get; set; }
}
}
diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs
index 579c9e91e1..0ee633dcc6 100644
--- a/Emby.Naming/Video/CleanDateTimeParser.cs
+++ b/Emby.Naming/Video/CleanDateTimeParser.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
@@ -12,9 +9,20 @@ namespace Emby.Naming.Video
///
public static class CleanDateTimeParser
{
+ ///
+ /// Attempts to clean the name.
+ ///
+ /// Name of video.
+ /// Optional list of regexes to clean the name.
+ /// Returns object.
public static CleanDateTimeResult Clean(string name, IReadOnlyList cleanDateTimeRegexes)
{
CleanDateTimeResult result = new CleanDateTimeResult(name);
+ if (string.IsNullOrEmpty(name))
+ {
+ return result;
+ }
+
var len = cleanDateTimeRegexes.Count;
for (int i = 0; i < len; i++)
{
diff --git a/Emby.Naming/Video/CleanDateTimeResult.cs b/Emby.Naming/Video/CleanDateTimeResult.cs
index 57eeaa7e32..c675a19d0f 100644
--- a/Emby.Naming/Video/CleanDateTimeResult.cs
+++ b/Emby.Naming/Video/CleanDateTimeResult.cs
@@ -1,22 +1,21 @@
-#pragma warning disable CS1591
-#nullable enable
-
namespace Emby.Naming.Video
{
+ ///
+ /// Holder structure for name and year.
+ ///
public readonly struct CleanDateTimeResult
{
- public CleanDateTimeResult(string name, int? year)
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Name of video.
+ /// Year of release.
+ public CleanDateTimeResult(string name, int? year = null)
{
Name = name;
Year = year;
}
- public CleanDateTimeResult(string name)
- {
- Name = name;
- Year = null;
- }
-
///
/// Gets the name.
///
diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs
index 3f584d5847..4eef3ebc5e 100644
--- a/Emby.Naming/Video/CleanStringParser.cs
+++ b/Emby.Naming/Video/CleanStringParser.cs
@@ -1,8 +1,6 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Emby.Naming.Video
@@ -12,8 +10,21 @@ namespace Emby.Naming.Video
///
public static class CleanStringParser
{
- public static bool TryClean(string name, IReadOnlyList expressions, out ReadOnlySpan newName)
+ ///
+ /// Attempts to extract clean name with regular expressions.
+ ///
+ /// Name of file.
+ /// List of regex to parse name and year from.
+ /// Parsing result string.
+ /// True if parsing was successful.
+ public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList expressions, out ReadOnlySpan newName)
{
+ if (string.IsNullOrEmpty(name))
+ {
+ newName = ReadOnlySpan.Empty;
+ return false;
+ }
+
var len = expressions.Count;
for (int i = 0; i < len; i++)
{
@@ -37,7 +48,7 @@ namespace Emby.Naming.Video
return true;
}
- newName = string.Empty;
+ newName = ReadOnlySpan.Empty;
return false;
}
}
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index fc0424faab..a32af002cc 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -1,92 +1,100 @@
-#pragma warning disable CS1591
-
using System;
using System.IO;
-using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Audio;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
+ ///
+ /// Resolve if file is extra for video.
+ ///
public class ExtraResolver
{
private readonly NamingOptions _options;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// object containing VideoExtraRules and passed to and .
public ExtraResolver(NamingOptions options)
{
_options = options;
}
+ ///
+ /// Attempts to resolve if file is extra.
+ ///
+ /// Path to file.
+ /// Returns object.
public ExtraResult GetExtraInfo(string path)
- {
- return _options.VideoExtraRules
- .Select(i => GetExtraInfo(path, i))
- .FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
- }
-
- private ExtraResult GetExtraInfo(string path, ExtraRule rule)
{
var result = new ExtraResult();
- if (rule.MediaType == MediaType.Audio)
+ for (var i = 0; i < _options.VideoExtraRules.Length; i++)
{
- if (!AudioFileParser.IsAudioFile(path, _options))
+ var rule = _options.VideoExtraRules[i];
+ if (rule.MediaType == MediaType.Audio)
{
- return result;
+ if (!AudioFileParser.IsAudioFile(path, _options))
+ {
+ continue;
+ }
}
- }
- else if (rule.MediaType == MediaType.Video)
- {
- if (!new VideoResolver(_options).IsVideoFile(path))
+ else if (rule.MediaType == MediaType.Video)
{
- return result;
+ if (!VideoResolver.IsVideoFile(path, _options))
+ {
+ continue;
+ }
}
- }
- else
- {
- return result;
- }
-
- if (rule.RuleType == ExtraRuleType.Filename)
- {
- var filename = Path.GetFileNameWithoutExtension(path);
- if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
+ var pathSpan = path.AsSpan();
+ if (rule.RuleType == ExtraRuleType.Filename)
{
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
- else if (rule.RuleType == ExtraRuleType.Suffix)
- {
- var filename = Path.GetFileNameWithoutExtension(path);
+ var filename = Path.GetFileNameWithoutExtension(pathSpan);
- if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
+ if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
+ }
+ else if (rule.RuleType == ExtraRuleType.Suffix)
{
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
+ var filename = Path.GetFileNameWithoutExtension(pathSpan);
+
+ if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
}
- }
- else if (rule.RuleType == ExtraRuleType.Regex)
- {
- var filename = Path.GetFileName(path);
+ else if (rule.RuleType == ExtraRuleType.Regex)
+ {
+ var filename = Path.GetFileName(path);
- var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
+ var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
- if (regex.IsMatch(filename))
+ if (regex.IsMatch(filename))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
+ }
+ else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
+ var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
+ if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
}
- }
- else if (rule.RuleType == ExtraRuleType.DirectoryName)
- {
- var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
- if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
+
+ if (result.ExtraType != null)
{
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
+ return result;
}
}
diff --git a/Emby.Naming/Video/ExtraResult.cs b/Emby.Naming/Video/ExtraResult.cs
index 15db32e876..243fc2b415 100644
--- a/Emby.Naming/Video/ExtraResult.cs
+++ b/Emby.Naming/Video/ExtraResult.cs
@@ -1,9 +1,10 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
+ ///
+ /// Holder object for passing results from ExtraResolver.
+ ///
public class ExtraResult
{
///
@@ -16,6 +17,6 @@ namespace Emby.Naming.Video
/// Gets or sets the rule.
///
/// The rule.
- public ExtraRule Rule { get; set; }
+ public ExtraRule? Rule { get; set; }
}
}
diff --git a/Emby.Naming/Video/ExtraRule.cs b/Emby.Naming/Video/ExtraRule.cs
index 7c9702e244..e267ac55fc 100644
--- a/Emby.Naming/Video/ExtraRule.cs
+++ b/Emby.Naming/Video/ExtraRule.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Model.Entities;
using MediaType = Emby.Naming.Common.MediaType;
@@ -10,6 +8,21 @@ namespace Emby.Naming.Video
///
public class ExtraRule
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Type of extra.
+ /// Type of rule.
+ /// Token.
+ /// Media type.
+ public ExtraRule(ExtraType extraType, ExtraRuleType ruleType, string token, MediaType mediaType)
+ {
+ Token = token;
+ ExtraType = extraType;
+ RuleType = ruleType;
+ MediaType = mediaType;
+ }
+
///
/// Gets or sets the token to use for matching against the file path.
///
diff --git a/Emby.Naming/Video/ExtraRuleType.cs b/Emby.Naming/Video/ExtraRuleType.cs
index e89876f4ae..3243195057 100644
--- a/Emby.Naming/Video/ExtraRuleType.cs
+++ b/Emby.Naming/Video/ExtraRuleType.cs
@@ -1,7 +1,8 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.Video
{
+ ///
+ /// Extra rules type to determine against what should be matched.
+ ///
public enum ExtraRuleType
{
///
@@ -22,6 +23,6 @@ namespace Emby.Naming.Video
///
/// Match against the name of the directory containing the file.
///
- DirectoryName = 3,
+ DirectoryName = 3
}
}
diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs
index 3ef190b865..6519db57c3 100644
--- a/Emby.Naming/Video/FileStack.cs
+++ b/Emby.Naming/Video/FileStack.cs
@@ -1,24 +1,43 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Linq;
namespace Emby.Naming.Video
{
+ ///
+ /// Object holding list of files paths with additional information.
+ ///
public class FileStack
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public FileStack()
{
Files = new List();
}
- public string Name { get; set; }
+ ///
+ /// Gets or sets name of file stack.
+ ///
+ public string Name { get; set; } = string.Empty;
+ ///
+ /// Gets or sets list of paths in stack.
+ ///
public List Files { get; set; }
+ ///
+ /// Gets or sets a value indicating whether stack is directory stack.
+ ///
public bool IsDirectoryStack { get; set; }
+ ///
+ /// Helper function to determine if path is in the stack.
+ ///
+ /// Path of desired file.
+ /// Requested type of stack.
+ /// True if file is in the stack.
public bool ContainsFile(string file, bool isDirectory)
{
if (IsDirectoryStack == isDirectory)
diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs
deleted file mode 100644
index a8bd9d5c5d..0000000000
--- a/Emby.Naming/Video/FlagParser.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.IO;
-using Emby.Naming.Common;
-
-namespace Emby.Naming.Video
-{
- public class FlagParser
- {
- private readonly NamingOptions _options;
-
- public FlagParser(NamingOptions options)
- {
- _options = options;
- }
-
- public string[] GetFlags(string path)
- {
- return GetFlags(path, _options.VideoFlagDelimiters);
- }
-
- public string[] GetFlags(string path, char[] delimeters)
- {
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
-
- var file = Path.GetFileName(path);
-
- return file.Split(delimeters, StringSplitOptions.RemoveEmptyEntries);
- }
- }
-}
diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs
index 51c26af863..0890899894 100644
--- a/Emby.Naming/Video/Format3DParser.cs
+++ b/Emby.Naming/Video/Format3DParser.cs
@@ -1,35 +1,37 @@
-#pragma warning disable CS1591
-
using System;
-using System.Linq;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
- public class Format3DParser
+ ///
+ /// Parse 3D format related flags.
+ ///
+ public static class Format3DParser
{
- private readonly NamingOptions _options;
-
- public Format3DParser(NamingOptions options)
+ // Static default result to save on allocation costs.
+ private static readonly Format3DResult _defaultResult = new (false, null);
+
+ ///
+ /// Parse 3D format related flags.
+ ///
+ /// Path to file.
+ /// The naming options.
+ /// Returns object.
+ public static Format3DResult Parse(ReadOnlySpan path, NamingOptions namingOptions)
{
- _options = options;
- }
+ int oldLen = namingOptions.VideoFlagDelimiters.Length;
+ Span delimiters = stackalloc char[oldLen + 1];
+ namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
+ delimiters[oldLen] = ' ';
- public Format3DResult Parse(string path)
- {
- int oldLen = _options.VideoFlagDelimiters.Length;
- var delimeters = new char[oldLen + 1];
- _options.VideoFlagDelimiters.CopyTo(delimeters, 0);
- delimeters[oldLen] = ' ';
-
- return Parse(new FlagParser(_options).GetFlags(path, delimeters));
+ return Parse(path, delimiters, namingOptions);
}
- internal Format3DResult Parse(string[] videoFlags)
+ private static Format3DResult Parse(ReadOnlySpan path, ReadOnlySpan delimiters, NamingOptions namingOptions)
{
- foreach (var rule in _options.Format3DRules)
+ foreach (var rule in namingOptions.Format3DRules)
{
- var result = Parse(videoFlags, rule);
+ var result = Parse(path, rule, delimiters);
if (result.Is3D)
{
@@ -37,51 +39,43 @@ namespace Emby.Naming.Video
}
}
- return new Format3DResult();
+ return _defaultResult;
}
- private static Format3DResult Parse(string[] videoFlags, Format3DRule rule)
+ private static Format3DResult Parse(ReadOnlySpan path, Format3DRule rule, ReadOnlySpan delimiters)
{
- var result = new Format3DResult();
+ bool is3D = false;
+ string? format3D = null;
- if (string.IsNullOrEmpty(rule.PreceedingToken))
+ // If there's no preceding token we just consider it found
+ var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken);
+ while (path.Length > 0)
{
- result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
- result.Is3D = !string.IsNullOrEmpty(result.Format3D);
-
- if (result.Is3D)
+ var index = path.IndexOfAny(delimiters);
+ if (index == -1)
{
- result.Tokens.Add(rule.Token);
+ index = path.Length - 1;
}
- }
- else
- {
- var foundPrefix = false;
- string format = null;
- foreach (var flag in videoFlags)
- {
- if (foundPrefix)
- {
- result.Tokens.Add(rule.PreceedingToken);
+ var currentSlice = path[..index];
+ path = path[(index + 1)..];
- if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
- {
- format = flag;
- result.Tokens.Add(rule.Token);
- }
+ if (!foundPrefix)
+ {
+ foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
+ continue;
+ }
- break;
- }
+ is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
- foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase);
+ if (is3D)
+ {
+ format3D = rule.Token;
+ break;
}
-
- result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
- result.Format3D = format;
}
- return result;
+ return is3D ? new Format3DResult(true, format3D) : _defaultResult;
}
}
}
diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs
index fa0e9d3b80..aac959c133 100644
--- a/Emby.Naming/Video/Format3DResult.cs
+++ b/Emby.Naming/Video/Format3DResult.cs
@@ -1,32 +1,31 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
namespace Emby.Naming.Video
{
+ ///
+ /// Helper object to return data from .
+ ///
public class Format3DResult
{
- public Format3DResult()
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A value indicating whether the parsed string contains 3D tokens.
+ /// The 3D format. Value might be null if [is3D] is false.
+ public Format3DResult(bool is3D, string? format3D)
{
- Tokens = new List();
+ Is3D = is3D;
+ Format3D = format3D;
}
///
- /// Gets or sets a value indicating whether [is3 d].
+ /// Gets a value indicating whether [is3 d].
///
/// true if [is3 d]; otherwise, false.
- public bool Is3D { get; set; }
+ public bool Is3D { get; }
///
- /// Gets or sets the format3 d.
+ /// Gets the format3 d.
///
/// The format3 d.
- public string Format3D { get; set; }
-
- ///
- /// Gets or sets the tokens.
- ///
- /// The tokens.
- public List Tokens { get; set; }
+ public string? Format3D { get; }
}
}
diff --git a/Emby.Naming/Video/Format3DRule.cs b/Emby.Naming/Video/Format3DRule.cs
index 310ec84e8f..e562691df9 100644
--- a/Emby.Naming/Video/Format3DRule.cs
+++ b/Emby.Naming/Video/Format3DRule.cs
@@ -1,9 +1,21 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.Video
{
+ ///
+ /// Data holder class for 3D format rule.
+ ///
public class Format3DRule
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Token.
+ /// Token present before current token.
+ public Format3DRule(string token, string? precedingToken = null)
+ {
+ Token = token;
+ PrecedingToken = precedingToken;
+ }
+
///
/// Gets or sets the token.
///
@@ -11,9 +23,9 @@ namespace Emby.Naming.Video
public string Token { get; set; }
///
- /// Gets or sets the preceeding token.
+ /// Gets or sets the preceding token.
///
- /// The preceeding token.
- public string PreceedingToken { get; set; }
+ /// The preceding token.
+ public string? PrecedingToken { get; set; }
}
}
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index f733cd2620..36f65a5624 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -1,64 +1,92 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
+using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
{
+ ///
+ /// Resolve from list of paths.
+ ///
public class StackResolver
{
private readonly NamingOptions _options;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// object containing VideoFileStackingRegexes and passes options to .
public StackResolver(NamingOptions options)
{
_options = options;
}
+ ///
+ /// Resolves only directories from paths.
+ ///
+ /// List of paths.
+ /// Enumerable of directories.
public IEnumerable ResolveDirectories(IEnumerable files)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
}
+ ///
+ /// Resolves only files from paths.
+ ///
+ /// List of paths.
+ /// Enumerable of files.
public IEnumerable ResolveFiles(IEnumerable files)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
}
- public IEnumerable ResolveAudioBooks(IEnumerable files)
+ ///
+ /// Resolves audiobooks from paths.
+ ///
+ /// List of paths.
+ /// Enumerable of directories.
+ public IEnumerable ResolveAudioBooks(IEnumerable files)
{
- var groupedDirectoryFiles = files.GroupBy(file =>
- file.IsDirectory
- ? file.FullName
- : Path.GetDirectoryName(file.FullName));
+ var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
foreach (var directory in groupedDirectoryFiles)
{
- var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
- foreach (var file in directory)
+ if (string.IsNullOrEmpty(directory.Key))
{
- if (file.IsDirectory)
+ foreach (var file in directory)
{
- continue;
+ var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
+ stack.Files.Add(file.Path);
+ yield return stack;
}
-
- stack.Files.Add(file.FullName);
}
+ else
+ {
+ var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
+ foreach (var file in directory)
+ {
+ stack.Files.Add(file.Path);
+ }
- yield return stack;
+ yield return stack;
+ }
}
}
+ ///
+ /// Resolves videos from paths.
+ ///
+ /// List of paths.
+ /// Enumerable of videos.
public IEnumerable Resolve(IEnumerable files)
{
- var resolver = new VideoResolver(_options);
-
var list = files
- .Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))
+ .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
.OrderBy(i => i.FullName)
.ToList();
@@ -81,10 +109,10 @@ namespace Emby.Naming.Video
if (match1.Success)
{
- var title1 = match1.Groups[1].Value;
- var volume1 = match1.Groups[2].Value;
- var ignore1 = match1.Groups[3].Value;
- var extension1 = match1.Groups[4].Value;
+ var title1 = match1.Groups["title"].Value;
+ var volume1 = match1.Groups["volume"].Value;
+ var ignore1 = match1.Groups["ignore"].Value;
+ var extension1 = match1.Groups["extension"].Value;
var j = i + 1;
while (j < list.Count)
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index f1b5d7bcca..079987fe8a 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.IO;
using System.Linq;
@@ -8,13 +5,23 @@ using Emby.Naming.Common;
namespace Emby.Naming.Video
{
+ ///
+ /// Resolve if file is stub (.disc).
+ ///
public static class StubResolver
{
+ ///
+ /// Tries to resolve if file is stub (.disc).
+ ///
+ /// Path to file.
+ /// NamingOptions containing StubFileExtensions and StubTypes.
+ /// Stub type.
+ /// True if file is a stub.
public static bool TryResolveFile(string path, NamingOptions options, out string? stubType)
{
stubType = default;
- if (path == null)
+ if (string.IsNullOrEmpty(path))
{
return false;
}
diff --git a/Emby.Naming/Video/StubResult.cs b/Emby.Naming/Video/StubResult.cs
deleted file mode 100644
index 1b8e99b0dc..0000000000
--- a/Emby.Naming/Video/StubResult.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-namespace Emby.Naming.Video
-{
- public struct StubResult
- {
- ///
- /// Gets or sets a value indicating whether this instance is stub.
- ///
- /// true if this instance is stub; otherwise, false.
- public bool IsStub { get; set; }
-
- ///
- /// Gets or sets the type of the stub.
- ///
- /// The type of the stub.
- public string StubType { get; set; }
- }
-}
diff --git a/Emby.Naming/Video/StubTypeRule.cs b/Emby.Naming/Video/StubTypeRule.cs
index 8285cb51a3..dfb3ac013d 100644
--- a/Emby.Naming/Video/StubTypeRule.cs
+++ b/Emby.Naming/Video/StubTypeRule.cs
@@ -1,9 +1,21 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.Video
{
+ ///
+ /// Data class holding information about Stub type rule.
+ ///
public class StubTypeRule
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Token.
+ /// Stub type.
+ public StubTypeRule(string token, string stubType)
+ {
+ Token = token;
+ StubType = stubType;
+ }
+
///
/// Gets or sets the token.
///
diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs
index 11e789b663..481773ff60 100644
--- a/Emby.Naming/Video/VideoFileInfo.cs
+++ b/Emby.Naming/Video/VideoFileInfo.cs
@@ -1,3 +1,4 @@
+using System;
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
@@ -7,6 +8,35 @@ namespace Emby.Naming.Video
///
public class VideoFileInfo
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Name of file.
+ /// Path to the file.
+ /// Container type.
+ /// Year of release.
+ /// Extra type.
+ /// Extra rule.
+ /// Format 3D.
+ /// Is 3D.
+ /// Is Stub.
+ /// Stub type.
+ /// Is directory.
+ public VideoFileInfo(string name, string path, string? container, int? year = default, ExtraType? extraType = default, ExtraRule? extraRule = default, string? format3D = default, bool is3D = default, bool isStub = default, string? stubType = default, bool isDirectory = default)
+ {
+ Path = path;
+ Container = container;
+ Name = name;
+ Year = year;
+ ExtraType = extraType;
+ ExtraRule = extraRule;
+ Format3D = format3D;
+ Is3D = is3D;
+ IsStub = isStub;
+ StubType = stubType;
+ IsDirectory = isDirectory;
+ }
+
///
/// Gets or sets the path.
///
@@ -17,7 +47,7 @@ namespace Emby.Naming.Video
/// Gets or sets the container.
///
/// The container.
- public string Container { get; set; }
+ public string? Container { get; set; }
///
/// Gets or sets the name.
@@ -41,13 +71,13 @@ namespace Emby.Naming.Video
/// Gets or sets the extra rule.
///
/// The extra rule.
- public ExtraRule ExtraRule { get; set; }
+ public ExtraRule? ExtraRule { get; set; }
///
/// Gets or sets the format3 d.
///
/// The format3 d.
- public string Format3D { get; set; }
+ public string? Format3D { get; set; }
///
/// Gets or sets a value indicating whether [is3 d].
@@ -65,7 +95,7 @@ namespace Emby.Naming.Video
/// Gets or sets the type of the stub.
///
/// The type of the stub.
- public string StubType { get; set; }
+ public string? StubType { get; set; }
///
/// Gets or sets a value indicating whether this instance is a directory.
@@ -77,15 +107,14 @@ namespace Emby.Naming.Video
/// Gets the file name without extension.
///
/// The file name without extension.
- public string FileNameWithoutExtension => !IsDirectory
- ? System.IO.Path.GetFileNameWithoutExtension(Path)
- : System.IO.Path.GetFileName(Path);
+ public ReadOnlySpan FileNameWithoutExtension => !IsDirectory
+ ? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan())
+ : System.IO.Path.GetFileName(Path.AsSpan());
///
public override string ToString()
{
- // Makes debugging easier
- return Name ?? base.ToString();
+ return "VideoFileInfo(Name: '" + Name + "')";
}
}
}
diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs
index ea74c40e2a..930fdb33f8 100644
--- a/Emby.Naming/Video/VideoInfo.cs
+++ b/Emby.Naming/Video/VideoInfo.cs
@@ -12,7 +12,7 @@ namespace Emby.Naming.Video
/// Initializes a new instance of the class.
///
/// The name.
- public VideoInfo(string name)
+ public VideoInfo(string? name)
{
Name = name;
@@ -25,7 +25,7 @@ namespace Emby.Naming.Video
/// Gets or sets the name.
///
/// The name.
- public string Name { get; set; }
+ public string? Name { get; set; }
///
/// Gets or sets the year.
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 948fe037b5..ed7d511a39 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -11,22 +9,23 @@ using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
{
- public class VideoListResolver
+ ///
+ /// Resolves alternative versions and extras from list of video files.
+ ///
+ public static class VideoListResolver
{
- private readonly NamingOptions _options;
-
- public VideoListResolver(NamingOptions options)
- {
- _options = options;
- }
-
- public IEnumerable Resolve(List files, bool supportMultiVersion = true)
+ ///
+ /// Resolves alternative versions and extras from list of video files.
+ ///
+ /// List of related video files.
+ /// The naming options.
+ /// Indication we should consider multi-versions of content.
+ /// Returns enumerable of which groups files together when related.
+ public static IEnumerable Resolve(IEnumerable files, NamingOptions namingOptions, bool supportMultiVersion = true)
{
- var videoResolver = new VideoResolver(_options);
-
var videoInfos = files
- .Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
- .Where(i => i != null)
+ .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
+ .OfType()
.ToList();
// Filter out all extras, otherwise they could cause stacks to not be resolved
@@ -35,11 +34,11 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
- var stackResult = new StackResolver(_options)
+ var stackResult = new StackResolver(namingOptions)
.Resolve(nonExtras).ToList();
var remainingFiles = videoInfos
- .Where(i => !stackResult.Any(s => s.ContainsFile(i.Path, i.IsDirectory)))
+ .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
.ToList();
var list = new List();
@@ -48,21 +47,17 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList()
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
+ .OfType()
+ .ToList()
};
info.Year = info.Files[0].Year;
- var extraBaseNames = new List { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
-
- var extras = GetExtras(remainingFiles, extraBaseNames);
+ var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
if (extras.Count > 0)
{
- remainingFiles = remainingFiles
- .Except(extras)
- .ToList();
-
info.Extras = extras;
}
@@ -75,15 +70,12 @@ namespace Emby.Naming.Video
foreach (var media in standaloneMedia)
{
- var info = new VideoInfo(media.Name) { Files = new List { media } };
+ var info = new VideoInfo(media.Name) { Files = new[] { media } };
info.Year = info.Files[0].Year;
- var extras = GetExtras(remainingFiles, new List { media.FileNameWithoutExtension });
-
- remainingFiles = remainingFiles
- .Except(extras.Concat(new[] { media }))
- .ToList();
+ remainingFiles.Remove(media);
+ var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
info.Extras = extras;
@@ -92,8 +84,7 @@ namespace Emby.Naming.Video
if (supportMultiVersion)
{
- list = GetVideosGroupedByVersion(list)
- .ToList();
+ list = GetVideosGroupedByVersion(list, namingOptions);
}
// If there's only one resolved video, use the folder name as well to find extras
@@ -101,19 +92,14 @@ namespace Emby.Naming.Video
{
var info = list[0];
var videoPath = list[0].Files[0].Path;
- var parentPath = Path.GetDirectoryName(videoPath);
+ var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
- if (!string.IsNullOrEmpty(parentPath))
+ if (!parentPath.IsEmpty)
{
var folderName = Path.GetFileName(parentPath);
- if (!string.IsNullOrEmpty(folderName))
+ if (!folderName.IsEmpty)
{
- var extras = GetExtras(remainingFiles, new List { folderName });
-
- remainingFiles = remainingFiles
- .Except(extras)
- .ToList();
-
+ var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
extras.AddRange(info.Extras);
info.Extras = extras;
}
@@ -133,7 +119,7 @@ namespace Emby.Naming.Video
}
// If there's only one video, accept all trailers
- // Be lenient because people use all kinds of mish mash conventions with trailers
+ // Be lenient because people use all kinds of mishmash conventions with trailers.
if (list.Count == 1)
{
var trailers = remainingFiles
@@ -151,86 +137,168 @@ namespace Emby.Naming.Video
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
- Files = new List { i },
+ Files = new[] { i },
Year = i.Year
}));
return list;
}
- private IEnumerable GetVideosGroupedByVersion(List videos)
+ private static List GetVideosGroupedByVersion(List videos, NamingOptions namingOptions)
{
if (videos.Count == 0)
{
return videos;
}
- var list = new List();
+ var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
- var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path));
-
- if (!string.IsNullOrEmpty(folderName)
- && folderName.Length > 1
- && videos.All(i => i.Files.Count == 1
- && IsEligibleForMultiVersion(folderName, i.Files[0].Path))
- && HaveSameYear(videos))
+ if (folderName.Length <= 1 || !HaveSameYear(videos))
{
- var ordered = videos.OrderBy(i => i.Name).ToList();
-
- list.Add(ordered[0]);
+ return videos;
+ }
- var alternateVersionsLen = ordered.Count - 1;
- var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- for (int i = 0; i < alternateVersionsLen; i++)
+ // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
+ for (var i = 0; i < videos.Count; i++)
+ {
+ var video = videos[i];
+ if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
- alternateVersions[i] = ordered[i + 1].Files[0];
+ return videos;
}
+ }
+
+ // The list is created and overwritten in the caller, so we are allowed to do in-place sorting
+ videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
- list[0].AlternateVersions = alternateVersions;
- list[0].Name = folderName;
- var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
- extras.AddRange(list[0].Extras);
- list[0].Extras = extras;
+ var list = new List
+ {
+ videos[0]
+ };
- return list;
+ var alternateVersionsLen = videos.Count - 1;
+ var alternateVersions = new VideoFileInfo[alternateVersionsLen];
+ var extras = new List(list[0].Extras);
+ for (int i = 0; i < alternateVersionsLen; i++)
+ {
+ var video = videos[i + 1];
+ alternateVersions[i] = video.Files[0];
+ extras.AddRange(video.Extras);
}
- return videos;
+ list[0].AlternateVersions = alternateVersions;
+ list[0].Name = folderName.ToString();
+ list[0].Extras = extras;
+
+ return list;
}
- private bool HaveSameYear(List videos)
+ private static bool HaveSameYear(IReadOnlyList videos)
{
- return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
+ if (videos.Count == 1)
+ {
+ return true;
+ }
+
+ var firstYear = videos[0].Year ?? -1;
+ for (var i = 1; i < videos.Count; i++)
+ {
+ if ((videos[i].Year ?? -1) != firstYear)
+ {
+ return false;
+ }
+ }
+
+ return true;
}
- private bool IsEligibleForMultiVersion(string folderName, string testFilename)
+ private static bool IsEligibleForMultiVersion(ReadOnlySpan folderName, string testFilePath, NamingOptions namingOptions)
{
- testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty;
+ var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
+ if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
- if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
+ // Remove the folder name before cleaning as we don't care about cleaning that part
+ if (folderName.Length <= testFilename.Length)
{
- testFilename = testFilename.Substring(folderName.Length).Trim();
- return string.IsNullOrEmpty(testFilename)
+ testFilename = testFilename[folderName.Length..].Trim();
+ }
+
+ // There are no span overloads for regex unfortunately
+ var tmpTestFilename = testFilename.ToString();
+ if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
+ {
+ tmpTestFilename = cleanName.Trim().ToString();
+ }
+
+ // The CleanStringParser should have removed common keywords etc.
+ return string.IsNullOrEmpty(tmpTestFilename)
|| testFilename[0] == '-'
- || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
+ || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+ }
+
+ private static ReadOnlySpan TrimFilenameDelimiters(ReadOnlySpan name, ReadOnlySpan videoFlagDelimiters)
+ {
+ return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
+ }
+
+ private static bool StartsWith(ReadOnlySpan fileName, ReadOnlySpan baseName, ReadOnlySpan trimmedBaseName)
+ {
+ if (baseName.IsEmpty)
+ {
+ return false;
}
- return false;
+ return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
+ || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ ///
+ /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
+ ///
+ /// The list of remaining filenames.
+ /// The base name to use for the comparison.
+ /// The video flag delimiters.
+ /// A list of video extras for [baseName].
+ private static List ExtractExtras(IList remainingFiles, ReadOnlySpan baseName, ReadOnlySpan videoFlagDelimiters)
+ {
+ return ExtractExtras(remainingFiles, baseName, ReadOnlySpan.Empty, videoFlagDelimiters);
}
- private List GetExtras(IEnumerable remainingFiles, List baseNames)
+ ///
+ /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
+ ///
+ /// The list of remaining filenames.
+ /// The first base name to use for the comparison.
+ /// The second base name to use for the comparison.
+ /// The video flag delimiters.
+ /// A list of video extras for [firstBaseName] and [secondBaseName].
+ private static List ExtractExtras(IList remainingFiles, ReadOnlySpan firstBaseName, ReadOnlySpan secondBaseName, ReadOnlySpan videoFlagDelimiters)
{
- foreach (var name in baseNames.ToList())
+ var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
+ var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
+
+ var result = new List();
+ for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
{
- var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
- baseNames.Add(trimmedName);
+ var file = remainingFiles[pos];
+ if (file.ExtraType == null)
+ {
+ continue;
+ }
+
+ var filename = file.FileNameWithoutExtension;
+ if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
+ || StartsWith(filename, secondBaseName, trimmedSecondBaseName))
+ {
+ result.Add(file);
+ remainingFiles.RemoveAt(pos);
+ }
}
- return remainingFiles
- .Where(i => i.ExtraType != null)
- .Where(i => baseNames.Any(b =>
- i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
- .ToList();
+ return result;
}
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index b4aee614b0..3b1d906c64 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -1,40 +1,36 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
- public class VideoResolver
+ ///
+ /// Resolves from file path.
+ ///
+ public static class VideoResolver
{
- private readonly NamingOptions _options;
-
- public VideoResolver(NamingOptions options)
- {
- _options = options;
- }
-
///
/// Resolves the directory.
///
/// The path.
+ /// The naming options.
/// VideoFileInfo.
- public VideoFileInfo? ResolveDirectory(string path)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
{
- return Resolve(path, true);
+ return Resolve(path, true, namingOptions);
}
///
/// Resolves the file.
///
/// The path.
+ /// The naming options.
/// VideoFileInfo.
- public VideoFileInfo? ResolveFile(string path)
+ public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
{
- return Resolve(path, false);
+ return Resolve(path, false, namingOptions);
}
///
@@ -42,29 +38,30 @@ namespace Emby.Naming.Video
///
/// The path.
/// if set to true [is folder].
+ /// The naming options.
/// Whether or not the name should be parsed for info.
/// VideoFileInfo.
/// path is null.
- public VideoFileInfo? Resolve(string path, bool isDirectory, bool parseName = true)
+ public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
{
if (string.IsNullOrEmpty(path))
{
- throw new ArgumentNullException(nameof(path));
+ return null;
}
bool isStub = false;
- string? container = null;
+ ReadOnlySpan container = ReadOnlySpan.Empty;
string? stubType = null;
if (!isDirectory)
{
- var extension = Path.GetExtension(path);
+ var extension = Path.GetExtension(path.AsSpan());
// Check supported extensions
- if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's not supported. Check stub extensions
- if (!StubResolver.TryResolveFile(path, _options, out stubType))
+ if (!StubResolver.TryResolveFile(path, namingOptions, out stubType))
{
return null;
}
@@ -75,66 +72,86 @@ namespace Emby.Naming.Video
container = extension.TrimStart('.');
}
- var flags = new FlagParser(_options).GetFlags(path);
- var format3DResult = new Format3DParser(_options).Parse(flags);
+ var format3DResult = Format3DParser.Parse(path, namingOptions);
- var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
+ var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
- var name = isDirectory
- ? Path.GetFileName(path)
- : Path.GetFileNameWithoutExtension(path);
+ var name = Path.GetFileNameWithoutExtension(path);
int? year = null;
if (parseName)
{
- var cleanDateTimeResult = CleanDateTime(name);
+ var cleanDateTimeResult = CleanDateTime(name, namingOptions);
name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
- && TryCleanString(name, out ReadOnlySpan newName))
+ && TryCleanString(name, namingOptions, out ReadOnlySpan newName))
{
name = newName.ToString();
}
}
- return new VideoFileInfo
- {
- Path = path,
- Container = container,
- IsStub = isStub,
- Name = name,
- Year = year,
- StubType = stubType,
- Is3D = format3DResult.Is3D,
- Format3D = format3DResult.Format3D,
- ExtraType = extraResult.ExtraType,
- IsDirectory = isDirectory,
- ExtraRule = extraResult.Rule
- };
+ return new VideoFileInfo(
+ path: path,
+ container: container.IsEmpty ? null : container.ToString(),
+ isStub: isStub,
+ name: name,
+ year: year,
+ stubType: stubType,
+ is3D: format3DResult.Is3D,
+ format3D: format3DResult.Format3D,
+ extraType: extraResult.ExtraType,
+ isDirectory: isDirectory,
+ extraRule: extraResult.Rule);
}
- public bool IsVideoFile(string path)
+ ///
+ /// Determines if path is video file based on extension.
+ ///
+ /// Path to file.
+ /// The naming options.
+ /// True if is video file.
+ public static bool IsVideoFile(string path, NamingOptions namingOptions)
{
- var extension = Path.GetExtension(path) ?? string.Empty;
- return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ var extension = Path.GetExtension(path.AsSpan());
+ return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
- public bool IsStubFile(string path)
+ ///
+ /// Determines if path is video file stub based on extension.
+ ///
+ /// Path to file.
+ /// The naming options.
+ /// True if is video file stub.
+ public static bool IsStubFile(string path, NamingOptions namingOptions)
{
- var extension = Path.GetExtension(path) ?? string.Empty;
- return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ var extension = Path.GetExtension(path.AsSpan());
+ return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
- public bool TryCleanString(string name, out ReadOnlySpan newName)
+ ///
+ /// Tries to clean name of clutter.
+ ///
+ /// Raw name.
+ /// The naming options.
+ /// Clean name.
+ /// True if cleaning of name was successful.
+ public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan newName)
{
- return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
+ return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}
- public CleanDateTimeResult CleanDateTime(string name)
+ ///
+ /// Tries to get name and year from raw name.
+ ///
+ /// Raw name.
+ /// The naming options.
+ /// Returns with name and optional year.
+ public static CleanDateTimeResult CleanDateTime(string name, NamingOptions namingOptions)
{
- return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
+ return CleanDateTimeParser.Clean(name, namingOptions.CleanDateTimeRegexes);
}
}
}
diff --git a/Emby.Notifications/CoreNotificationTypes.cs b/Emby.Notifications/CoreNotificationTypes.cs
index a602b72213..ec3490e23b 100644
--- a/Emby.Notifications/CoreNotificationTypes.cs
+++ b/Emby.Notifications/CoreNotificationTypes.cs
@@ -75,10 +75,6 @@ namespace Emby.Notifications
Type = NotificationType.VideoPlaybackStopped.ToString()
},
new NotificationTypeInfo
- {
- Type = NotificationType.CameraImageUploaded.ToString()
- },
- new NotificationTypeInfo
{
Type = NotificationType.UserLockedOut.ToString()
},
@@ -114,10 +110,6 @@ namespace Emby.Notifications
{
note.Category = _localization.GetLocalizedString("Plugin");
}
- else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
- {
- note.Category = _localization.GetLocalizedString("Sync");
- }
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("User");
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 1d430a5e58..5edcf2f295 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -6,11 +6,9 @@
- netstandard2.1
+ net5.0
false
true
- true
- enable
@@ -25,14 +23,9 @@
-
-
- ../jellyfin.ruleset
-
-
diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs
index ded22d26cc..e8ae14ff22 100644
--- a/Emby.Notifications/NotificationEntryPoint.cs
+++ b/Emby.Notifications/NotificationEntryPoint.cs
@@ -77,13 +77,12 @@ namespace Emby.Notifications
{
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
- _appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
return Task.CompletedTask;
}
- private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e)
+ private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e)
{
var type = NotificationType.ServerRestartRequired.ToString();
@@ -99,7 +98,7 @@ namespace Emby.Notifications
await SendNotification(notification, null).ConfigureAwait(false);
}
- private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs e)
+ private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs e)
{
var entry = e.Argument;
@@ -132,26 +131,7 @@ namespace Emby.Notifications
return _config.GetConfiguration("notifications");
}
- private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e)
- {
- if (!_appHost.HasUpdateAvailable)
- {
- return;
- }
-
- var type = NotificationType.ApplicationUpdateAvailable.ToString();
-
- var notification = new NotificationRequest
- {
- Description = "Please see jellyfin.org for details.",
- NotificationType = type,
- Name = _localization.GetLocalizedString("NewVersionIsAvailable")
- };
-
- await SendNotification(notification, null).ConfigureAwait(false);
- }
-
- private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
+ private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@@ -197,7 +177,7 @@ namespace Emby.Notifications
return item.SourceType == SourceType.Library;
}
- private async void LibraryUpdateTimerCallback(object state)
+ private async void LibraryUpdateTimerCallback(object? state)
{
List items;
@@ -209,7 +189,10 @@ namespace Emby.Notifications
_libraryUpdateTimer = null;
}
- items = items.Take(10).ToList();
+ if (items.Count > 10)
+ {
+ items = items.GetRange(0, 10);
+ }
foreach (var item in items)
{
@@ -322,7 +305,6 @@ namespace Emby.Notifications
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
- _appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
_disposed = true;
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index dbe01257f4..00b2f0f94c 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -19,23 +19,16 @@
- netstandard2.1
+ net5.0
false
true
- true
- enable
-
-
- ../jellyfin.ruleset
-
-
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 2adc1d6c34..6edfad575a 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
- DataPath = Path.Combine(ProgramDataPath, "data");
+ _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
///
@@ -55,14 +55,10 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
///
/// The data directory.
- public string DataPath
- {
- get => _dataPath;
- private set => _dataPath = Directory.CreateDirectory(value).FullName;
- }
+ public string DataPath => _dataPath;
///
- public string VirtualDataPath { get; } = "%AppDataPath%";
+ public string VirtualDataPath => "%AppDataPath%";
///
/// Gets the image cache path.
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 4ab0a2a3f2..d385356345 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
private readonly ConcurrentDictionary _configurations = new ConcurrentDictionary();
+ ///
+ /// The _configuration sync lock.
+ ///
+ private readonly object _configurationSyncLock = new object();
+
private ConfigurationStore[] _configurationStores = Array.Empty();
private IConfigurationFactory[] _configurationFactories = Array.Empty();
@@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase
///
private bool _configurationLoaded;
- ///
- /// The _configuration sync lock.
- ///
- private readonly object _configurationSyncLock = new object();
-
///
/// The _configuration.
///
@@ -133,6 +135,33 @@ namespace Emby.Server.Implementations.AppBase
}
}
+ ///
+ /// Manually pre-loads a factory so that it is available pre system initialisation.
+ ///
+ /// Class to register.
+ public virtual void RegisterConfiguration()
+ where T : IConfigurationFactory
+ {
+ IConfigurationFactory factory = Activator.CreateInstance();
+
+ if (_configurationFactories == null)
+ {
+ _configurationFactories = new[] { factory };
+ }
+ else
+ {
+ var oldLen = _configurationFactories.Length;
+ var arr = new IConfigurationFactory[oldLen + 1];
+ _configurationFactories.CopyTo(arr, 0);
+ arr[oldLen] = factory;
+ _configurationFactories = arr;
+ }
+
+ _configurationStores = _configurationFactories
+ .SelectMany(i => i.GetConfigurations())
+ .ToArray();
+ }
+
///
/// Adds parts.
///
@@ -270,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
///
public object GetConfiguration(string key)
{
- return _configurations.GetOrAdd(key, k =>
- {
- var file = GetConfigurationFile(key);
+ return _configurations.GetOrAdd(
+ key,
+ (k, configurationManager) =>
+ {
+ var file = configurationManager.GetConfigurationFile(k);
- var configurationInfo = _configurationStores
- .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
+ var configurationInfo = Array.Find(
+ configurationManager._configurationStores,
+ i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
- if (configurationInfo == null)
- {
- throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
- }
+ if (configurationInfo == null)
+ {
+ throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
+ }
- var configurationType = configurationInfo.ConfigurationType;
+ var configurationType = configurationInfo.ConfigurationType;
- lock (_configurationSyncLock)
- {
- return LoadConfiguration(file, configurationType);
- }
- });
+ lock (configurationManager._configurationSyncLock)
+ {
+ return configurationManager.LoadConfiguration(file, configurationType);
+ }
+ },
+ this);
}
private object LoadConfiguration(string path, Type configurationType)
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 4c9ab33a7c..0308a68e42 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.IO;
using System.Linq;
@@ -35,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
}
catch (Exception)
{
- configuration = Activator.CreateInstance(type);
+ // Note: CreateInstance returns null for Nullable, e.g. CreateInstance(typeof(int?)) returns null.
+ configuration = Activator.CreateInstance(type)!;
}
using var stream = new MemoryStream(buffer?.Length ?? 0);
@@ -48,10 +47,12 @@ namespace Emby.Server.Implementations.AppBase
// If the file didn't exist before, or if something has changed, re-save
if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
{
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
+ Directory.CreateDirectory(directory);
// Save it after load in case we got new items
- using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
fs.Write(newBytes, 0, newBytesLen);
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 31ca738296..bf7ddace2d 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -1,19 +1,17 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
-using System.Net.Http;
-using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
@@ -30,7 +28,6 @@ using Emby.Server.Implementations.Cryptography;
using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Dto;
-using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
@@ -46,12 +43,14 @@ using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
+using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
+using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Manager;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
@@ -84,7 +83,6 @@ using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
@@ -96,15 +94,15 @@ using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
-using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
@@ -120,15 +118,14 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager;
- private readonly INetworkManager _networkManager;
+ private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
+ private readonly IPluginManager _pluginManager;
+ private List _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
- private IHttpClientFactory _httpClientFactory;
-
private string[] _urlPrefixes;
///
@@ -152,16 +149,15 @@ namespace Emby.Server.Implementations
return false;
}
- if (OperatingSystem.Id == OperatingSystemId.Windows
- || OperatingSystem.Id == OperatingSystemId.Darwin)
- {
- return true;
- }
-
- return false;
+ return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
}
}
+ ///
+ /// Gets the singleton instance.
+ ///
+ public INetworkManager NetManager { get; internal set; }
+
///
/// Occurs when [has pending restart changed].
///
@@ -183,14 +179,6 @@ namespace Emby.Server.Implementations
protected IServiceCollection ServiceCollection { get; }
- private IPlugin[] _plugins;
-
- ///
- /// Gets the plugins.
- ///
- /// The plugins.
- public IReadOnlyList Plugins => _plugins;
-
///
/// Gets the logger factory.
///
@@ -214,10 +202,10 @@ namespace Emby.Server.Implementations
private readonly List _disposableParts = new List();
///
- /// Gets the configuration manager.
+ /// Gets or sets the configuration manager.
///
/// The configuration manager.
- protected IConfigurationManager ConfigurationManager { get; set; }
+ public ServerConfigurationManager ConfigurationManager { get; set; }
///
/// Gets or sets the service provider.
@@ -235,10 +223,9 @@ namespace Emby.Server.Implementations
public int HttpsPort { get; private set; }
///
- /// Gets the server configuration manager.
+ /// Gets the value of the PublishedServerUrl setting.
///
- /// The server configuration manager.
- public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
+ public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
///
/// Initializes a new instance of the class.
@@ -246,55 +233,55 @@ namespace Emby.Server.Implementations
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
+ /// The interface.
/// Instance of the interface.
- /// Instance of the interface.
/// Instance of the interface.
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
+ IConfiguration startupConfig,
IFileSystem fileSystem,
- INetworkManager networkManager,
IServiceCollection serviceCollection)
{
- _xmlSerializer = new MyXmlSerializer();
- _jsonSerializer = new JsonSerializer();
-
- ServiceCollection = serviceCollection;
-
- _networkManager = networkManager;
- networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
-
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
+ _startupOptions = options;
+ _startupConfig = startupConfig;
_fileSystemManager = fileSystem;
-
- ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
+ ServiceCollection = serviceCollection;
Logger = LoggerFactory.CreateLogger();
-
- _startupOptions = options;
-
- // Initialize runtime stat collection
- if (ServerConfigurationManager.Configuration.EnableMetrics)
- {
- DotNetRuntimeStatsBuilder.Default().StartCollecting();
- }
-
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
- _networkManager.NetworkChanged += OnNetworkChanged;
-
- CertificateInfo = new CertificateInfo
- {
- Path = ServerConfigurationManager.Configuration.CertificatePath,
- Password = ServerConfigurationManager.Configuration.CertificatePassword
- };
- Certificate = GetCertificate(CertificateInfo);
-
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
+
+ _xmlSerializer = new MyXmlSerializer();
+ ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
+ _pluginManager = new PluginManager(
+ LoggerFactory.CreateLogger(),
+ this,
+ ConfigurationManager.Configuration,
+ ApplicationPaths.PluginsPath,
+ ApplicationVersion);
+ }
+
+ ///
+ /// Temporary function to migration network settings out of system.xml and into network.xml.
+ /// TODO: remove at the point when a fixed migration path has been decided upon.
+ ///
+ private void MigrateNetworkConfiguration()
+ {
+ string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
+ if (!File.Exists(path))
+ {
+ var networkSettings = new NetworkConfiguration();
+ ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
+ _xmlSerializer.SerializeToFile(networkSettings, path);
+ Logger.LogDebug("Successfully migrated network settings.");
+ }
}
public string ExpandVirtualPath(string path)
@@ -313,16 +300,6 @@ namespace Emby.Server.Implementations
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
- private string[] GetConfiguredLocalSubnets()
- {
- return ServerConfigurationManager.Configuration.LocalNetworkSubnets;
- }
-
- private void OnNetworkChanged(object sender, EventArgs e)
- {
- _validAddressResults.Clear();
- }
-
///
public Version ApplicationVersion { get; }
@@ -339,7 +316,7 @@ namespace Emby.Server.Implementations
/// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service.
///
- public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org";
+ public string ApplicationUserAgentAddress => "team@jellyfin.org";
///
/// Gets the current application name.
@@ -353,10 +330,7 @@ namespace Emby.Server.Implementations
{
get
{
- if (_deviceId == null)
- {
- _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
- }
+ _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
@@ -376,7 +350,7 @@ namespace Emby.Server.Implementations
///
/// Creates an instance of type and resolves all constructor dependencies.
///
- /// /// The type.
+ /// The type.
/// T.
public T CreateInstance()
=> ActivatorUtilities.CreateInstance(ServiceProvider);
@@ -388,30 +362,48 @@ namespace Emby.Server.Implementations
/// System.Object.
protected object CreateInstanceSafe(Type type)
{
+ _creatingInstances ??= new List();
+
+ if (_creatingInstances.IndexOf(type) != -1)
+ {
+ Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
+ foreach (var entry in _creatingInstances)
+ {
+ Logger.LogError("Called from: {TypeName}", entry.FullName);
+ }
+
+ _pluginManager.FailPlugin(type.Assembly);
+
+ throw new ExternalException("DI Loop detected.");
+ }
+
try
{
+ _creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type);
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error creating {Type}", type);
+ // If this is a plugin fail it.
+ _pluginManager.FailPlugin(type.Assembly);
return null;
}
+ finally
+ {
+ _creatingInstances.Remove(type);
+ }
}
///
/// Resolves this instance.
///
- /// The type
+ /// The type.
/// ``0.
public T Resolve() => ServiceProvider.GetService();
- ///
- /// Gets the export types.
- ///
- /// The type.
- /// IEnumerable{Type}.
+ ///
public IEnumerable GetExportTypes()
{
var currentType = typeof(T);
@@ -440,17 +432,40 @@ namespace Emby.Server.Implementations
return parts;
}
+ ///
+ public IReadOnlyCollection GetExports(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
+ {
+ // Convert to list so this isn't executed for each iteration
+ var parts = GetExportTypes()
+ .Select(i => defaultFunc(i))
+ .Where(i => i != null)
+ .Cast()
+ .ToList();
+
+ if (manageLifetime)
+ {
+ lock (_disposableParts)
+ {
+ _disposableParts.AddRange(parts.OfType());
+ }
+ }
+
+ return parts;
+ }
+
///
/// Runs the startup tasks.
///
/// .
- public async Task RunStartupTasksAsync()
+ public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
{
+ cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks");
Resolve().AddTasks(GetExports(false));
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
+ ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
_mediaEncoder.SetFFmpegPath();
@@ -458,14 +473,21 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports();
+ cancellationToken.ThrowIfCancellationRequested();
+
var stopWatch = new Stopwatch();
stopWatch.Start();
+
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
stopWatch.Restart();
+
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
stopWatch.Stop();
@@ -489,34 +511,41 @@ namespace Emby.Server.Implementations
///
public void Init()
{
- HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
- HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
+ DiscoverTypes();
- // Safeguard against invalid configuration
- if (HttpPort == HttpsPort)
+ ConfigurationManager.AddParts(GetExports());
+
+ // Have to migrate settings here as migration subsystem not yet initialised.
+ MigrateNetworkConfiguration();
+ NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger());
+
+ // Initialize runtime stat collection
+ if (ConfigurationManager.Configuration.EnableMetrics)
{
- HttpPort = ServerConfiguration.DefaultHttpPort;
- HttpsPort = ServerConfiguration.DefaultHttpsPort;
+ DotNetRuntimeStatsBuilder.Default().StartCollecting();
}
- if (Plugins != null)
- {
- var pluginBuilder = new StringBuilder();
+ var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
+ HttpPort = networkConfiguration.HttpServerPortNumber;
+ HttpsPort = networkConfiguration.HttpsPortNumber;
- foreach (var plugin in Plugins)
- {
- pluginBuilder.Append(plugin.Name)
- .Append(' ')
- .Append(plugin.Version)
- .AppendLine();
- }
-
- Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
+ // Safeguard against invalid configuration
+ if (HttpPort == HttpsPort)
+ {
+ HttpPort = NetworkConfiguration.DefaultHttpPort;
+ HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
- DiscoverTypes();
+ CertificateInfo = new CertificateInfo
+ {
+ Path = networkConfiguration.CertificatePath,
+ Password = networkConfiguration.CertificatePassword
+ };
+ Certificate = GetCertificate(CertificateInfo);
RegisterServices();
+
+ _pluginManager.RegisterServices(ServiceCollection);
}
///
@@ -528,20 +557,16 @@ namespace Emby.Server.Implementations
ServiceCollection.AddMemoryCache();
- ServiceCollection.AddSingleton(ConfigurationManager);
+ ServiceCollection.AddSingleton(ConfigurationManager);
+ ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton(this);
-
+ ServiceCollection.AddSingleton(_pluginManager);
ServiceCollection.AddSingleton(ApplicationPaths);
- ServiceCollection.AddSingleton();
-
ServiceCollection.AddSingleton(_fileSystemManager);
- ServiceCollection.AddSingleton();
ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton(_networkManager);
-
- ServiceCollection.AddSingleton();
+ ServiceCollection.AddSingleton(NetManager);
ServiceCollection.AddSingleton();
@@ -560,8 +585,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(this);
ServiceCollection.AddSingleton(ApplicationPaths);
- ServiceCollection.AddSingleton(ServerConfigurationManager);
-
ServiceCollection.AddSingleton();
ServiceCollection.AddSingleton();
@@ -573,12 +596,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton();
- // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required
- ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
-
- // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required
- ServiceCollection.AddTransient(provider => new Lazy