diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index 14df7e7c8d..cf74a4201b 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: 5.0.100
+ default: 6.0.x
jobs:
- job: CompatibilityCheck
diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml
deleted file mode 100644
index 177f78889c..0000000000
--- a/.ci/azure-pipelines-api-client.yml
+++ /dev/null
@@ -1,59 +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'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- 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
- - task: CmdLine@2
- displayName: 'Build stable typescript axios client'
- 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
- - task: Npm@1
- displayName: 'Publish stable typescript axios client'
- inputs:
- command: custom
- customCommand: publish --access public
- 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 95dd3ccac2..b7112ba245 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: 5.0.100
+ DotNetSdkVersion: 6.0.x
jobs:
- job: Build
@@ -91,3 +91,10 @@ jobs:
inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
+
+ - task: PublishPipelineArtifact@1
+ displayName: 'Publish Artifact Extensions'
+ condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+ inputs:
+ targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
+ artifactName: 'Jellyfin.Extensions'
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 0a63b329ba..89f7137fd0 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -39,6 +39,14 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
+ - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+ displayName: Set release version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
+ - script: './bump-version $(JellyfinVersion)'
+ displayName: Bump internal version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
displayName: 'Build Dockerfile'
@@ -80,6 +88,14 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
+ - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+ displayName: Set release version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
+ - script: './bump-version $(JellyfinVersion)'
+ displayName: Bump internal version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec'
inputs:
@@ -127,6 +143,10 @@ jobs:
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+ - script: './bump-version $(JellyfinVersion)'
+ displayName: Bump internal version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
- task: Docker@2
displayName: 'Push Unstable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
@@ -160,7 +180,6 @@ jobs:
dependsOn:
- BuildPackage
- BuildDocker
- condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
@@ -182,31 +201,39 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
- commands: nohup sudo /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) $(Build.SourceBranch) &
- 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'
+ displayName: 'Use .NET 6.0 sdk'
inputs:
packageType: 'sdk'
- version: '5.0.x'
+ version: '6.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
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+ custom: 'pack'
+ arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
- task: DotNetCoreCLI@2
displayName: 'Build Unstable Nuget packages'
@@ -219,6 +246,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
@@ -233,7 +261,7 @@ jobs:
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 36152c82a4..cc94dc2c5a 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: 5.0.100
+ default: 6.0.x
jobs:
- job: Test
@@ -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/net5.0/openapi.json"
+ targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json"
artifactName: 'OpenAPI Spec'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index ec4c254358..19c9caacbb 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -5,8 +5,6 @@ variables:
value: 'tests/**/*Tests.csproj'
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
-- name: DotNetSdkVersion
- value: 5.0.100
pr:
autoCancel: true
@@ -57,10 +55,10 @@ jobs:
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
+ Extensions:
+ NugetPackageName: Jellyfin.Extensions
+ AssemblyFileName: Jellyfin.Extensions.dll
LinuxImage: 'ubuntu-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
-
-- ${{ 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/.copr b/.copr
new file mode 120000
index 0000000000..100fe0cd7b
--- /dev/null
+++ b/.copr
@@ -0,0 +1 @@
+fedora
\ No newline at end of file
diff --git a/.copr/Makefile b/.copr/Makefile
deleted file mode 120000
index ec3c90dfd9..0000000000
--- a/.copr/Makefile
+++ /dev/null
@@ -1 +0,0 @@
-../fedora/Makefile
\ No newline at end of file
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/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index d67e1c98bf..0000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-name: Bug report
-about: Create a bug report
-title: ''
-labels: bug
-assignees: ''
-
----
-
-**Describe the bug**
-
-
-**System (please complete the following information):**
- - OS: [e.g. Debian, Windows]
- - Virtualization: [e.g. Docker, KVM, LXC]
- - Clients: [Browser, Android, Fire Stick, etc.]
- - Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
- - Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
- - Playback: [Direct Play, Remux, Direct Stream, Transcode]
- - Installed Plugins: [e.g. none, Fanart, Anime, etc.]
- - Reverse Proxy: [e.g. none, nginx, apache, etc.]
- - Base URL: [e.g. none, yes: /example]
- - Networking: [e.g. Host, Bridge/NAT]
- - Storage: [e.g. local, NFS, cloud]
-
-**To Reproduce**
-
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-
-
-**Logs**
-
-
-**Screenshots**
-
-
-**Additional context**
-
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
new file mode 100644
index 0000000000..63e0f0e22d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -0,0 +1,106 @@
+name: Issue Report
+description: File an issue report
+title: "[Issue]: "
+labels: [bug, triage]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: Please describe your bug
+ description: Also tell us, what did you expect to happen?
+ placeholder: |
+ The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
+
+ This is my issue.
+
+ Steps to Reproduce
+ 1. In this environment...
+ 2. With this config...
+ 3. Run '...'
+ 4. See error...
+ validations:
+ required: true
+ - type: dropdown
+ id: version
+ attributes:
+ label: Jellyfin Version
+ description: What version of Jellyfin are you running?
+ options:
+ - 10.7.7
+ - 10.7.z
+ - 10.6.4
+ - Other
+ validations:
+ required: true
+ - type: input
+ id: version-other
+ attributes:
+ label: "if other:"
+ placeholder: Other
+ - type: textarea
+ attributes:
+ label: Environment
+ description: |
+ Examples:
+ - **OS**: [e.g. Debian, Windows]
+ - **Virtualization**: [e.g. Docker, KVM, LXC]
+ - **Clients**: [Browser, Android, Fire Stick, etc.]
+ - **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
+ - **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
+ - **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
+ - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
+ - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
+ - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
+ - **Base URL**: [e.g. none, yes: /example]
+ - **Networking**: [e.g. Host, Bridge/NAT]
+ - **Storage**: [e.g. local, NFS, cloud]
+ value: |
+ - OS:
+ - Virtualization:
+ - Clients:
+ - Browser:
+ - FFmpeg Version:
+ - Playback Method:
+ - Hardware Acceleration:
+ - Plugins:
+ - Reverse Proxy:
+ - Base URL:
+ - Networking:
+ - Storage:
+ render: markdown
+ - type: textarea
+ id: logs
+ attributes:
+ label: Jellyfin logs
+ description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
+ placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
+ render: shell
+ - type: textarea
+ id: ffmpeg-logs
+ attributes:
+ label: FFmpeg logs
+ description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
+ placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
+ render: shell
+ - type: textarea
+ id: browserlogs
+ attributes:
+ label: Please attach any browser or client logs here
+ placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Please attach any screenshots here
+ placeholder: Images can be pasted directly into the textbox and will be hosted by github.
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Code of Conduct
+ description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
+ options:
+ - label: I agree to follow this project's Code of Conduct
+ required: true
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
index 5388948189..ea1d30cdfa 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -24,7 +24,8 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
- dotnet-version: '5.0.100'
+ dotnet-version: '6.0.x'
+
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
new file mode 100644
index 0000000000..af4d8beb93
--- /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.5
+ env:
+ GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
+
+ check-backport:
+ name: Check Backport
+ if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify as seen
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ github.event.comment.id }}
+ reactions: eyes
+
+ - name: Checkout the latest code
+ uses: actions/checkout@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/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
new file mode 100644
index 0000000000..3e93468401
--- /dev/null
+++ b/.github/workflows/openapi.yml
@@ -0,0 +1,124 @@
+name: OpenAPI
+on:
+ push:
+ branches:
+ - master
+ pull_request_target:
+
+jobs:
+ openapi-head:
+ name: OpenAPI - HEAD
+ runs-on: ubuntu-latest
+ permissions: read-all
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.event.pull_request.head.ref }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '6.0.x'
+ - name: Generate openapi.json
+ run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
+ - name: Upload openapi.json
+ uses: actions/upload-artifact@v2
+ with:
+ name: openapi-head
+ retention-days: 14
+ if-no-files-found: error
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
+
+ openapi-base:
+ name: OpenAPI - BASE
+ if: ${{ github.base_ref != '' }}
+ runs-on: ubuntu-latest
+ permissions: read-all
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.base_ref }}
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '6.0.x'
+ - name: Generate openapi.json
+ run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
+ - name: Upload openapi.json
+ uses: actions/upload-artifact@v2
+ with:
+ name: openapi-base
+ retention-days: 14
+ if-no-files-found: error
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
+
+ openapi-diff:
+ name: OpenAPI - Difference
+ if: ${{ github.event_name == 'pull_request_target' }}
+ runs-on: ubuntu-latest
+ needs:
+ - openapi-head
+ - openapi-base
+ steps:
+ - name: Download openapi-head
+ uses: actions/download-artifact@v2
+ with:
+ name: openapi-head
+ path: openapi-head
+ - name: Download openapi-base
+ uses: actions/download-artifact@v2
+ with:
+ name: openapi-base
+ path: openapi-base
+ - name: Workaround openapi-diff issue
+ run: |
+ sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
+ sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
+ - name: Calculate OpenAPI difference
+ uses: docker://openapitools/openapi-diff
+ continue-on-error: true
+ with:
+ args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
+ - id: read-diff
+ name: Read openapi-diff output
+ run: |
+ body=$(cat openapi-changes.md)
+ body="${body//'%'/'%25'}"
+ body="${body//$'\n'/'%0A'}"
+ body="${body//$'\r'/'%0D'}"
+ echo ::set-output name=body::$body
+ - name: Find difference comment
+ uses: peter-evans/find-comment@v1
+ id: find-comment
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ direction: last
+ body-includes: openapi-diff-workflow-comment
+ - name: Reply or edit difference comment (changed)
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ steps.read-diff.outputs.body != '' }}
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ edit-mode: replace
+ body: |
+
+
+ Changes in OpenAPI specification found. Expand to see details.
+
+ ${{ steps.read-diff.outputs.body }}
+
+
+ - name: Edit difference comment (unchanged)
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ edit-mode: replace
+ body: |
+
+
+ No changes to OpenAPI specification found. See history of this comment for previous changes.
diff --git a/.gitignore b/.gitignore
index 7cd3d0068a..c2ae76c1e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -268,6 +268,7 @@ doc/
# Deployment artifacts
dist
*.exe
+*.dll
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts
@@ -277,3 +278,6 @@ web/
web-src.*
MediaBrowser.WebDashboard/jellyfin-web
apiclient/generated
+
+# Omnisharp crash logs
+mono_crash.*.json
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e55ea22485..b82956a721 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/net5.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.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/net5.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 1200275d52..d52e133249 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -17,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)
@@ -45,10 +46,12 @@
- [fruhnow](https://github.com/fruhnow)
- [geilername](https://github.com/geilername)
- [gnattu](https://github.com/gnattu)
+ - [GodTamIt](https://github.com/GodTamIt)
- [grafixeyehero](https://github.com/grafixeyehero)
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
+ - [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
@@ -68,6 +71,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)
@@ -104,10 +108,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)
+ - [ssenart](https://github.com/ssenart)
- [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles)
- [SuperSandro2000](https://github.com/SuperSandro2000)
@@ -143,6 +148,9 @@
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
+ - [peterspenler](https://github.com/peterspenler)
+ - [MBR-0001](https://github.com/MBR-0001)
+ - [jonas-resch](https://github.com/jonas-resch)
# Emby Contributors
@@ -207,3 +215,5 @@
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [olsh](https://github.com/olsh)
+ - [lbenini](https://github.com/lbenini)
+ - [gnuyent](https://github.com/gnuyent)
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000000..d243cde2b7
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,17 @@
+
+
+
+
+ enable
+ $(MSBuildThisFileDirectory)/jellyfin.ruleset
+
+
+
+ true
+
+
+
+ AllEnabledByDefault
+
+
+
diff --git a/Dockerfile b/Dockerfile
index 41dd3d081e..e133c08193 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,22 +1,18 @@
-ARG DOTNET_VERSION=5.0
+# DESIGNED FOR BUILDING ON AMD64 ONLY
+#####################################
+# Requires binfm_misc registration
+# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
+ARG DOTNET_VERSION=6.0
-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/sdk:${DOTNET_VERSION}-buster-slim 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:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -25,19 +21,17 @@ 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
+ARG GMMLIB_VERSION=21.2.1
+ARG IGC_VERSION=1.0.8517
+ARG NEO_VERSION=21.35.20826
+ARG LEVEL_ZERO_VERSION=1.2.20826
# Install dependencies:
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
+# curl: healthcheck
RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https curl \
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
&& apt-get update \
@@ -68,14 +62,32 @@ 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
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# because of changes in docker and systemd we need to not build in parallel at the moment
+# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm b/Dockerfile.arm
index e0eaca0edd..a46fa331df 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -1,31 +1,20 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
+# DESIGNED FOR BUILDING ON ARM ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.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/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:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -35,6 +24,8 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
+
+# curl: setup & healthcheck
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
@@ -53,7 +44,7 @@ RUN apt-get update \
vainfo \
libva2 \
locales \
- && apt-get remove curl gnupg -y \
+ && apt-get remove gnupg -y \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
@@ -61,17 +52,33 @@ 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 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# Discard objs - may cause failures if exists
+RUN find . -type d -name obj | xargs -r rm -r
+# Build
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index db7de935cf..1279c47f8e 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -1,18 +1,52 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
+# DESIGNED FOR BUILDING ON ARM64 ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.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 multiarch/qemu-user-static:x86_64-aarch64 as qemu
+FROM arm64v8/debian:stable-slim as app
+
+# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
+ARG DEBIAN_FRONTEND="noninteractive"
+# http://stackoverflow.com/questions/48162574/ddg#49462622
+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=qemu /usr/bin/qemu-aarch64-static /usr/bin
+
+# curl: healcheck
+RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
+ ffmpeg \
+ libssl-dev \
+ ca-certificates \
+ libfontconfig1 \
+ libfreetype6 \
+ libomxil-bellagio0 \
+ libomxil-bellagio-bin \
+ locales \
+ curl \
+ && apt-get clean autoclean -y \
+ && apt-get autoremove -y \
+ && rm -rf /var/lib/apt/lists/* \
+ && mkdir -p /cache /config /media \
+ && chmod 777 /cache /config /media \
+ && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
+
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+ENV LC_ALL en_US.UTF-8
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
@@ -23,44 +57,19 @@ 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 app
-# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
-ARG DEBIAN_FRONTEND="noninteractive"
-# http://stackoverflow.com/questions/48162574/ddg#49462622
-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=qemu /usr/bin/qemu-aarch64-static /usr/bin
-RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
- ffmpeg \
- libssl-dev \
- ca-certificates \
- libfontconfig1 \
- libfreetype6 \
- libomxil-bellagio0 \
- libomxil-bellagio-bin \
- locales \
- && apt-get clean autoclean -y \
- && apt-get autoremove -y \
- && rm -rf /var/lib/apt/lists/* \
- && mkdir -p /cache /config /media \
- && chmod 777 /cache /config /media \
- && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
+ENV HEALTHCHECK_URL=http://localhost:8096/health
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
-ENV LC_ALL en_US.UTF-8
-ENV LANG en_US.UTF-8
-ENV LANGUAGE en_US:en
-
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/bin/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
index 7bbd9acf82..755d29160c 100644
--- a/DvdLib/DvdLib.csproj
+++ b/DvdLib/DvdLib.csproj
@@ -10,10 +10,11 @@
- net5.0
+ net6.0
false
true
- true
+ AllDisabledByDefault
+ disable
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
index b4a11ed5d6..7f8ece47dc 100644
--- a/DvdLib/Ifo/Dvd.cs
+++ b/DvdLib/Ifo/Dvd.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
@@ -76,7 +77,7 @@ namespace DvdLib.Ifo
private void ReadVTS(ushort vtsNum, IReadOnlyList allFiles)
{
- var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
+ var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs
index e63a858605..91fac4bef5 100644
--- a/Emby.Dlna/Configuration/DlnaOptions.cs
+++ b/Emby.Dlna/Configuration/DlnaOptions.cs
@@ -72,7 +72,7 @@ namespace Emby.Dlna.Configuration
///
/// Gets or sets the default user account that the dlna server uses.
///
- public string DefaultUserId { get; set; }
+ public string? DefaultUserId { get; set; }
///
/// Gets or sets a value indicating whether playTo device profiles should be created.
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/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs
index 2f8d197a7a..1a1790ee6a 100644
--- a/Emby.Dlna/ConnectionManager/ControlHandler.cs
+++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs
@@ -31,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))
{
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
index 2f3107450c..9020dea994 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
@@ -138,7 +138,7 @@ namespace Emby.Dlna.ContentDirectory
///
/// The .
/// The .
- private User GetUser(DeviceProfile profile)
+ private User? GetUser(DeviceProfile profile)
{
if (!string.IsNullOrEmpty(profile.UserId))
{
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 27f1fdabad..fde3f2f893 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -1,5 +1,6 @@
+#nullable disable
+
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -7,7 +8,6 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
-using Emby.Dlna.Configuration;
using Emby.Dlna.Didl;
using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
@@ -18,23 +18,16 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
-using Book = MediaBrowser.Controller.Entities.Book;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Dlna.ContentDirectory
{
@@ -50,7 +43,6 @@ namespace Emby.Dlna.ContentDirectory
private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataManager;
- private readonly IServerConfigurationManager _config;
private readonly User _user;
private readonly IUserViewManager _userViewManager;
private readonly ITVSeriesManager _tvSeriesManager;
@@ -104,7 +96,6 @@ namespace Emby.Dlna.ContentDirectory
_userViewManager = userViewManager;
_tvSeriesManager = tvSeriesManager;
_profile = profile;
- _config = config;
_didlBuilder = new DidlBuilder(
profile,
@@ -121,7 +112,7 @@ 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)
{
@@ -201,8 +192,8 @@ namespace Emby.Dlna.ContentDirectory
///
/// Adds a "XSetBookmark" element to the xml document.
///
- /// The .
- private void HandleXSetBookmark(IDictionary sparams)
+ /// The method parameters.
+ private void HandleXSetBookmark(IReadOnlyDictionary sparams)
{
var id = sparams["ObjectID"];
@@ -288,52 +279,28 @@ namespace Emby.Dlna.ContentDirectory
/// The xml feature list.
private static 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();
- }
-
- ///
- /// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist.
- ///
- /// The .
- /// The key.
- /// The defaultValue.
- /// The .
- public static string GetValueOrDefault(IDictionary sparams, string key, string defaultValue)
- {
- if (sparams != null && sparams.TryGetValue(key, out string val))
- {
- return val;
- }
-
- return defaultValue;
+ return ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "";
}
///
/// Builds the "Browse" xml response.
///
/// The .
- /// The .
+ /// The method parameters.
/// The device Id to use.
- private void HandleBrowse(XmlWriter xmlWriter, IDictionary sparams, string deviceId)
+ 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;
@@ -354,75 +321,73 @@ namespace Emby.Dlna.ContentDirectory
int totalCount;
- using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
+ var settings = new XmlWriterSettings
{
- var settings = new XmlWriterSettings()
+ Encoding = Encoding.UTF8,
+ CloseOutput = false,
+ OmitXmlDeclaration = true,
+ ConformanceLevel = ConformanceLevel.Fragment
+ };
+
+ using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
+ using (var writer = XmlWriter.Create(builder, settings))
+ {
+ writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
+
+ writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+ writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+ writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
+
+ DidlBuilder.WriteXmlRootAttributes(_profile, writer);
+
+ var serverItem = GetItemFromObjectId(id);
+ var item = serverItem.Item;
+
+ if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
{
- Encoding = Encoding.UTF8,
- CloseOutput = false,
- OmitXmlDeclaration = true,
- ConformanceLevel = ConformanceLevel.Fragment
- };
+ totalCount = 1;
- using (var writer = XmlWriter.Create(builder, settings))
- {
- writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
-
- writer.WriteAttributeString("xmlns", "dc", null, NsDc);
- writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
- writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
-
- DidlBuilder.WriteXmlRootAttributes(_profile, writer);
-
- var serverItem = GetItemFromObjectId(id);
- var item = serverItem.Item;
-
- if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
+ if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
{
- totalCount = 1;
+ var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
- if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
- {
- var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
-
- _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter);
- }
-
- provided++;
+ _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
}
else
{
- var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
- totalCount = childrenResult.TotalRecordCount;
-
- provided = childrenResult.Items.Count;
-
- foreach (var i in childrenResult.Items)
- {
- var childItem = i.Item;
- var displayStubType = i.StubType;
-
- if (childItem.IsDisplayedAsFolder || displayStubType.HasValue)
- {
- var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0)
- .TotalRecordCount;
-
- _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, childItem, _user, item, serverItem.StubType, deviceId, filter);
- }
- }
+ _didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter);
}
- writer.WriteFullEndElement();
+ provided++;
+ }
+ else
+ {
+ var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
+ totalCount = childrenResult.TotalRecordCount;
+
+ provided = childrenResult.Items.Count;
+
+ foreach (var i in childrenResult.Items)
+ {
+ var childItem = i.Item;
+ var displayStubType = i.StubType;
+
+ if (childItem.IsDisplayedAsFolder || displayStubType.HasValue)
+ {
+ var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0)
+ .TotalRecordCount;
+
+ _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter);
+ }
+ else
+ {
+ _didlBuilder.WriteItemElement(writer, childItem, _user, item, serverItem.StubType, deviceId, filter);
+ }
+ }
}
+ writer.WriteFullEndElement();
+ writer.Flush();
xmlWriter.WriteElementString("Result", builder.ToString());
}
@@ -435,9 +400,9 @@ namespace Emby.Dlna.ContentDirectory
/// Builds the response to the "X_BrowseByLetter request.
///
/// The .
- /// The .
+ /// The method parameters.
/// The device id.
- private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary sparams, string deviceId)
+ private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary sparams, string deviceId)
{
// TODO: Implement this method
HandleSearch(xmlWriter, sparams, deviceId);
@@ -447,13 +412,13 @@ namespace Emby.Dlna.ContentDirectory
/// Builds a response to the "Search" request.
///
/// The xmlWriter.
- /// The sparams.
+ /// The method parameters.
/// The deviceId.
- private void HandleSearch(XmlWriter xmlWriter, IDictionary sparams, string 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
@@ -473,53 +438,46 @@ namespace Emby.Dlna.ContentDirectory
}
QueryResult childrenResult;
+ var settings = new XmlWriterSettings
+ {
+ Encoding = Encoding.UTF8,
+ CloseOutput = false,
+ OmitXmlDeclaration = true,
+ ConformanceLevel = ConformanceLevel.Fragment
+ };
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
+ using (var writer = XmlWriter.Create(builder, settings))
{
- var settings = new XmlWriterSettings()
+ writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
+ writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+ writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+ writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
+
+ DidlBuilder.WriteXmlRootAttributes(_profile, writer);
+
+ var serverItem = GetItemFromObjectId(sparams["ContainerID"]);
+
+ var item = serverItem.Item;
+
+ childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount);
+ foreach (var i in childrenResult.Items)
{
- Encoding = Encoding.UTF8,
- CloseOutput = false,
- OmitXmlDeclaration = true,
- ConformanceLevel = ConformanceLevel.Fragment
- };
-
- using (var writer = XmlWriter.Create(builder, settings))
- {
- writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
-
- writer.WriteAttributeString("xmlns", "dc", null, NsDc);
- writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
- writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
-
- DidlBuilder.WriteXmlRootAttributes(_profile, writer);
-
- var serverItem = GetItemFromObjectId(sparams["ContainerID"]);
-
- var item = serverItem.Item;
-
- childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount);
-
- var dlnaOptions = _config.GetDlnaConfiguration();
-
- foreach (var i in childrenResult.Items)
+ if (i.IsDisplayedAsFolder)
{
- if (i.IsDisplayedAsFolder)
- {
- var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0)
- .TotalRecordCount;
+ var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0)
+ .TotalRecordCount;
- _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, i, _user, item, serverItem.StubType, deviceId, filter);
- }
+ _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter);
+ }
+ else
+ {
+ _didlBuilder.WriteItemElement(writer, i, _user, item, serverItem.StubType, deviceId, filter);
}
-
- writer.WriteFullEndElement();
}
+ writer.WriteFullEndElement();
+ writer.Flush();
xmlWriter.WriteElementString("Result", builder.ToString());
}
@@ -542,48 +500,38 @@ namespace Emby.Dlna.ContentDirectory
{
var folder = (Folder)item;
- var sortOrders = folder.IsPreSorted
- ? Array.Empty<(string, SortOrder)>()
- : new[] { (ItemSortBy.SortName, sort.SortOrder) };
-
string[] mediaTypes = Array.Empty();
bool? isFolder = null;
- if (search.SearchType == SearchType.Audio)
+ switch (search.SearchType)
{
- mediaTypes = new[] { MediaType.Audio };
- isFolder = false;
- }
- else if (search.SearchType == SearchType.Video)
- {
- mediaTypes = new[] { MediaType.Video };
- isFolder = false;
- }
- else if (search.SearchType == SearchType.Image)
- {
- mediaTypes = new[] { MediaType.Photo };
- isFolder = false;
- }
- else if (search.SearchType == SearchType.Playlist)
- {
- // items = items.OfType();
- isFolder = true;
- }
- else if (search.SearchType == SearchType.MusicAlbum)
- {
- // items = items.OfType();
- isFolder = true;
+ case SearchType.Audio:
+ mediaTypes = new[] { MediaType.Audio };
+ isFolder = false;
+ break;
+ case SearchType.Video:
+ mediaTypes = new[] { MediaType.Video };
+ isFolder = false;
+ break;
+ case SearchType.Image:
+ mediaTypes = new[] { MediaType.Photo };
+ isFolder = false;
+ break;
+ case SearchType.Playlist:
+ case SearchType.MusicAlbum:
+ isFolder = true;
+ break;
}
return folder.GetItems(new InternalItemsQuery
{
Limit = limit,
StartIndex = startIndex,
- OrderBy = sortOrders,
+ OrderBy = GetOrderBy(sort, folder.IsPreSorted),
User = user,
Recursive = true,
IsMissing = false,
- ExcludeItemTypes = new[] { nameof(Book) },
+ ExcludeItemTypes = new[] { BaseItemKind.Book },
IsFolder = isFolder,
MediaTypes = mediaTypes,
DtoOptions = GetDtoOptions()
@@ -611,52 +559,49 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private QueryResult GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit)
{
- if (item is MusicGenre)
+ switch (item)
{
- return GetMusicGenreItems(item, Guid.Empty, user, sort, startIndex, limit);
+ case MusicGenre:
+ return GetMusicGenreItems(item, user, sort, startIndex, limit);
+ case MusicArtist:
+ return GetMusicArtistItems(item, user, sort, startIndex, limit);
+ case Genre:
+ return GetGenreItems(item, user, sort, startIndex, limit);
}
- if (item is MusicArtist)
+ if (stubType != StubType.Folder && item is IHasCollectionType collectionFolder)
{
- return GetMusicArtistItems(item, Guid.Empty, user, sort, startIndex, limit);
- }
-
- if (item is Genre)
- {
- return GetGenreItems(item, Guid.Empty, user, sort, startIndex, limit);
- }
-
- if ((!stubType.HasValue || stubType.Value != StubType.Folder)
- && item is IHasCollectionType collectionFolder)
- {
- if (string.Equals(CollectionType.Music, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+ var collectionType = collectionFolder.CollectionType;
+ if (string.Equals(CollectionType.Music, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
}
- else if (string.Equals(CollectionType.Movies, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.Movies, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
}
- else if (string.Equals(CollectionType.TvShows, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.TvShows, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetTvFolders(item, user, stubType, sort, startIndex, limit);
}
- else if (string.Equals(CollectionType.Folders, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.Folders, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetFolders(user, startIndex, limit);
}
- else if (string.Equals(CollectionType.LiveTv, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.LiveTv, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetLiveTvChannels(user, sort, startIndex, limit);
}
}
- if (stubType.HasValue)
+ if (stubType.HasValue && stubType.Value != StubType.Folder)
{
- if (stubType.Value != StubType.Folder)
- {
- return ApplyPaging(new QueryResult(), startIndex, limit);
- }
+ // TODO should this be doing something?
+ return new QueryResult();
}
var folder = (Folder)item;
@@ -666,13 +611,12 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit,
StartIndex = startIndex,
IsVirtualItem = false,
- ExcludeItemTypes = new[] { nameof(Book) },
+ ExcludeItemTypes = new[] { BaseItemKind.Book },
IsPlaceHolder = false,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, folder.IsPreSorted)
};
- SetSorting(query, sort, folder.IsPreSorted);
-
var queryResult = folder.GetItems(query);
return ToResult(queryResult);
@@ -692,10 +636,9 @@ namespace Emby.Dlna.ContentDirectory
{
StartIndex = startIndex,
Limit = limit,
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
+ OrderBy = GetOrderBy(sort, false)
};
- query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
-
- SetSorting(query, sort, false);
var result = _libraryManager.GetItemsResult(query);
@@ -717,117 +660,57 @@ namespace Emby.Dlna.ContentDirectory
var query = new InternalItemsQuery(user)
{
StartIndex = startIndex,
- Limit = limit
+ Limit = limit,
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
- if (stubType.HasValue && stubType.Value == StubType.Latest)
+ switch (stubType)
{
- return GetMusicLatest(item, user, query);
+ case StubType.Latest:
+ return GetLatest(item, query, BaseItemKind.Audio);
+ case StubType.Playlists:
+ return GetMusicPlaylists(query);
+ case StubType.Albums:
+ return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum);
+ case StubType.Artists:
+ return GetMusicArtists(item, query);
+ case StubType.AlbumArtists:
+ return GetMusicAlbumArtists(item, query);
+ case StubType.FavoriteAlbums:
+ return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum, true);
+ case StubType.FavoriteArtists:
+ return GetFavoriteArtists(item, query);
+ case StubType.FavoriteSongs:
+ return GetChildrenOfItem(item, query, BaseItemKind.Audio, true);
+ case StubType.Songs:
+ return GetChildrenOfItem(item, query, BaseItemKind.Audio);
+ case StubType.Genres:
+ return GetMusicGenres(item, query);
}
- if (stubType.HasValue && stubType.Value == StubType.Playlists)
+ var serverItems = new ServerItem[]
{
- return GetMusicPlaylists(user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Albums)
- {
- return GetMusicAlbums(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Artists)
- {
- return GetMusicArtists(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.AlbumArtists)
- {
- return GetMusicAlbumArtists(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteAlbums)
- {
- return GetFavoriteAlbums(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteArtists)
- {
- return GetFavoriteArtists(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteSongs)
- {
- return GetFavoriteSongs(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Songs)
- {
- return GetMusicSongs(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Genres)
- {
- return GetMusicGenres(item, user, query);
- }
-
- var list = new List
- {
- new ServerItem(item)
- {
- StubType = StubType.Latest
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Playlists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Albums
- },
-
- new ServerItem(item)
- {
- StubType = StubType.AlbumArtists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Artists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Songs
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Genres
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteArtists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteAlbums
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteSongs
- }
+ new (item, StubType.Latest),
+ new (item, StubType.Playlists),
+ new (item, StubType.Albums),
+ new (item, StubType.AlbumArtists),
+ new (item, StubType.Artists),
+ new (item, StubType.Songs),
+ new (item, StubType.Genres),
+ new (item, StubType.FavoriteArtists),
+ new (item, StubType.FavoriteAlbums),
+ new (item, StubType.FavoriteSongs)
};
+ if (limit < serverItems.Length)
+ {
+ serverItems = serverItems[..limit.Value];
+ }
+
return new QueryResult
{
- Items = list,
- TotalRecordCount = list.Count
+ Items = serverItems,
+ TotalRecordCount = serverItems.Length
};
}
@@ -846,68 +729,41 @@ namespace Emby.Dlna.ContentDirectory
var query = new InternalItemsQuery(user)
{
StartIndex = startIndex,
- Limit = limit
+ Limit = limit,
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
- if (stubType.HasValue && stubType.Value == StubType.ContinueWatching)
+ switch (stubType)
{
- return GetMovieContinueWatching(item, user, query);
+ case StubType.ContinueWatching:
+ return GetMovieContinueWatching(item, query);
+ case StubType.Latest:
+ return GetLatest(item, query, BaseItemKind.Movie);
+ case StubType.Movies:
+ return GetChildrenOfItem(item, query, BaseItemKind.Movie);
+ case StubType.Collections:
+ return GetMovieCollections(query);
+ case StubType.Favorites:
+ return GetChildrenOfItem(item, query, BaseItemKind.Movie, true);
+ case StubType.Genres:
+ return GetGenres(item, query);
}
- if (stubType.HasValue && stubType.Value == StubType.Latest)
+ var array = new ServerItem[]
{
- return GetMovieLatest(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Movies)
- {
- return GetMovieMovies(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Collections)
- {
- return GetMovieCollections(user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Favorites)
- {
- return GetMovieFavorites(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Genres)
- {
- return GetGenres(item, user, query);
- }
-
- var array = new[]
- {
- new ServerItem(item)
- {
- StubType = StubType.ContinueWatching
- },
- new ServerItem(item)
- {
- StubType = StubType.Latest
- },
- new ServerItem(item)
- {
- StubType = StubType.Movies
- },
- new ServerItem(item)
- {
- StubType = StubType.Collections
- },
- new ServerItem(item)
- {
- StubType = StubType.Favorites
- },
- new ServerItem(item)
- {
- StubType = StubType.Genres
- }
+ new (item, StubType.ContinueWatching),
+ new (item, StubType.Latest),
+ new (item, StubType.Movies),
+ new (item, StubType.Collections),
+ new (item, StubType.Favorites),
+ new (item, StubType.Genres)
};
+ if (limit < array.Length)
+ {
+ array = array[..limit.Value];
+ }
+
return new QueryResult
{
Items = array,
@@ -924,22 +780,21 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private QueryResult GetFolders(User user, int? startIndex, int? limit)
{
- var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true)
+ var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true);
+ var totalRecordCount = folders.Count;
+ // Handle paging
+ var items = folders
.OrderBy(i => i.SortName)
- .Select(i => new ServerItem(i)
- {
- StubType = StubType.Folder
- })
+ .Skip(startIndex ?? 0)
+ .Take(limit ?? int.MaxValue)
+ .Select(i => new ServerItem(i, StubType.Folder))
.ToArray();
- return ApplyPaging(
- new QueryResult
- {
- Items = folders,
- TotalRecordCount = folders.Length
- },
- startIndex,
- limit);
+ return new QueryResult
+ {
+ Items = items,
+ TotalRecordCount = totalRecordCount
+ };
}
///
@@ -957,87 +812,48 @@ namespace Emby.Dlna.ContentDirectory
var query = new InternalItemsQuery(user)
{
StartIndex = startIndex,
- Limit = limit
+ Limit = limit,
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
- if (stubType.HasValue && stubType.Value == StubType.ContinueWatching)
+ switch (stubType)
{
- return GetMovieContinueWatching(item, user, query);
+ case StubType.ContinueWatching:
+ return GetMovieContinueWatching(item, query);
+ case StubType.NextUp:
+ return GetNextUp(item, query);
+ case StubType.Latest:
+ return GetLatest(item, query, BaseItemKind.Episode);
+ case StubType.Series:
+ return GetChildrenOfItem(item, query, BaseItemKind.Series);
+ case StubType.FavoriteSeries:
+ return GetChildrenOfItem(item, query, BaseItemKind.Series, true);
+ case StubType.FavoriteEpisodes:
+ return GetChildrenOfItem(item, query, BaseItemKind.Episode, true);
+ case StubType.Genres:
+ return GetGenres(item, query);
}
- if (stubType.HasValue && stubType.Value == StubType.NextUp)
+ var serverItems = new ServerItem[]
{
- return GetNextUp(item, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Latest)
- {
- return GetTvLatest(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Series)
- {
- return GetSeries(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteSeries)
- {
- return GetFavoriteSeries(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteEpisodes)
- {
- return GetFavoriteEpisodes(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Genres)
- {
- return GetGenres(item, user, query);
- }
-
- var list = new List
- {
- new ServerItem(item)
- {
- StubType = StubType.ContinueWatching
- },
-
- new ServerItem(item)
- {
- StubType = StubType.NextUp
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Latest
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Series
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteSeries
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteEpisodes
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Genres
- }
+ new (item, StubType.ContinueWatching),
+ new (item, StubType.NextUp),
+ new (item, StubType.Latest),
+ new (item, StubType.Series),
+ new (item, StubType.FavoriteSeries),
+ new (item, StubType.FavoriteEpisodes),
+ new (item, StubType.Genres)
};
+ if (limit < serverItems.Length)
+ {
+ serverItems = serverItems[..limit.Value];
+ }
+
return new QueryResult
{
- Items = list,
- TotalRecordCount = list.Count
+ Items = serverItems,
+ TotalRecordCount = serverItems.Length
};
}
@@ -1045,14 +861,12 @@ 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)
+ private QueryResult GetMovieContinueWatching(BaseItem parent, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
- query.SetUser(user);
query.OrderBy = new[]
{
@@ -1061,47 +875,7 @@ namespace Emby.Dlna.ContentDirectory
};
query.IsResumable = true;
- query.Limit = 10;
-
- var result = _libraryManager.GetItemsResult(query);
-
- 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[] { 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[] { nameof(Movie) };
+ query.Limit ??= 10;
var result = _libraryManager.GetItemsResult(query);
@@ -1111,16 +885,12 @@ namespace Emby.Dlna.ContentDirectory
///
/// Returns the Movie collections meeting the criteria.
///
- /// The see cref="User"/>.
/// The see cref="InternalItemsQuery"/>.
/// The .
- private QueryResult GetMovieCollections(User user, InternalItemsQuery query)
+ private QueryResult GetMovieCollections(InternalItemsQuery query)
{
query.Recursive = true;
- // query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(BoxSet) };
+ query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
var result = _libraryManager.GetItemsResult(query);
@@ -1128,139 +898,19 @@ namespace Emby.Dlna.ContentDirectory
}
///
- /// Returns the Music albums meeting the criteria.
+ /// Returns the children that meet the criteria.
///
/// The .
- /// The .
/// The .
+ /// The item type.
+ /// A value indicating whether to only fetch favorite items.
/// The .
- private QueryResult GetMusicAlbums(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType, bool isFavorite = false)
{
query.Recursive = true;
query.Parent = parent;
- query.SetUser(user);
-
- 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[] { 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[] { 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[] { 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[] { 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[] { 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[] { nameof(MusicAlbum) };
+ query.IsFavorite = isFavorite;
+ query.IncludeItemTypes = new[] { itemType };
var result = _libraryManager.GetItemsResult(query);
@@ -1272,139 +922,90 @@ namespace Emby.Dlna.ContentDirectory
/// The GetGenres.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetGenres(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetGenres(BaseItem parent, InternalItemsQuery query)
{
- var genresResult = _libraryManager.GetGenres(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var genresResult = _libraryManager.GetGenres(query);
- var result = new QueryResult
- {
- TotalRecordCount = genresResult.TotalRecordCount,
- Items = genresResult.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ return ToResult(genresResult);
}
///
/// Returns the music genres meeting the criteria.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetMusicGenres(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetMusicGenres(BaseItem parent, InternalItemsQuery query)
{
- var genresResult = _libraryManager.GetMusicGenres(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var genresResult = _libraryManager.GetMusicGenres(query);
- var result = new QueryResult
- {
- TotalRecordCount = genresResult.TotalRecordCount,
- Items = genresResult.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ return ToResult(genresResult);
}
///
/// Returns the music albums by artist that meet the criteria.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetMusicAlbumArtists(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query)
{
- var artists = _libraryManager.GetAlbumArtists(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var artists = _libraryManager.GetAlbumArtists(query);
- var result = new QueryResult
- {
- TotalRecordCount = artists.TotalRecordCount,
- Items = artists.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ return ToResult(artists);
}
///
/// Returns the music artists meeting the criteria.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetMusicArtists(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetMusicArtists(BaseItem parent, InternalItemsQuery query)
{
- var artists = _libraryManager.GetArtists(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
-
- var result = new QueryResult
- {
- TotalRecordCount = artists.TotalRecordCount,
- Items = artists.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var artists = _libraryManager.GetArtists(query);
+ return ToResult(artists);
}
///
/// Returns the artists tagged as favourite that meet the criteria.
///
/// The .
- /// The .
/// The .
/// The .
- private QueryResult GetFavoriteArtists(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetFavoriteArtists(BaseItem parent, InternalItemsQuery query)
{
- var artists = _libraryManager.GetArtists(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit,
- IsFavorite = true
- });
-
- var result = new QueryResult
- {
- TotalRecordCount = artists.TotalRecordCount,
- Items = artists.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ query.IsFavorite = true;
+ var artists = _libraryManager.GetArtists(query);
+ return ToResult(artists);
}
///
/// Returns the music playlists meeting the criteria.
///
- /// The user.
/// The query.
/// The .
- private QueryResult GetMusicPlaylists(User user, InternalItemsQuery query)
+ private QueryResult GetMusicPlaylists(InternalItemsQuery query)
{
query.Parent = null;
- query.IncludeItemTypes = new[] { nameof(Playlist) };
- query.SetUser(user);
+ query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
query.Recursive = true;
var result = _libraryManager.GetItemsResult(query);
@@ -1412,31 +1013,6 @@ 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)>();
-
- var items = _userViewManager.GetLatestItems(
- new LatestItemsQuery
- {
- UserId = user.Id,
- Limit = 50,
- IncludeItemTypes = new[] { nameof(Audio) },
- ParentId = parent?.Id ?? Guid.Empty,
- GroupItems = true
- },
- query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
-
- return ToResult(items);
- }
-
///
/// Returns the next up item meeting the criteria.
///
@@ -1452,7 +1028,8 @@ namespace Emby.Dlna.ContentDirectory
{
Limit = query.Limit,
StartIndex = query.StartIndex,
- UserId = query.User.Id
+ // User cannot be null here as the caller has set it
+ UserId = query.User!.Id
},
new[] { parent },
query.DtoOptions);
@@ -1461,47 +1038,23 @@ namespace Emby.Dlna.ContentDirectory
}
///
- /// Returns the latest tv meeting the criteria.
+ /// Returns the latest items of [itemType] meeting the criteria.
///
/// The .
- /// The .
/// The .
+ /// The item type.
/// The .
- private QueryResult GetTvLatest(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
- UserId = user.Id,
- Limit = 50,
- IncludeItemTypes = new[] { nameof(Episode) },
- ParentId = parent == null ? Guid.Empty : parent.Id,
- GroupItems = false
- },
- query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
-
- 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)>();
-
- var items = _userViewManager.GetLatestItems(
- new LatestItemsQuery
- {
- UserId = user.Id,
- Limit = 50,
- IncludeItemTypes = new[] { nameof(Movie) },
+ // User cannot be null here as the caller has set it
+ UserId = query.User!.Id,
+ Limit = query.Limit ?? 50,
+ IncludeItemTypes = new[] { itemType },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
},
@@ -1514,27 +1067,24 @@ namespace Emby.Dlna.ContentDirectory
/// 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)
+ private QueryResult GetMusicArtistItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
{
Recursive = true,
- ParentId = parentId,
ArtistIds = new[] { item.Id },
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
Limit = limit,
StartIndex = startIndex,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
-
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
@@ -1544,31 +1094,28 @@ namespace Emby.Dlna.ContentDirectory
/// 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)
+ private QueryResult GetGenreItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
{
Recursive = true,
- ParentId = parentId,
GenreIds = new[] { item.Id },
IncludeItemTypes = new[]
{
- nameof(Movie),
- nameof(Series)
+ BaseItemKind.Movie,
+ BaseItemKind.Series
},
Limit = limit,
StartIndex = startIndex,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
-
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
@@ -1578,46 +1125,43 @@ namespace Emby.Dlna.ContentDirectory
/// 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)
+ private QueryResult GetMusicGenreItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
{
Recursive = true,
- ParentId = parentId,
GenreIds = new[] { item.Id },
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
Limit = limit,
StartIndex = startIndex,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
-
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
///
- /// Converts a array into a .
+ /// Converts into a .
///
/// An array of .
/// A .
- private static QueryResult ToResult(BaseItem[] result)
+ private static QueryResult ToResult(IReadOnlyCollection result)
{
var serverItems = result
- .Select(i => new ServerItem(i))
+ .Select(i => new ServerItem(i, null))
.ToArray();
return new QueryResult
{
- TotalRecordCount = result.Length,
+ TotalRecordCount = result.Count,
Items = serverItems
};
}
@@ -1629,10 +1173,12 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private static QueryResult ToResult(QueryResult result)
{
- var serverItems = result
- .Items
- .Select(i => new ServerItem(i))
- .ToArray();
+ var length = result.Items.Count;
+ var serverItems = new ServerItem[length];
+ for (var i = 0; i < length; i++)
+ {
+ serverItems[i] = new ServerItem(result.Items[i], null);
+ }
return new QueryResult
{
@@ -1642,35 +1188,34 @@ namespace Emby.Dlna.ContentDirectory
}
///
- /// Sets the sorting method on a query.
+ /// Converts a query result to a .
///
- /// The .
- /// The .
- /// True if pre-sorted.
- private static void SetSorting(InternalItemsQuery query, SortCriteria sort, bool isPreSorted)
+ /// A .
+ /// The .
+ private static QueryResult ToResult(QueryResult<(BaseItem, ItemCounts)> result)
{
- if (isPreSorted)
+ var length = result.Items.Count;
+ var serverItems = new ServerItem[length];
+ for (var i = 0; i < length; i++)
{
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ serverItems[i] = new ServerItem(result.Items[i].Item1, null);
}
- else
+
+ return new QueryResult
{
- query.OrderBy = new[] { (ItemSortBy.SortName, sort.SortOrder) };
- }
+ TotalRecordCount = result.TotalRecordCount,
+ Items = serverItems
+ };
}
///
- /// Apply paging to a query.
+ /// Gets the sorting method on a query.
///
- /// The .
- /// The start index.
- /// The maximum number to return.
- /// A .
- private static QueryResult ApplyPaging(QueryResult result, int? startIndex, int? limit)
+ /// The .
+ /// True if pre-sorted.
+ private static (string, SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
{
- result.Items = result.Items.Skip(startIndex ?? 0).Take(limit ?? int.MaxValue).ToArray();
-
- return result;
+ return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
}
///
@@ -1681,7 +1226,7 @@ namespace Emby.Dlna.ContentDirectory
private ServerItem GetItemFromObjectId(string id)
{
return DidlBuilder.IsIdRoot(id)
- ? new ServerItem(_libraryManager.GetUserRootFolder())
+ ? new ServerItem(_libraryManager.GetUserRootFolder(), null)
: ParseItemId(id);
}
@@ -1699,37 +1244,29 @@ namespace Emby.Dlna.ContentDirectory
var paramsIndex = id.IndexOf(ParamsSrch, StringComparison.OrdinalIgnoreCase);
if (paramsIndex != -1)
{
- id = id.Substring(paramsIndex + ParamsSrch.Length);
+ id = id[(paramsIndex + ParamsSrch.Length)..];
var parts = id.Split(';');
id = parts[23];
}
- var enumNames = Enum.GetNames(typeof(StubType));
- foreach (var name in enumNames)
+ var dividerIndex = id.IndexOf('_', StringComparison.Ordinal);
+ if (dividerIndex != -1 && Enum.TryParse(id.AsSpan(0, dividerIndex), true, out var parsedStubType))
{
- if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
- {
- stubType = Enum.Parse(name, true);
- id = id.Split('_', 2)[1];
-
- break;
- }
+ id = id[(dividerIndex + 1)..];
+ stubType = parsedStubType;
}
if (Guid.TryParse(id, out var itemId))
{
var item = _libraryManager.GetItemById(itemId);
- return new ServerItem(item)
- {
- StubType = stubType
- };
+ return new ServerItem(item, stubType);
}
Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id);
- return new ServerItem(_libraryManager.GetUserRootFolder());
+ return new ServerItem(_libraryManager.GetUserRootFolder(), null);
}
}
}
diff --git a/Emby.Dlna/ContentDirectory/ServerItem.cs b/Emby.Dlna/ContentDirectory/ServerItem.cs
index 34244000c1..df05fa9666 100644
--- a/Emby.Dlna/ContentDirectory/ServerItem.cs
+++ b/Emby.Dlna/ContentDirectory/ServerItem.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory
@@ -13,24 +11,29 @@ namespace Emby.Dlna.ContentDirectory
/// Initializes a new instance of the class.
///
/// The .
- public ServerItem(BaseItem item)
+ /// The stub type.
+ public ServerItem(BaseItem item, StubType? stubType)
{
Item = item;
- if (item is IItemByName && !(item is Folder))
+ if (stubType.HasValue)
+ {
+ StubType = stubType;
+ }
+ else if (item is IItemByName and not Folder)
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}
}
///
- /// Gets or sets the underlying base item.
+ /// Gets the underlying base item.
///
- public BaseItem Item { get; set; }
+ public BaseItem Item { get; }
///
- /// Gets or sets the DLNA item type.
+ /// Gets the DLNA item type.
///
- public StubType? StubType { get; set; }
+ public StubType? StubType { get; }
}
}
diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs
index 982ae5d68e..187dc1d75a 100644
--- a/Emby.Dlna/ContentDirectory/StubType.cs
+++ b/Emby.Dlna/ContentDirectory/StubType.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1602
namespace Emby.Dlna.ContentDirectory
{
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 abaf522bca..b00e1c98af 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;
@@ -39,8 +41,6 @@ namespace Emby.Dlna.Didl
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor;
private readonly string _serverAddress;
@@ -96,6 +96,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();
@@ -207,7 +208,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(),
@@ -313,7 +315,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue)
{
- writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
+ writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
if (filter.Contains("res@size"))
@@ -324,7 +326,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue)
{
- writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
+ writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
}
}
}
@@ -338,7 +340,7 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue)
{
- writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
+ writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
}
if (filter.Contains("res@resolution"))
@@ -357,12 +359,12 @@ namespace Emby.Dlna.Didl
if (targetSampleRate.HasValue)
{
- writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
if (totalBitrate.HasValue)
{
- writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
var mediaProfile = _profile.GetVideoMediaProfile(
@@ -548,7 +550,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue)
{
- writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
+ writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
if (filter.Contains("res@size"))
@@ -559,7 +561,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue)
{
- writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
+ writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
}
}
}
@@ -571,17 +573,17 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue)
{
- writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
+ writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
}
if (targetSampleRate.HasValue)
{
- writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
if (targetAudioBitrate.HasValue)
{
- writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
var mediaProfile = _profile.GetAudioMediaProfile(
@@ -598,7 +600,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,
@@ -634,7 +637,7 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("searchable", "1");
- writer.WriteAttributeString("childCount", childCount.ToString(_usCulture));
+ writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
var clientId = GetClientId(folder, stubType);
@@ -726,7 +729,7 @@ namespace Emby.Dlna.Didl
{
if (item.PremiereDate.HasValue)
{
- AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
+ AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc);
}
}
@@ -743,7 +746,7 @@ namespace Emby.Dlna.Didl
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
}
- if (!(item is Folder))
+ if (item is not Folder)
{
if (filter.Contains("dc:description"))
{
@@ -926,11 +929,11 @@ namespace Emby.Dlna.Didl
if (item.IndexNumber.HasValue)
{
- AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
+ AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
if (item is Episode)
{
- AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
+ AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
}
}
}
@@ -973,15 +976,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 +1048,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 +1220,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/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 21ba1c7552..d9d2a345ad 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -1,20 +1,18 @@
#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Globalization;
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.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@@ -36,7 +34,7 @@ namespace Emby.Dlna
private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly Dictionary> _profiles = new Dictionary>(StringComparer.Ordinal);
@@ -94,12 +92,14 @@ namespace Emby.Dlna
}
}
+ ///
public DeviceProfile GetDefaultProfile()
{
return new DefaultProfile();
}
- public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
+ ///
+ public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
{
if (deviceInfo == null)
{
@@ -109,109 +109,57 @@ 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);
+ _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
}
else
{
- LogUnmatchedProfile(deviceInfo);
+ _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
}
return profile;
}
- private void LogUnmatchedProfile(DeviceIdentification profile)
+ ///
+ /// Attempts to match a device with a profile.
+ /// Rules:
+ /// - If the profile field has no value, the field matches irregardless of its contents.
+ /// - the profile field can be an exact match, or a reg exp.
+ ///
+ /// The of the device.
+ /// The of the profile.
+ /// True if they match.
+ public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
{
- 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);
-
- _logger.LogInformation(builder.ToString());
- }
-
- private 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;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
- {
- if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
- {
- if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
- {
- return false;
- }
- }
-
- return true;
+ 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);
}
private bool IsRegexOrSubstringMatch(string input, string pattern)
{
+ if (string.IsNullOrEmpty(pattern))
+ {
+ // In profile identification: An empty pattern matches anything.
+ return true;
+ }
+
+ if (string.IsNullOrEmpty(input))
+ {
+ // The profile contains a value, and the device doesn't.
+ return false;
+ }
+
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 +168,8 @@ namespace Emby.Dlna
}
}
- public DeviceProfile GetProfile(IHeaderDictionary headers)
+ ///
+ public DeviceProfile? GetProfile(IHeaderDictionary headers)
{
if (headers == null)
{
@@ -228,15 +177,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 +233,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 +273,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 +303,7 @@ namespace Emby.Dlna
}
}
+ ///
public IEnumerable GetProfileInfos()
{
return GetProfileInfosInternal().Select(i => i.Info);
@@ -357,17 +311,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()
@@ -387,15 +338,20 @@ namespace Emby.Dlna
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 length = stream.Length;
var fileInfo = _fileSystem.GetFileInfo(path);
- if (!fileInfo.Exists || fileInfo.Length != stream.Length)
+ if (!fileInfo.Exists || fileInfo.Length != length)
{
Directory.CreateDirectory(systemProfilesPath);
- using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ var fileOptions = AsyncFile.WriteOptions;
+ fileOptions.Mode = FileMode.Create;
+ fileOptions.PreallocationSize = length;
+ using (var fileStream = new FileStream(path, fileOptions))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
@@ -407,6 +363,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 +381,7 @@ namespace Emby.Dlna
}
}
+ ///
public void CreateProfile(DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@@ -439,7 +397,8 @@ namespace Emby.Dlna
SaveProfile(profile, path, DeviceProfileType.User);
}
- public void UpdateProfile(DeviceProfile profile)
+ ///
+ public void UpdateProfile(string profileId, DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@@ -453,7 +412,7 @@ namespace Emby.Dlna
throw new ArgumentException("Profile is missing Name");
}
- var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
+ var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
var path = Path.Combine(UserProfilesPath, newFilename);
@@ -497,9 +456,11 @@ namespace Emby.Dlna
var json = JsonSerializer.Serialize(profile, _jsonOptions);
- return JsonSerializer.Deserialize(json, _jsonOptions);
+ // 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,26 +470,37 @@ namespace Emby.Dlna
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
}
- public ImageStream GetIcon(string filename)
+ ///
+ public ImageStream? GetIcon(string filename)
{
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? ImageFormat.Png
: ImageFormat.Jpg;
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
-
- return new ImageStream
+ var stream = _assembly.GetManifestResourceStream(resource);
+ if (stream == null)
{
- Format = format,
- Stream = _assembly.GetManifestResourceStream(resource)
+ return null;
+ }
+
+ return new ImageStream(stream)
+ {
+ Format = format
};
}
private class InternalProfileInfo
{
- internal DeviceProfileInfo Info { get; set; }
+ internal InternalProfileInfo(DeviceProfileInfo info, string path)
+ {
+ Info = info;
+ Path = path;
+ }
- internal string Path { get; set; }
+ internal DeviceProfileInfo Info { get; }
+
+ internal string Path { get; }
}
}
@@ -553,7 +525,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 8b057a0950..7fdbd44f00 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -17,24 +17,18 @@
- net5.0
+ net6.0
false
true
- true
-
-
- ../jellyfin.ruleset
-
-
@@ -78,7 +72,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 ff81e83b5e..d17e238715 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;
@@ -9,6 +11,7 @@ using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
@@ -23,8 +26,6 @@ namespace Emby.Dlna.Eventing
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
@@ -49,11 +50,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)
@@ -84,9 +81,7 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header))
{
// Starts with SECOND-
- header = header.Split('-')[^1];
-
- if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return val;
}
@@ -101,23 +96,15 @@ 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;
+ response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
return response;
}
@@ -174,7 +161,7 @@ namespace Emby.Dlna.Eventing
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
- options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
+ options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
try
{
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 82490ec310..08f639d932 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -5,7 +7,6 @@ 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;
@@ -26,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
{
@@ -53,7 +52,6 @@ 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;
@@ -126,9 +124,10 @@ namespace Emby.Dlna.Main
config);
Current = this;
- _netConfig = config.GetConfiguration("network");
- _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
- if (_disabled)
+ var netConfig = config.GetConfiguration(NetworkConfigurationStore.StoreKey);
+ _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
+
+ if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
@@ -202,8 +201,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)
{
@@ -219,16 +218,14 @@ namespace Emby.Dlna.Main
}
}
- private void LogMessage(string msg)
- {
- _logger.LogDebug(msg);
- }
-
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
{
try
{
- ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+ if (communicationsServer != null)
+ {
+ ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+ }
}
catch (Exception ex)
{
@@ -263,9 +260,13 @@ namespace Emby.Dlna.Main
try
{
- _publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
+ _publisher = new SsdpDevicePublisher(
+ _communicationsServer,
+ MediaBrowser.Common.System.OperatingSystem.Name,
+ Environment.OSVersion.VersionString,
+ _config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
- LogFunction = LogMessage,
+ LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
SupportPnpRootDevice = false
};
@@ -310,12 +311,9 @@ namespace Emby.Dlna.Main
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
- _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
+ _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
- var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
- // DLNA will only work over http, so we must reset to http:// : {port}
- uri.Scheme = "http://";
- uri.Port = _netConfig.HttpServerPortNumber;
+ var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri);
var device = new SsdpRootDevice
{
@@ -401,7 +399,6 @@ namespace Emby.Dlna.Main
_imageProcessor,
_deviceDiscovery,
_httpClientFactory,
- _config,
_userDataManager,
_localization,
_mediaSourceManager,
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/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 938ce5fbf0..0b22880003 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;
@@ -18,8 +20,6 @@ namespace Emby.Dlna.PlayTo
{
public class Device : IDisposable
{
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
@@ -219,7 +219,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;
@@ -235,7 +235,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;
@@ -253,7 +259,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;
@@ -270,7 +276,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);
}
@@ -278,7 +290,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;
@@ -291,7 +303,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);
@@ -305,7 +323,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;
@@ -325,14 +343,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
{
@@ -343,6 +368,42 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true);
}
+ /*
+ * 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))
@@ -378,6 +439,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);
@@ -388,7 +453,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;
@@ -396,7 +461,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);
@@ -406,7 +477,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;
@@ -414,7 +485,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;
@@ -528,7 +605,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;
@@ -561,7 +638,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- Volume = int.Parse(volumeValue, UsCulture);
+ Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
if (Volume > 0)
{
@@ -578,7 +655,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;
@@ -665,6 +742,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,
@@ -733,6 +814,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,
@@ -754,7 +840,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(duration)
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
- Duration = TimeSpan.Parse(duration, UsCulture);
+ Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
}
else
{
@@ -766,7 +852,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
- Position = TimeSpan.Parse(position, UsCulture);
+ Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture);
}
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
@@ -914,6 +1000,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;
@@ -942,6 +1032,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;
@@ -973,6 +1067,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();
@@ -990,7 +1088,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)
};
@@ -1094,8 +1192,8 @@ namespace Emby.Dlna.PlayTo
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
- var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
- var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
+ var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
return new DeviceIcon
{
@@ -1160,10 +1258,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
- {
- MediaInfo = mediaInfo
- });
+ PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
}
private void OnPlaybackProgress(UBaseObject mediaInfo)
@@ -1173,27 +1268,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 311fae2400..f25d8017ef 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;
@@ -28,8 +30,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlayToController : ISessionController, IDisposable
{
- private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-
private readonly SessionInfo _session;
private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
@@ -102,6 +102,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
@@ -132,7 +148,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{
- if (_disposed)
+ if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
return;
}
@@ -156,6 +172,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)
{
@@ -425,6 +450,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;
}
@@ -499,8 +529,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,
@@ -514,8 +544,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(),
@@ -623,6 +653,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))
{
@@ -681,7 +714,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
{
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return SetAudioStreamIndex(val);
}
@@ -693,7 +726,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index))
{
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return SetSubtitleStreamIndex(val);
}
@@ -705,7 +738,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
{
- if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
+ if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{
return _device.SetVolume(volume, cancellationToken);
}
@@ -736,6 +769,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);
@@ -761,6 +798,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);
@@ -777,7 +818,7 @@ namespace Emby.Dlna.PlayTo
var currentWait = 0;
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
{
- await Task.Delay(Interval).ConfigureAwait(false);
+ await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
currentWait += Interval;
}
@@ -896,16 +937,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;
}
}
}
@@ -943,11 +984,7 @@ namespace Emby.Dlna.PlayTo
request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
-
- // Be careful, IsDirectStream==true by default (Static != false or not in query).
- // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
- request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
-
+ 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 a6793a7081..294bda5b6a 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -9,7 +11,6 @@ using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
@@ -33,7 +34,6 @@ namespace Emby.Dlna.PlayTo
private readonly IServerApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerConfigurationManager _config;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
@@ -45,7 +45,7 @@ namespace Emby.Dlna.PlayTo
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
- public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
+ public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{
_logger = logger;
_sessionManager = sessionManager;
@@ -56,7 +56,6 @@ namespace Emby.Dlna.PlayTo
_imageProcessor = imageProcessor;
_deviceDiscovery = deviceDiscovery;
_httpClientFactory = httpClientFactory;
- _config = config;
_userDataManager = userDataManager;
_localization = localization;
_mediaSourceManager = mediaSourceManager;
@@ -171,19 +170,26 @@ namespace Emby.Dlna.PlayTo
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
- var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
+ var sessionInfo = await _sessionManager
+ .LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
+ .ConfigureAwait(false);
var controller = sessionInfo.SessionControllers.OfType().FirstOrDefault();
if (controller == null)
{
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
+ if (device == null)
+ {
+ _logger.LogError("Ignoring device as xml response is invalid.");
+ return;
+ }
string deviceName = device.Properties.Name;
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
- string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
+ string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
controller = new PlayToController(
sessionInfo,
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 e28840a896..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;
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index 557bc69a73..cade7b4c2c 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -1,8 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Globalization;
-using System.IO;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
@@ -19,8 +20,6 @@ namespace Emby.Dlna.PlayTo
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
private const string FriendlyName = "Jellyfin";
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly IHttpClientFactory _httpClientFactory;
public SsdpHttpClient(IHttpClientFactory httpClientFactory)
@@ -44,11 +43,13 @@ namespace Emby.Dlna.PlayTo
header,
cancellationToken)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var reader = new StreamReader(stream, Encoding.UTF8);
- return XDocument.Parse(
- await reader.ReadToEndAsync().ConfigureAwait(false),
- LoadOptions.PreserveWhitespace);
+ return await XDocument.LoadAsync(
+ stream,
+ LoadOptions.None,
+ cancellationToken).ConfigureAwait(false);
}
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@@ -77,14 +78,15 @@ namespace Emby.Dlna.PlayTo
{
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
options.Headers.UserAgent.ParseAdd(USERAGENT);
- options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
- options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
+ options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
+ options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
- options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
+ options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
}
public async Task GetDataAsync(string url, CancellationToken cancellationToken)
@@ -93,11 +95,19 @@ 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);
+ response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var reader = new StreamReader(stream, Encoding.UTF8);
- return XDocument.Parse(
- await reader.ReadToEndAsync().ConfigureAwait(false),
- LoadOptions.PreserveWhitespace);
+ try
+ {
+ return await XDocument.LoadAsync(
+ stream,
+ LoadOptions.None,
+ 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 0865968ad1..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,9 +68,9 @@ 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
};
}
@@ -91,8 +89,8 @@ namespace Emby.Dlna.PlayTo
return new StateVariable
{
- Name = container.GetValue(UPnpNamespaces.Svc + "name"),
- DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
+ Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
+ DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
AllowedValues = allowedValues
};
}
@@ -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/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/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 09525aae4e..80a45f2b25 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -15,7 +15,6 @@ namespace Emby.Dlna.Server
{
private readonly DeviceProfile _profile;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly string _serverUdn;
private readonly string _serverAddress;
private readonly string _serverName;
@@ -193,10 +192,10 @@ namespace Emby.Dlna.Server
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
+ .Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
.Append("");
builder.Append("")
- .Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
+ .Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
.Append("");
builder.Append("")
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 8d2486fee6..780aad9c18 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,7 +47,7 @@ namespace Emby.Dlna.Service
private async Task ProcessControlRequestInternalAsync(ControlRequest request)
{
- ControlRequestInfo requestInfo = null;
+ ControlRequestInfo? requestInfo = null;
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
{
@@ -64,7 +64,7 @@ namespace Emby.Dlna.Service
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
}
- Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
+ Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
var settings = new XmlWriterSettings
{
@@ -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);
@@ -210,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/BaseService.cs b/Emby.Dlna/Service/BaseService.cs
index a97c4d63a6..68fd987585 100644
--- a/Emby.Dlna/Service/BaseService.cs
+++ b/Emby.Dlna/Service/BaseService.cs
@@ -23,14 +23,14 @@ namespace Emby.Dlna.Service
return EventManager.CancelEventSubscription(subscriptionId);
}
- public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string timeoutString, string callbackUrl)
+ public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
{
- return EventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callbackUrl);
+ return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl);
}
- public EventSubscriptionResponse CreateEventSubscription(string notificationType, string timeoutString, string callbackUrl)
+ public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
{
- return EventManager.CreateEventSubscription(notificationType, timeoutString, callbackUrl);
+ return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl);
}
}
}
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 7d479a5c65..149e4b5d91 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -6,11 +6,9 @@
- net5.0
+ net6.0
false
true
- true
- enable
@@ -25,14 +23,9 @@
-
-
- ../jellyfin.ruleset
-
-
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 8a2301d2d6..3f75e4fc79 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;
@@ -25,7 +26,7 @@ namespace Emby.Drawing
public sealed class ImageProcessor : IImageProcessor, IDisposable
{
// Increment this when there's a change requiring caches to be invalidated
- private const string Version = "3";
+ private const char Version = '3';
private static readonly HashSet _transparentImageTypes
= new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
@@ -101,7 +102,7 @@ namespace Emby.Drawing
{
var file = await ProcessImage(options).ConfigureAwait(false);
- using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
+ using (var fileStream = AsyncFile.OpenRead(file.Item1))
{
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
}
@@ -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/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs
index 8b47dd12e4..2b610ec796 100644
--- a/Emby.Naming/Audio/AudioFileParser.cs
+++ b/Emby.Naming/Audio/AudioFileParser.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Audio
{
@@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
/// 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/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs
index adf403ab6d..acd8905af6 100644
--- a/Emby.Naming/AudioBook/AudioBookInfo.cs
+++ b/Emby.Naming/AudioBook/AudioBookInfo.cs
@@ -15,13 +15,13 @@ namespace Emby.Naming.AudioBook
/// List of files composing the actual audiobook.
/// List of extra files.
/// Alternative version of files.
- public AudioBookInfo(string name, int? year, List? files, List? extras, List? alternateVersions)
+ public AudioBookInfo(string name, int? year, IReadOnlyList files, IReadOnlyList extras, IReadOnlyList alternateVersions)
{
Name = name;
Year = year;
- Files = files ?? new List();
- Extras = extras ?? new List();
- AlternateVersions = alternateVersions ?? new List();
+ Files = files;
+ Extras = extras;
+ AlternateVersions = alternateVersions;
}
///
@@ -39,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 e9ea9b7a5d..dd8a05bb3e 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
public class AudioBookListResolver
{
private readonly NamingOptions _options;
+ private readonly AudioBookResolver _audioBookResolver;
///
/// Initializes a new instance of the class.
@@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
public AudioBookListResolver(NamingOptions options)
{
_options = options;
+ _audioBookResolver = new AudioBookResolver(_options);
}
///
@@ -31,21 +33,19 @@ namespace Emby.Naming.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))
+ .Select(i => _audioBookResolver.Resolve(i.FullName))
.OfType()
.ToList();
- var stackResult = new StackResolver(_options)
- .ResolveAudioBooks(audiobookFileInfos);
+ var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
var stackFiles = stack.Files
- .Select(i => audioBookResolver.Resolve(i))
+ .Select(i => _audioBookResolver.Resolve(i))
.OfType()
.ToList();
@@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook
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(" ", ".");
+ var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.');
foreach (var group in groupedBy)
{
@@ -87,7 +87,7 @@ namespace Emby.Naming.AudioBook
foreach (var audioFile in group)
{
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
- if (name.Equals("audiobook") ||
+ if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
{
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index f6ad3601d7..183b6c3b11 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.AudioBook
{
@@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
var extension = Path.GetExtension(path);
// Check supported extensions
- if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return null;
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 035d1b2280..aa62a47f17 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -1,4 +1,7 @@
+#pragma warning disable CA1819
+
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
@@ -122,11 +125,11 @@ namespace Emby.Naming.Common
token: "DSR")
};
- VideoFileStackingExpressions = new[]
+ VideoFileStackingRules = new[]
{
- "(?.*?)(?[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?.*?)(?\\.[^.]+)$",
- "(?.*?)(?[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?.*?)(?\\.[^.]+)$",
- "(?.*?)(?[ ._-]*[a-d])(?.*?)(?\\.[^.]+)$"
+ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
+ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[a-d])[\)\]]?(?:\.[^.]+)?$", false),
+ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?[a-d])(?:\.[^.]+)?$", false)
};
CleanDateTimes = new[]
@@ -137,8 +140,11 @@ 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|blu-ray|x264|x265|h264|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
- @"(\[.*\])"
+ @"^\s*(?.+?)[ _\,\.\(\)\[\]\-](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|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+ @"^(?.+?)(\[.*\])",
+ @"^\s*(?.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
+ @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?.+)",
+ @"^\s*(?.+?)\s+-\s+[0-9]+\s*$"
};
SubtitleFileExtensions = new[]
@@ -250,6 +256,8 @@ namespace Emby.Naming.Common
},
//
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
+ //
+ new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?[0-9]{4})[\\.-](?[0-9]{2})[\\.-](?[0-9]{2})", true)
{
DateTimeFormats = new[]
@@ -277,12 +285,18 @@ 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
},
- // Case Closed (1996-2007)/Case Closed - 317.mkv
+ // 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
@@ -299,11 +313,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 +376,20 @@ namespace Emby.Naming.Common
IsOptimistic = true,
IsNamed = true
},
- // "Episode 16", "Episode 16 - Title"
- new EpisodeExpression(@".*[\\\/][^\\\/]* (?[0-9]{1,3})(-(?[0-9]{2,3}))*[^\\\/]*$")
+
+ // Series and season only expression
+ // "the show/season 1", "the show/s01"
+ new EpisodeExpression(@"(.*(\\|\/))*(?.+)\/[Ss](eason)?[\. _\-]*(?[0-9]+)")
{
- IsOptimistic = true,
IsNamed = true
- }
+ },
+
+ // Series and season only expression
+ // "the show S01", "the show season 1"
+ new EpisodeExpression(@"(.*(\\|\/))*(?.+)[\. _\-]+[sS](eason)?[\. _\-]*(?[0-9]+)")
+ {
+ IsNamed = true
+ },
};
EpisodeWithoutSeasonExpressions = new[]
@@ -381,6 +404,12 @@ namespace Emby.Naming.Common
VideoExtraRules = new[]
{
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.DirectoryName,
+ "trailers",
+ MediaType.Video),
+
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Filename,
@@ -447,6 +476,12 @@ namespace Emby.Naming.Common
"theme",
MediaType.Audio),
+ new ExtraRule(
+ ExtraType.ThemeSong,
+ ExtraRuleType.DirectoryName,
+ "theme-music",
+ MediaType.Audio),
+
new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
@@ -477,6 +512,12 @@ namespace Emby.Naming.Common
"-deleted",
MediaType.Video),
+ new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.Suffix,
+ "-deletedscene",
+ MediaType.Video),
+
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
@@ -535,7 +576,7 @@ namespace Emby.Naming.Common
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
"extras",
- MediaType.Video),
+ MediaType.Video)
};
Format3DRules = new[]
@@ -587,7 +628,7 @@ namespace Emby.Naming.Common
AudioBookNamesExpressions = new[]
{
// Detect year usually in brackets after name Batman (2020)
- @"^(?.+?)\s*\(\s*(?\d{4})\s*\)\s*$",
+ @"^(?.+?)\s*\(\s*(?[0-9]{4})\s*\)\s*$",
@"^\s*(?[^ ].*?)\s*$"
};
@@ -647,9 +688,29 @@ namespace Emby.Naming.Common
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
+ AllExtrasTypesFolderNames = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["trailers"] = ExtraType.Trailer,
+ ["theme-music"] = ExtraType.ThemeSong,
+ ["backdrops"] = ExtraType.ThemeVideo,
+ ["extras"] = ExtraType.Unknown,
+ ["behind the scenes"] = ExtraType.BehindTheScenes,
+ ["deleted scenes"] = ExtraType.DeletedScene,
+ ["interviews"] = ExtraType.Interview,
+ ["scenes"] = ExtraType.Scene,
+ ["samples"] = ExtraType.Sample,
+ ["shorts"] = ExtraType.Clip,
+ ["featurettes"] = ExtraType.Clip
+ };
+
Compile();
}
+ ///
+ /// Gets or sets the folder name to extra types mapping.
+ ///
+ public Dictionary AllExtrasTypesFolderNames { get; set; }
+
///
/// Gets or sets list of audio file extensions.
///
@@ -731,9 +792,9 @@ namespace Emby.Naming.Common
public Format3DRule[] Format3DRules { get; set; }
///
- /// Gets or sets list of raw video file-stacking expressions strings.
+ /// Gets the file stacking rules.
///
- public string[] VideoFileStackingExpressions { get; set; }
+ public FileStackRule[] VideoFileStackingRules { get; }
///
/// Gets or sets list of raw clean DateTimes regular expressions strings.
@@ -755,11 +816,6 @@ namespace Emby.Naming.Common
///
public ExtraRule[] VideoExtraRules { get; set; }
- ///
- /// Gets list of video file-stack regular expressions.
- ///
- public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty();
-
///
/// Gets list of clean datetime regular expressions.
///
@@ -785,7 +841,6 @@ namespace Emby.Naming.Common
///
public void Compile()
{
- VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 24c15759d4..2bf8eacb1b 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -1,4 +1,4 @@
-
+
@@ -6,15 +6,13 @@
- net5.0
+ net6.0
false
true
- true
true
true
true
snupkg
- enable
@@ -23,35 +21,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/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs
index a19340ef69..5809c512a8 100644
--- a/Emby.Naming/Subtitles/SubtitleParser.cs
+++ b/Emby.Naming/Subtitles/SubtitleParser.cs
@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Subtitles
{
@@ -34,7 +35,7 @@ namespace Emby.Naming.Subtitles
}
var extension = Path.GetExtension(path);
- if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return null;
}
@@ -42,11 +43,11 @@ namespace Emby.Naming.Subtitles
var flags = GetFlags(path);
var info = new SubtitleInfo(
path,
- _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
- _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
+ _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
+ _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
- var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
- && !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
+ var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
+ && !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
.ToList();
// Should have a name, language and file extension
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs
index f7df587864..6cebc40c27 100644
--- a/Emby.Naming/TV/EpisodeResolver.cs
+++ b/Emby.Naming/TV/EpisodeResolver.cs
@@ -1,8 +1,8 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Extensions;
namespace Emby.Naming.TV
{
@@ -16,7 +16,7 @@ namespace Emby.Naming.TV
///
/// Initializes a new instance of the class.
///
- /// object containing VideoFileExtensions and passed to , , and .
+ /// object containing VideoFileExtensions and passed to , and .
public EpisodeResolver(NamingOptions options)
{
_options = options;
@@ -48,7 +48,7 @@ namespace Emby.Naming.TV
{
var extension = Path.GetExtension(path);
// Check supported extensions
- if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's not supported. Check stub extensions
if (!StubResolver.TryResolveFile(path, _options, out stubType))
@@ -62,12 +62,16 @@ 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);
+ if (!parsingResult.Success && !isStub)
+ {
+ return null;
+ }
+
return new EpisodeInfo(path)
{
Container = container,
diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index d11c7c99e8..6236f86c43 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -60,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)
{
diff --git a/Emby.Naming/TV/SeriesInfo.cs b/Emby.Naming/TV/SeriesInfo.cs
new file mode 100644
index 0000000000..5d6cb4bd37
--- /dev/null
+++ b/Emby.Naming/TV/SeriesInfo.cs
@@ -0,0 +1,29 @@
+namespace Emby.Naming.TV
+{
+ ///
+ /// Holder object for Series information.
+ ///
+ public class SeriesInfo
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Path to the file.
+ public SeriesInfo(string path)
+ {
+ Path = path;
+ }
+
+ ///
+ /// Gets or sets the path.
+ ///
+ /// The path.
+ public string Path { get; set; }
+
+ ///
+ /// Gets or sets the name of the series.
+ ///
+ /// The name of the series.
+ public string? Name { get; set; }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesPathParser.cs b/Emby.Naming/TV/SeriesPathParser.cs
new file mode 100644
index 0000000000..4dfbb36a37
--- /dev/null
+++ b/Emby.Naming/TV/SeriesPathParser.cs
@@ -0,0 +1,60 @@
+using Emby.Naming.Common;
+
+namespace Emby.Naming.TV
+{
+ ///
+ /// Used to parse information about series from paths containing more information that only the series name.
+ /// Uses the same regular expressions as the EpisodePathParser but have different success criteria.
+ ///
+ public static class SeriesPathParser
+ {
+ ///
+ /// Parses information about series from path.
+ ///
+ /// object containing EpisodeExpressions and MultipleEpisodeExpressions.
+ /// Path.
+ /// Returns object.
+ public static SeriesPathParserResult Parse(NamingOptions options, string path)
+ {
+ SeriesPathParserResult? result = null;
+
+ foreach (var expression in options.EpisodeExpressions)
+ {
+ var currentResult = Parse(path, expression);
+ if (currentResult.Success)
+ {
+ result = currentResult;
+ break;
+ }
+ }
+
+ if (result != null)
+ {
+ if (!string.IsNullOrEmpty(result.SeriesName))
+ {
+ result.SeriesName = result.SeriesName.Trim(' ', '_', '.', '-');
+ }
+ }
+
+ return result ?? new SeriesPathParserResult();
+ }
+
+ private static SeriesPathParserResult Parse(string name, EpisodeExpression expression)
+ {
+ var result = new SeriesPathParserResult();
+
+ var match = expression.Regex.Match(name);
+
+ if (match.Success && match.Groups.Count >= 3)
+ {
+ if (expression.IsNamed)
+ {
+ result.SeriesName = match.Groups["seriesname"].Value;
+ result.Success = !string.IsNullOrEmpty(result.SeriesName) && !string.IsNullOrEmpty(match.Groups["seasonnumber"]?.Value);
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesPathParserResult.cs b/Emby.Naming/TV/SeriesPathParserResult.cs
new file mode 100644
index 0000000000..44cd2fdfa1
--- /dev/null
+++ b/Emby.Naming/TV/SeriesPathParserResult.cs
@@ -0,0 +1,19 @@
+namespace Emby.Naming.TV
+{
+ ///
+ /// Holder object for result.
+ ///
+ public class SeriesPathParserResult
+ {
+ ///
+ /// 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; }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs
new file mode 100644
index 0000000000..156a03c9ed
--- /dev/null
+++ b/Emby.Naming/TV/SeriesResolver.cs
@@ -0,0 +1,49 @@
+using System.IO;
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.TV
+{
+ ///
+ /// Used to resolve information about series from path.
+ ///
+ public static class SeriesResolver
+ {
+ ///
+ /// Regex that matches strings of at least 2 characters separated by a dot or underscore.
+ /// Used for removing separators between words, i.e turns "The_show" into "The show" while
+ /// preserving namings like "S.H.O.W".
+ ///
+ private static readonly Regex _seriesNameRegex = new Regex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))");
+
+ ///
+ /// Resolve information about series from path.
+ ///
+ /// object passed to .
+ /// Path to series.
+ /// SeriesInfo.
+ public static SeriesInfo Resolve(NamingOptions options, string path)
+ {
+ string seriesName = Path.GetFileName(path);
+
+ SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
+ if (result.Success)
+ {
+ if (!string.IsNullOrEmpty(result.SeriesName))
+ {
+ seriesName = result.SeriesName;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(seriesName))
+ {
+ seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
+ }
+
+ return new SeriesInfo(path)
+ {
+ Name = seriesName
+ };
+ }
+ }
+}
diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs
index 09a0cd1893..a336f8fbd1 100644
--- a/Emby.Naming/Video/CleanStringParser.cs
+++ b/Emby.Naming/Video/CleanStringParser.cs
@@ -1,5 +1,5 @@
-using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Emby.Naming.Video
@@ -16,28 +16,35 @@ namespace Emby.Naming.Video
/// List of regex to parse name and year from.
/// Parsing result string.
/// True if parsing was successful.
- public static bool TryClean(string name, IReadOnlyList expressions, out ReadOnlySpan newName)
+ public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList expressions, out string newName)
{
- var len = expressions.Count;
- for (int i = 0; i < len; i++)
+ if (string.IsNullOrEmpty(name))
+ {
+ newName = string.Empty;
+ return false;
+ }
+
+ // Iteratively apply the regexps to clean the string.
+ bool cleaned = false;
+ for (int i = 0; i < expressions.Count; i++)
{
if (TryClean(name, expressions[i], out newName))
{
- return true;
+ cleaned = true;
+ name = newName;
}
}
- newName = ReadOnlySpan.Empty;
- return false;
+ newName = cleaned ? name : string.Empty;
+ return cleaned;
}
- private static bool TryClean(string name, Regex expression, out ReadOnlySpan newName)
+ private static bool TryClean(string name, Regex expression, out string newName)
{
var match = expression.Match(name);
- int index = match.Index;
- if (match.Success && index != 0)
+ if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
{
- newName = name.AsSpan().Slice(0, match.Index);
+ newName = cleaned.Value;
return true;
}
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index 1d3b36a1ad..fbdca859f7 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@@ -10,93 +11,141 @@ namespace Emby.Naming.Video
///
/// Resolve if file is extra for video.
///
- public class ExtraResolver
+ public static 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;
- }
+ private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
///
/// Attempts to resolve if file is extra.
///
/// Path to file.
+ /// The naming options.
/// 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)
+ public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
{
var result = new ExtraResult();
- if (rule.MediaType == MediaType.Audio)
+ for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
{
- if (!AudioFileParser.IsAudioFile(path, _options))
+ var rule = namingOptions.VideoExtraRules[i];
+ if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
+ || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
+ {
+ continue;
+ }
+
+ var pathSpan = path.AsSpan();
+ if (rule.RuleType == ExtraRuleType.Filename)
+ {
+ var filename = Path.GetFileNameWithoutExtension(pathSpan);
+
+ if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
+ }
+ else if (rule.RuleType == ExtraRuleType.Suffix)
+ {
+ // Trim the digits from the end of the filename so we can recognize things like -trailer2
+ var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
+
+ if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
+ }
+ else if (rule.RuleType == ExtraRuleType.Regex)
+ {
+ var filename = Path.GetFileName(path);
+
+ var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ if (isMatch)
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
+ }
+ else if (rule.RuleType == ExtraRuleType.DirectoryName)
+ {
+ var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
+ if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
+ }
+
+ if (result.ExtraType != null)
{
return result;
}
}
- else if (rule.MediaType == MediaType.Video)
- {
- if (!new VideoResolver(_options).IsVideoFile(path))
- {
- return result;
- }
- }
-
- if (rule.RuleType == ExtraRuleType.Filename)
- {
- var filename = Path.GetFileNameWithoutExtension(path);
-
- if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
- else if (rule.RuleType == ExtraRuleType.Suffix)
- {
- var filename = Path.GetFileNameWithoutExtension(path);
-
- if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
- else if (rule.RuleType == ExtraRuleType.Regex)
- {
- var filename = Path.GetFileName(path);
-
- var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
-
- if (regex.IsMatch(filename))
- {
- 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))
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
return result;
}
+
+ ///
+ /// Finds extras matching the video info.
+ ///
+ /// The list of file video infos.
+ /// The video to compare against.
+ /// The video flag delimiters.
+ /// A list of video extras for [videoInfo].
+ public static IReadOnlyList GetExtras(IReadOnlyList files, VideoFileInfo videoInfo, ReadOnlySpan videoFlagDelimiters)
+ {
+ var parentDir = videoInfo.IsDirectory ? videoInfo.Path : Path.GetDirectoryName(videoInfo.Path.AsSpan());
+
+ var trimmedFileNameWithoutExtension = TrimFilenameDelimiters(videoInfo.FileNameWithoutExtension, videoFlagDelimiters);
+ var trimmedVideoInfoName = TrimFilenameDelimiters(videoInfo.Name, videoFlagDelimiters);
+
+ var result = new List();
+ for (var pos = files.Count - 1; pos >= 0; pos--)
+ {
+ var current = files[pos];
+ // ignore non-extras and multi-file (can this happen?)
+ if (current.ExtraType == null || current.Files.Count > 1)
+ {
+ continue;
+ }
+
+ var currentFile = current.Files[0];
+ var trimmedCurrentFileName = TrimFilenameDelimiters(currentFile.Name, videoFlagDelimiters);
+
+ // first check filenames
+ bool isValid = StartsWith(trimmedCurrentFileName, trimmedFileNameWithoutExtension)
+ || (StartsWith(trimmedCurrentFileName, trimmedVideoInfoName) && currentFile.Year == videoInfo.Year);
+
+ // then by directory
+ if (!isValid)
+ {
+ // When the extra rule type is DirectoryName we must go one level higher to get the "real" dir name
+ var currentParentDir = currentFile.ExtraRule?.RuleType == ExtraRuleType.DirectoryName
+ ? Path.GetDirectoryName(Path.GetDirectoryName(currentFile.Path.AsSpan()))
+ : Path.GetDirectoryName(currentFile.Path.AsSpan());
+
+ isValid = !currentParentDir.IsEmpty && !parentDir.IsEmpty && currentParentDir.Equals(parentDir, StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (isValid)
+ {
+ result.Add(currentFile);
+ }
+ }
+
+ return result.OrderBy(r => r.Path).ToArray();
+ }
+
+ 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)
+ {
+ return !baseName.IsEmpty && fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase);
+ }
}
}
diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs
index 6519db57c3..4902e6728e 100644
--- a/Emby.Naming/Video/FileStack.cs
+++ b/Emby.Naming/Video/FileStack.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
@@ -12,25 +12,30 @@ namespace Emby.Naming.Video
///
/// Initializes a new instance of the class.
///
- public FileStack()
+ /// The stack name.
+ /// Whether the stack files are directories.
+ /// The stack files.
+ public FileStack(string name, bool isDirectory, IReadOnlyList files)
{
- Files = new List();
+ Name = name;
+ IsDirectoryStack = isDirectory;
+ Files = files;
}
///
- /// Gets or sets name of file stack.
+ /// Gets the name of file stack.
///
- public string Name { get; set; } = string.Empty;
+ public string Name { get; }
///
- /// Gets or sets list of paths in stack.
+ /// Gets the list of paths in stack.
///
- public List Files { get; set; }
+ public IReadOnlyList Files { get; }
///
- /// Gets or sets a value indicating whether stack is directory stack.
+ /// Gets a value indicating whether stack is directory stack.
///
- public bool IsDirectoryStack { get; set; }
+ public bool IsDirectoryStack { get; }
///
/// Helper function to determine if path is in the stack.
@@ -40,12 +45,12 @@ namespace Emby.Naming.Video
/// True if file is in the stack.
public bool ContainsFile(string file, bool isDirectory)
{
- if (IsDirectoryStack == isDirectory)
+ if (string.IsNullOrEmpty(file))
{
- return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
+ return false;
}
- return false;
+ return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Naming/Video/FileStackRule.cs b/Emby.Naming/Video/FileStackRule.cs
new file mode 100644
index 0000000000..76b487f428
--- /dev/null
+++ b/Emby.Naming/Video/FileStackRule.cs
@@ -0,0 +1,48 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+
+namespace Emby.Naming.Video;
+
+///
+/// Regex based rule for file stacking (eg. disc1, disc2).
+///
+public class FileStackRule
+{
+ private readonly Regex _tokenRegex;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Token.
+ /// Whether the file stack rule uses numerical or alphabetical numbering.
+ public FileStackRule(string token, bool isNumerical)
+ {
+ _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+ IsNumerical = isNumerical;
+ }
+
+ ///
+ /// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
+ ///
+ public bool IsNumerical { get; }
+
+ ///
+ /// Match the input against the rule regex.
+ ///
+ /// The input.
+ /// The part type and number or null.
+ /// A value indicating whether the input matched the rule.
+ public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
+ {
+ result = null;
+ var match = _tokenRegex.Match(input);
+ if (!match.Success)
+ {
+ return false;
+ }
+
+ var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown";
+ result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
+ return true;
+ }
+}
diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs
deleted file mode 100644
index 439de18138..0000000000
--- a/Emby.Naming/Video/FlagParser.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.IO;
-using Emby.Naming.Common;
-
-namespace Emby.Naming.Video
-{
- ///
- /// Parses list of flags from filename based on delimiters.
- ///
- public class FlagParser
- {
- private readonly NamingOptions _options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// object containing VideoFlagDelimiters.
- public FlagParser(NamingOptions options)
- {
- _options = options;
- }
-
- ///
- /// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
- ///
- /// Path to file.
- /// List of found flags.
- public string[] GetFlags(string path)
- {
- return GetFlags(path, _options.VideoFlagDelimiters);
- }
-
- ///
- /// Parses flags from filename based on delimiters.
- ///
- /// Path to file.
- /// Delimiters used to extract flags.
- /// List of found flags.
- public string[] GetFlags(string path, char[] delimiters)
- {
- if (string.IsNullOrEmpty(path))
- {
- return Array.Empty();
- }
-
- // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
-
- var file = Path.GetFileName(path);
-
- return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
- }
- }
-}
diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs
index 4fd5d78ba7..0890899894 100644
--- a/Emby.Naming/Video/Format3DParser.cs
+++ b/Emby.Naming/Video/Format3DParser.cs
@@ -1,45 +1,37 @@
using System;
-using System.Linq;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
///
- /// Parste 3D format related flags.
+ /// Parse 3D format related flags.
///
- public class Format3DParser
+ public static class Format3DParser
{
- private readonly NamingOptions _options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// object containing VideoFlagDelimiters and passes options to .
- public Format3DParser(NamingOptions options)
- {
- _options = 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 Format3DResult Parse(string path)
+ public static Format3DResult Parse(ReadOnlySpan path, NamingOptions namingOptions)
{
- int oldLen = _options.VideoFlagDelimiters.Length;
- var delimiters = new char[oldLen + 1];
- _options.VideoFlagDelimiters.CopyTo(delimiters, 0);
+ int oldLen = namingOptions.VideoFlagDelimiters.Length;
+ Span delimiters = stackalloc char[oldLen + 1];
+ namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
delimiters[oldLen] = ' ';
- return Parse(new FlagParser(_options).GetFlags(path, delimiters));
+ 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)
{
@@ -47,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.PrecedingToken))
+ // 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);
- }
- }
- else
- {
- var foundPrefix = false;
- string? format = null;
-
- foreach (var flag in videoFlags)
- {
- if (foundPrefix)
- {
- result.Tokens.Add(rule.PrecedingToken);
-
- if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
- {
- format = flag;
- result.Tokens.Add(rule.Token);
- }
-
- break;
- }
-
- foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
+ index = path.Length - 1;
}
- result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
- result.Format3D = format;
+ var currentSlice = path[..index];
+ path = path[(index + 1)..];
+
+ if (!foundPrefix)
+ {
+ foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
+ continue;
+ }
+
+ is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
+
+ if (is3D)
+ {
+ format3D = rule.Token;
+ break;
+ }
}
- return result;
+ return is3D ? new Format3DResult(true, format3D) : _defaultResult;
}
}
}
diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs
index ac935f2030..aac959c133 100644
--- a/Emby.Naming/Video/Format3DResult.cs
+++ b/Emby.Naming/Video/Format3DResult.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace Emby.Naming.Video
{
///
@@ -10,27 +8,24 @@ namespace Emby.Naming.Video
///
/// Initializes a new instance of the class.
///
- public Format3DResult()
+ /// 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/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index 550c429614..8119a02674 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -2,7 +2,6 @@ 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;
@@ -12,37 +11,28 @@ namespace Emby.Naming.Video
///
/// Resolve from list of paths.
///
- public class StackResolver
+ public static 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.
+ /// The naming options.
/// Enumerable of directories.
- public IEnumerable ResolveDirectories(IEnumerable files)
+ public static IEnumerable ResolveDirectories(IEnumerable files, NamingOptions namingOptions)
{
- return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
+ return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
}
///
/// Resolves only files from paths.
///
/// List of paths.
+ /// The naming options.
/// Enumerable of files.
- public IEnumerable ResolveFiles(IEnumerable files)
+ public static IEnumerable ResolveFiles(IEnumerable files, NamingOptions namingOptions)
{
- return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
+ return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
}
///
@@ -50,7 +40,7 @@ namespace Emby.Naming.Video
///
/// List of paths.
/// Enumerable of directories.
- public IEnumerable ResolveAudioBooks(IEnumerable files)
+ public static IEnumerable ResolveAudioBooks(IEnumerable files)
{
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
@@ -60,19 +50,13 @@ namespace Emby.Naming.Video
{
foreach (var file in directory)
{
- var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
- stack.Files.Add(file.Path);
+ var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
yield return stack;
}
}
else
{
- var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
- foreach (var file in directory)
- {
- stack.Files.Add(file.Path);
- }
-
+ var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
yield return stack;
}
}
@@ -82,160 +66,91 @@ namespace Emby.Naming.Video
/// Resolves videos from paths.
///
/// List of paths.
+ /// The naming options.
/// Enumerable of videos.
- public IEnumerable Resolve(IEnumerable files)
+ public static IEnumerable Resolve(IEnumerable files, NamingOptions namingOptions)
{
- var resolver = new VideoResolver(_options);
+ var potentialFiles = files
+ .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
+ .OrderBy(i => i.FullName);
- var list = files
- .Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))
- .OrderBy(i => i.FullName)
- .ToList();
-
- var expressions = _options.VideoFileStackingRegexes;
-
- for (var i = 0; i < list.Count; i++)
+ var potentialStacks = new Dictionary();
+ foreach (var file in potentialFiles)
{
- var offset = 0;
-
- var file1 = list[i];
-
- var expressionIndex = 0;
- while (expressionIndex < expressions.Length)
+ var name = file.Name;
+ if (string.IsNullOrEmpty(name))
{
- var exp = expressions[expressionIndex];
- var stack = new FileStack();
+ name = Path.GetFileName(file.FullName);
+ }
- // (Title)(Volume)(Ignore)(Extension)
- var match1 = FindMatch(file1, exp, offset);
-
- if (match1.Success)
+ for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
+ {
+ var rule = namingOptions.VideoFileStackingRules[i];
+ if (!rule.Match(name, out var stackParsingResult))
{
- var title1 = match1.Groups["title"].Value;
- var volume1 = match1.Groups["volume"].Value;
- var ignore1 = match1.Groups["ignore"].Value;
- var extension1 = match1.Groups["extension"].Value;
+ continue;
+ }
- var j = i + 1;
- while (j < list.Count)
+ var stackName = stackParsingResult.Value.StackName;
+ var partNumber = stackParsingResult.Value.PartNumber;
+ var partType = stackParsingResult.Value.PartType;
+
+ if (!potentialStacks.TryGetValue(stackName, out var stackResult))
+ {
+ stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
+ potentialStacks[stackName] = stackResult;
+ }
+
+ if (stackResult.Parts.Count > 0)
+ {
+ if (stackResult.IsDirectory != file.IsDirectory
+ || !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
+ || stackResult.ContainsPart(partNumber))
{
- var file2 = list[j];
-
- if (file1.IsDirectory != file2.IsDirectory)
- {
- j++;
- continue;
- }
-
- // (Title)(Volume)(Ignore)(Extension)
- var match2 = FindMatch(file2, exp, offset);
-
- if (match2.Success)
- {
- var title2 = match2.Groups[1].Value;
- var volume2 = match2.Groups[2].Value;
- var ignore2 = match2.Groups[3].Value;
- var extension2 = match2.Groups[4].Value;
-
- if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
- {
- if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
- {
- if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
- && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
- {
- if (stack.Files.Count == 0)
- {
- stack.Name = title1 + ignore1;
- stack.IsDirectoryStack = file1.IsDirectory;
- stack.Files.Add(file1.FullName);
- }
-
- stack.Files.Add(file2.FullName);
- }
- else
- {
- // Sequel
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
- {
- // False positive, try again with offset
- offset = match1.Groups[3].Index;
- break;
- }
- else
- {
- // Extension mismatch
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else
- {
- // Title mismatch
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else
- {
- // No match 2, next expression
- offset = 0;
- expressionIndex++;
- break;
- }
-
- j++;
+ continue;
}
- if (j == list.Count)
+ if (rule.IsNumerical != stackResult.IsNumerical)
{
- expressionIndex = expressions.Length;
+ break;
}
}
- else
- {
- // No match 1
- offset = 0;
- expressionIndex++;
- }
- if (stack.Files.Count > 1)
- {
- yield return stack;
- i += stack.Files.Count - 1;
- break;
- }
+ stackResult.Parts.Add(partNumber, file);
+ break;
}
}
- }
- private static string GetRegexInput(FileSystemMetadata file)
- {
- // For directories, dummy up an extension otherwise the expressions will fail
- var input = !file.IsDirectory
- ? file.FullName
- : file.FullName + ".mkv";
-
- return Path.GetFileName(input);
- }
-
- private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
- {
- var regexInput = GetRegexInput(input);
-
- if (offset < 0 || offset >= regexInput.Length)
+ foreach (var (fileName, stack) in potentialStacks)
{
- return Match.Empty;
+ if (stack.Parts.Count < 2)
+ {
+ continue;
+ }
+
+ yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
+ }
+ }
+
+ private class StackMetadata
+ {
+ public StackMetadata(bool isDirectory, bool isNumerical, string partType)
+ {
+ Parts = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ IsDirectory = isDirectory;
+ IsNumerical = isNumerical;
+ PartType = partType;
}
- return regex.Match(regexInput, offset);
+ public Dictionary Parts { get; }
+
+ public bool IsDirectory { get; }
+
+ public bool IsNumerical { get; }
+
+ public string PartType { get; }
+
+ public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
}
}
}
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index 079987fe8a..f7ba606e3e 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
@@ -28,7 +28,7 @@ namespace Emby.Naming.Video
var extension = Path.GetExtension(path);
- if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs
index 1457db7378..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
@@ -106,9 +107,9 @@ 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()
diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs
index 930fdb33f8..8847ee9bc9 100644
--- a/Emby.Naming/Video/VideoInfo.cs
+++ b/Emby.Naming/Video/VideoInfo.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
@@ -17,7 +18,6 @@ namespace Emby.Naming.Video
Name = name;
Files = Array.Empty();
- Extras = Array.Empty();
AlternateVersions = Array.Empty();
}
@@ -39,16 +39,15 @@ namespace Emby.Naming.Video
/// The files.
public IReadOnlyList Files { get; set; }
- ///
- /// Gets or sets the extras.
- ///
- /// The extras.
- public IReadOnlyList Extras { get; set; }
-
///
/// Gets or sets the alternate versions.
///
/// The alternate versions.
public IReadOnlyList AlternateVersions { get; set; }
+
+ ///
+ /// Gets or sets the extra type.
+ ///
+ public ExtraType? ExtraType { get; set; }
}
}
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index fd16774739..4fc849256b 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -12,46 +11,43 @@ namespace Emby.Naming.Video
///
/// Resolves alternative versions and extras from list of video files.
///
- public class VideoListResolver
+ public static class VideoListResolver
{
- private readonly NamingOptions _options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// object containing CleanStringRegexes and VideoFlagDelimiters and passes options to and .
- public VideoListResolver(NamingOptions options)
- {
- _options = options;
- }
-
///
/// Resolves alternative versions and extras from list of video files.
///
- /// List of related video files.
+ /// List of related video files.
+ /// The naming options.
/// Indication we should consider multi-versions of content.
+ /// Whether to parse the name or use the filename.
/// Returns enumerable of which groups files together when related.
- public IEnumerable Resolve(List files, bool supportMultiVersion = true)
+ public static IReadOnlyList Resolve(IReadOnlyList videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
{
- var videoResolver = new VideoResolver(_options);
-
- var videoInfos = files
- .Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
- .OfType()
- .ToList();
-
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var nonExtras = videoInfos
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
- var stackResult = new StackResolver(_options)
- .Resolve(nonExtras).ToList();
+ var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
- var remainingFiles = videoInfos
- .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
- .ToList();
+ var remainingFiles = new List();
+ var standaloneMedia = new List();
+
+ for (var i = 0; i < videoInfos.Count; i++)
+ {
+ var current = videoInfos[i];
+ if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
+ {
+ continue;
+ }
+
+ remainingFiles.Add(current);
+ if (current.ExtraType == null)
+ {
+ standaloneMedia.Add(current);
+ }
+ }
var list = new List();
@@ -59,201 +55,136 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
.OfType()
.ToList()
};
info.Year = info.Files[0].Year;
-
- var extraBaseNames = new List { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
-
- var extras = GetExtras(remainingFiles, extraBaseNames);
-
- if (extras.Count > 0)
- {
- remainingFiles = remainingFiles
- .Except(extras)
- .ToList();
-
- info.Extras = extras;
- }
-
list.Add(info);
}
- var standaloneMedia = remainingFiles
- .Where(i => i.ExtraType == null)
- .ToList();
-
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();
-
- info.Extras = extras;
-
+ remainingFiles.Remove(media);
list.Add(info);
}
if (supportMultiVersion)
{
- list = GetVideosGroupedByVersion(list)
- .ToList();
- }
-
- // If there's only one resolved video, use the folder name as well to find extras
- if (list.Count == 1)
- {
- var info = list[0];
- var videoPath = list[0].Files[0].Path;
- var parentPath = Path.GetDirectoryName(videoPath);
-
- if (!string.IsNullOrEmpty(parentPath))
- {
- var folderName = Path.GetFileName(parentPath);
- if (!string.IsNullOrEmpty(folderName))
- {
- var extras = GetExtras(remainingFiles, new List { folderName });
-
- remainingFiles = remainingFiles
- .Except(extras)
- .ToList();
-
- extras.AddRange(info.Extras);
- info.Extras = extras;
- }
- }
-
- // Add the extras that are just based on file name as well
- var extrasByFileName = remainingFiles
- .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
- .ToList();
-
- remainingFiles = remainingFiles
- .Except(extrasByFileName)
- .ToList();
-
- extrasByFileName.AddRange(info.Extras);
- info.Extras = extrasByFileName;
- }
-
- // If there's only one video, accept all trailers
- // Be lenient because people use all kinds of mishmash conventions with trailers.
- if (list.Count == 1)
- {
- var trailers = remainingFiles
- .Where(i => i.ExtraType == ExtraType.Trailer)
- .ToList();
-
- trailers.AddRange(list[0].Extras);
- list[0].Extras = trailers;
-
- remainingFiles = remainingFiles
- .Except(trailers)
- .ToList();
+ list = GetVideosGroupedByVersion(list, namingOptions);
}
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
- Files = new List { i },
- Year = i.Year
+ Files = new[] { i },
+ Year = i.Year,
+ ExtraType = i.ExtraType
}));
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]);
-
- var alternateVersionsLen = ordered.Count - 1;
- var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- for (int i = 0; i < alternateVersionsLen; i++)
- {
- alternateVersions[i] = ordered[i + 1].Files[0];
- }
-
- 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;
-
- return list;
+ return videos;
}
- return videos;
- }
-
- private bool HaveSameYear(List videos)
- {
- return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
- }
-
- private bool IsEligibleForMultiVersion(string folderName, string? testFilename)
- {
- testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty;
-
- if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
+ // 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++)
{
- if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
+ var video = videos[i];
+ if (video.ExtraType != null)
{
- testFilename = cleanName.ToString();
+ continue;
}
- if (folderName.Length <= testFilename.Length)
+ if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
- testFilename = testFilename.Substring(folderName.Length).Trim();
+ return videos;
}
-
- return string.IsNullOrEmpty(testFilename)
- || testFilename[0].Equals('-')
- || testFilename[0].Equals('_')
- || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
}
- return false;
+ // 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));
+
+ var list = new List
+ {
+ videos[0]
+ };
+
+ var alternateVersionsLen = videos.Count - 1;
+ var alternateVersions = new VideoFileInfo[alternateVersionsLen];
+ for (int i = 0; i < alternateVersionsLen; i++)
+ {
+ var video = videos[i + 1];
+ alternateVersions[i] = video.Files[0];
+ }
+
+ list[0].AlternateVersions = alternateVersions;
+ list[0].Name = folderName.ToString();
+
+ return list;
}
- private List GetExtras(IEnumerable remainingFiles, List baseNames)
+ private static bool HaveSameYear(IReadOnlyList videos)
{
- foreach (var name in baseNames.ToList())
+ if (videos.Count == 1)
{
- var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
- baseNames.Add(trimmedName);
+ return true;
}
- return remainingFiles
- .Where(i => i.ExtraType != null)
- .Where(i => baseNames.Any(b =>
- i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
- .ToList();
+ 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 static bool IsEligibleForMultiVersion(ReadOnlySpan folderName, string testFilePath, NamingOptions namingOptions)
+ {
+ var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
+ if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Remove the folder name before cleaning as we don't care about cleaning that part
+ if (folderName.Length <= testFilename.Length)
+ {
+ 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();
+ }
+
+ // The CleanStringParser should have removed common keywords etc.
+ return string.IsNullOrEmpty(tmpTestFilename)
+ || testFilename[0] == '-'
+ || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index d7165d8d7f..9cadc14658 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -1,45 +1,37 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
///
/// Resolves from file path.
///
- public class VideoResolver
+ public static class VideoResolver
{
- private readonly NamingOptions _options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
- /// and passes options in , , and .
- public VideoResolver(NamingOptions options)
- {
- _options = options;
- }
-
///
/// Resolves the directory.
///
/// The path.
+ /// The naming options.
+ /// Whether to parse the name or use the filename.
/// VideoFileInfo.
- public VideoFileInfo? ResolveDirectory(string? path)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
{
- return Resolve(path, true);
+ return Resolve(path, true, namingOptions, parseName);
}
///
/// 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);
}
///
@@ -47,10 +39,11 @@ 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))
{
@@ -58,18 +51,18 @@ namespace Emby.Naming.Video
}
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;
}
@@ -80,33 +73,30 @@ 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 = ExtraResolver.GetExtraInfo(path, namingOptions);
- 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 var newName))
{
- name = newName.ToString();
+ name = newName;
}
}
return new VideoFileInfo(
path: path,
- container: container,
+ container: container.IsEmpty ? null : container.ToString(),
isStub: isStub,
name: name,
year: year,
@@ -122,43 +112,47 @@ namespace Emby.Naming.Video
/// Determines if path is video file based on extension.
///
/// Path to file.
+ /// The naming options.
/// True if is video file.
- public bool IsVideoFile(string path)
+ 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);
}
///
/// Determines if path is video file stub based on extension.
///
/// Path to file.
+ /// The naming options.
/// True if is video file stub.
- public bool IsStubFile(string path)
+ 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);
}
///
/// Tries to clean name of clutter.
///
/// Raw name.
+ /// The naming options.
/// Clean name.
/// True if cleaning of name was successful.
- public bool TryCleanString(string name, out ReadOnlySpan newName)
+ public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out string newName)
{
- return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
+ return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}
///
/// Tries to get name and year from raw name.
///
/// Raw name.
+ /// The naming options.
/// Returns with name and optional year.
- public CleanDateTimeResult CleanDateTime(string name)
+ 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/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 16ee918c46..d200682e65 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -6,11 +6,9 @@
- net5.0
+ net6.0
false
true
- true
- enable
@@ -25,14 +23,9 @@
-
-
- ../jellyfin.ruleset
-
-
diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs
index 7433d3c8ae..a56df70312 100644
--- a/Emby.Notifications/NotificationEntryPoint.cs
+++ b/Emby.Notifications/NotificationEntryPoint.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
@@ -77,7 +78,6 @@ namespace Emby.Notifications
{
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
- _appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
return Task.CompletedTask;
@@ -105,7 +105,7 @@ namespace Emby.Notifications
var type = entry.Type;
- if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
+ if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
{
return;
}
@@ -132,25 +132,6 @@ 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)
{
if (!FilterItem(e.Item))
@@ -325,7 +306,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 62e33e6c44..bf6252c195 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -19,23 +19,16 @@
- net5.0
+ net6.0
false
true
- true
- enable
-
-
- ../jellyfin.ruleset
-
-
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index 4071e4e547..cef82b4d66 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -60,7 +61,7 @@ namespace Emby.Photos
item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
- if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
+ if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
{
try
{
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 660bbb2deb..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,11 +55,7 @@ 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 => "%AppDataPath%";
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 4f72c8ce15..5ba4749a60 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.
///
@@ -297,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
///
public object GetConfiguration(string key)
{
- return _configurations.GetOrAdd(key, k =>
- {
- var file = GetConfigurationFile(key);
-
- var configurationInfo = _configurationStores
- .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
-
- if (configurationInfo == null)
+ return _configurations.GetOrAdd(
+ key,
+ static (k, configurationManager) =>
{
- throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
- }
+ var file = configurationManager.GetConfigurationFile(k);
- var configurationType = configurationInfo.ConfigurationType;
+ var configurationInfo = Array.Find(
+ configurationManager._configurationStores,
+ i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
- lock (_configurationSyncLock)
- {
- return LoadConfiguration(file, configurationType);
- }
- });
+ if (configurationInfo == null)
+ {
+ throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
+ }
+
+ var configurationType = configurationInfo.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 77819c7649..f923e59efb 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -1,9 +1,5 @@
-#nullable enable
-
using System;
using System.IO;
-using System.Linq;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase
@@ -36,26 +32,27 @@ namespace Emby.Server.Implementations.AppBase
}
catch (Exception)
{
- configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(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);
xmlSerializer.SerializeToStream(configuration, stream);
// Take the object we just got and serialize it back to bytes
- byte[] newBytes = stream.GetBuffer();
- int newBytesLen = (int)stream.Length;
+ Span newBytes = stream.GetBuffer().AsSpan(0, (int)stream.Length);
// If the file didn't exist before, or if something has changed, re-save
- if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
+ if (buffer == null || !newBytes.SequenceEqual(buffer))
{
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))
+ using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
- fs.Write(newBytes, 0, newBytesLen);
+ fs.Write(newBytes);
}
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 1b9bb86bbd..8892f7f40f 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -1,6 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -10,14 +13,13 @@ using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main;
using Emby.Dlna.Ssdp;
using Emby.Drawing;
+using Emby.Naming.Common;
using Emby.Notifications;
using Emby.Photos;
using Emby.Server.Implementations.Archiving;
@@ -38,11 +40,11 @@ using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
-using Emby.Server.Implementations.Security;
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;
@@ -50,16 +52,15 @@ 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;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
+using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -75,7 +76,6 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
@@ -99,10 +99,10 @@ 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
@@ -117,7 +117,13 @@ namespace Emby.Server.Implementations
///
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
+ ///
+ /// The disposable parts.
+ ///
+ private readonly ConcurrentDictionary _disposableParts = new ();
+
private readonly IFileSystem _fileSystemManager;
+ private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
@@ -125,8 +131,57 @@ namespace Emby.Server.Implementations
private List _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
- private string[] _urlPrefixes;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+
+ ///
+ /// Gets or sets all concrete types.
+ ///
+ /// All concrete types.
+ private Type[] _allConcreteTypes;
+
+ private DeviceId _deviceId;
+
+ private bool _disposed = false;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// The interface.
+ public ApplicationHost(
+ IServerApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IStartupOptions options,
+ IConfiguration startupConfig)
+ {
+ ApplicationPaths = applicationPaths;
+ LoggerFactory = loggerFactory;
+ _startupOptions = options;
+ _startupConfig = startupConfig;
+ _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger(), applicationPaths);
+
+ Logger = LoggerFactory.CreateLogger();
+ _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
+
+ 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);
+ }
+
+ ///
+ /// Occurs when [has pending restart changed].
+ ///
+ public event EventHandler HasPendingRestartChanged;
///
/// Gets a value indicating whether this instance can self restart.
@@ -149,25 +204,14 @@ 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].
- ///
- public event EventHandler HasPendingRestartChanged;
+ public INetworkManager NetManager { get; private set; }
///
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
@@ -183,35 +227,22 @@ namespace Emby.Server.Implementations
///
protected ILogger Logger { get; }
- protected IServiceCollection ServiceCollection { get; }
-
///
/// Gets the logger factory.
///
protected ILoggerFactory LoggerFactory { get; }
///
- /// Gets or sets the application paths.
+ /// Gets the application paths.
///
/// The application paths.
- protected IServerApplicationPaths ApplicationPaths { get; set; }
+ protected IServerApplicationPaths ApplicationPaths { get; }
///
- /// Gets or sets all concrete types.
- ///
- /// All concrete types.
- private Type[] _allConcreteTypes;
-
- ///
- /// The disposable parts.
- ///
- private readonly List _disposableParts = new List();
-
- ///
- /// Gets or sets the configuration manager.
+ /// Gets the configuration manager.
///
/// The configuration manager.
- protected IConfigurationManager ConfigurationManager { get; set; }
+ public ServerConfigurationManager ConfigurationManager { get; }
///
/// Gets or sets the service provider.
@@ -229,97 +260,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;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- public ApplicationHost(
- IServerApplicationPaths applicationPaths,
- ILoggerFactory loggerFactory,
- IStartupOptions options,
- IFileSystem fileSystem,
- IServiceCollection serviceCollection)
- {
- _xmlSerializer = new MyXmlSerializer();
-
- ServiceCollection = serviceCollection;
-
- ApplicationPaths = applicationPaths;
- LoggerFactory = loggerFactory;
- _fileSystemManager = fileSystem;
-
- ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
- // Have to migrate settings here as migration subsystem not yet initialised.
- MigrateNetworkConfiguration();
-
- // Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
- ConfigurationManager.RegisterConfiguration();
- NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger());
-
- Logger = LoggerFactory.CreateLogger();
-
- _startupOptions = options;
-
- // Initialize runtime stat collection
- if (ServerConfigurationManager.Configuration.EnableMetrics)
- {
- DotNetRuntimeStatsBuilder.Default().StartCollecting();
- }
-
- fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
-
- ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
- ApplicationVersionString = ApplicationVersion.ToString(3);
- ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
-
- _pluginManager = new PluginManager(
- LoggerFactory.CreateLogger(),
- this,
- ServerConfigurationManager.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(ServerConfigurationManager.Configuration, networkSettings);
- _xmlSerializer.SerializeToFile(networkSettings, path);
- Logger?.LogDebug("Successfully migrated network settings.");
- }
- }
-
- public string ExpandVirtualPath(string path)
- {
- var appPaths = ApplicationPaths;
-
- return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
- .Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
- }
-
- public string ReverseVirtualPath(string path)
- {
- var appPaths = ApplicationPaths;
-
- return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
- .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
- }
+ public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
///
public Version ApplicationVersion { get; }
@@ -345,16 +288,11 @@ namespace Emby.Server.Implementations
/// The application name.
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
- private DeviceId _deviceId;
-
public string SystemId
{
get
{
- if (_deviceId == null)
- {
- _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
- }
+ _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
@@ -363,21 +301,33 @@ namespace Emby.Server.Implementations
///
public string Name => ApplicationProductName;
- ///
- /// Creates an instance of type and resolves all constructor dependencies.
- ///
- /// The type.
- /// System.Object.
- public object CreateInstance(Type type)
- => ActivatorUtilities.CreateInstance(ServiceProvider, type);
+ private string CertificatePath { get; set; }
- ///
- /// Creates an instance of type and resolves all constructor dependencies.
- ///
- /// /// The type.
- /// T.
- public T CreateInstance()
- => ActivatorUtilities.CreateInstance(ServiceProvider);
+ public X509Certificate2 Certificate { get; private set; }
+
+ ///
+ public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
+
+ public string FriendlyName =>
+ string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
+ ? Environment.MachineName
+ : ConfigurationManager.Configuration.ServerName;
+
+ public string ExpandVirtualPath(string path)
+ {
+ var appPaths = ApplicationPaths;
+
+ return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
+ .Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public string ReverseVirtualPath(string path)
+ {
+ var appPaths = ApplicationPaths;
+
+ return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
+ .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
+ }
///
/// Creates the instance safe.
@@ -386,12 +336,9 @@ namespace Emby.Server.Implementations
/// System.Object.
protected object CreateInstanceSafe(Type type)
{
- if (_creatingInstances == null)
- {
- _creatingInstances = new List();
- }
+ _creatingInstances ??= new List();
- if (_creatingInstances.IndexOf(type) != -1)
+ if (_creatingInstances.Contains(type))
{
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
foreach (var entry in _creatingInstances)
@@ -401,7 +348,7 @@ namespace Emby.Server.Implementations
_pluginManager.FailPlugin(type.Assembly);
- throw new ExternalException("DI Loop detected.");
+ throw new TypeLoadException("DI Loop detected");
}
try
@@ -434,8 +381,15 @@ namespace Emby.Server.Implementations
public IEnumerable GetExportTypes()
{
var currentType = typeof(T);
-
- return _allConcreteTypes.Where(i => currentType.IsAssignableFrom(i));
+ var numberOfConcreteTypes = _allConcreteTypes.Length;
+ for (var i = 0; i < numberOfConcreteTypes; i++)
+ {
+ var type = _allConcreteTypes[i];
+ if (currentType.IsAssignableFrom(type))
+ {
+ yield return type;
+ }
+ }
}
///
@@ -450,9 +404,9 @@ namespace Emby.Server.Implementations
if (manageLifetime)
{
- lock (_disposableParts)
+ foreach (var part in parts.OfType())
{
- _disposableParts.AddRange(parts.OfType());
+ _disposableParts.TryAdd(part, byte.MinValue);
}
}
@@ -460,7 +414,7 @@ namespace Emby.Server.Implementations
}
///
- public IReadOnlyCollection GetExports(CreationDelegate defaultFunc, bool manageLifetime = true)
+ public IReadOnlyCollection GetExports(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
{
// Convert to list so this isn't executed for each iteration
var parts = GetExportTypes()
@@ -471,9 +425,9 @@ namespace Emby.Server.Implementations
if (manageLifetime)
{
- lock (_disposableParts)
+ foreach (var part in parts.OfType())
{
- _disposableParts.AddRange(parts.OfType());
+ _disposableParts.TryAdd(part, byte.MinValue);
}
}
@@ -483,9 +437,11 @@ namespace Emby.Server.Implementations
///
/// Runs the startup tasks.
///
+ /// The cancellation token.
/// .
- public async Task RunStartupTasksAsync()
+ public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
{
+ cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks");
Resolve().AddTasks(GetExports(false));
@@ -495,18 +451,25 @@ namespace Emby.Server.Implementations
_mediaEncoder.SetFFmpegPath();
- Logger.LogInformation("ServerId: {0}", SystemId);
+ Logger.LogInformation("ServerId: {ServerId}", SystemId);
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();
@@ -528,9 +491,21 @@ namespace Emby.Server.Implementations
}
///
- public void Init()
+ public void Init(IServiceCollection serviceCollection)
{
- var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
+ DiscoverTypes();
+
+ ConfigurationManager.AddParts(GetExports());
+
+ NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger());
+
+ // Initialize runtime stat collection
+ if (ConfigurationManager.Configuration.EnableMetrics)
+ {
+ DotNetRuntimeStatsBuilder.Default().StartCollecting();
+ }
+
+ var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
HttpPort = networkConfiguration.HttpServerPortNumber;
HttpsPort = networkConfiguration.HttpsPortNumber;
@@ -541,147 +516,133 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
- CertificateInfo = new CertificateInfo
- {
- Path = networkConfiguration.CertificatePath,
- Password = networkConfiguration.CertificatePassword
- };
- Certificate = GetCertificate(CertificateInfo);
+ CertificatePath = networkConfiguration.CertificatePath;
+ Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
- DiscoverTypes();
+ RegisterServices(serviceCollection);
- RegisterServices();
-
- _pluginManager.RegisterServices(ServiceCollection);
+ _pluginManager.RegisterServices(serviceCollection);
}
///
/// Registers services/resources with the service collection that will be available via DI.
///
- protected virtual void RegisterServices()
+ /// Instance of the interface.
+ protected virtual void RegisterServices(IServiceCollection serviceCollection)
{
- ServiceCollection.AddSingleton(_startupOptions);
+ serviceCollection.AddSingleton(_startupOptions);
- ServiceCollection.AddMemoryCache();
+ serviceCollection.AddMemoryCache();
- ServiceCollection.AddSingleton(ConfigurationManager);
- ServiceCollection.AddSingleton(this);
- ServiceCollection.AddSingleton(_pluginManager);
- ServiceCollection.AddSingleton(ApplicationPaths);
+ serviceCollection.AddSingleton(ConfigurationManager);
+ serviceCollection.AddSingleton(ConfigurationManager);
+ serviceCollection.AddSingleton(this);
+ serviceCollection.AddSingleton(_pluginManager);
+ serviceCollection.AddSingleton(ApplicationPaths);
- ServiceCollection.AddSingleton(_fileSystemManager);
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton(_fileSystemManager);
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton(NetManager);
+ serviceCollection.AddSingleton(NetManager);
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton(_xmlSerializer);
+ serviceCollection.AddSingleton(_xmlSerializer);
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton(this);
- ServiceCollection.AddSingleton(ApplicationPaths);
+ serviceCollection.AddSingleton(this);
+ serviceCollection.AddSingleton(ApplicationPaths);
- ServiceCollection.AddSingleton(ServerConfigurationManager);
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
-
- 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(provider.GetRequiredService));
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy isn't required
- ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
- ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
- ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
- ServiceCollection.AddSingleton();
+ serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
+ serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
+ serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
-
- ServiceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
// TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required
- ServiceCollection.AddTransient(provider => new Lazy