mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-12 20:13:01 +03:00
Compare commits
94 Commits
v10.8.2
...
v10.7.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5a7478600 | ||
|
|
ed333dec43 | ||
|
|
c17c32f9dc | ||
|
|
5cc8ed6516 | ||
|
|
cdba6b3d35 | ||
|
|
fa7a8752a9 | ||
|
|
d129afa74e | ||
|
|
bc8a1d2276 | ||
|
|
147f9e1edf | ||
|
|
7796486511 | ||
|
|
ab5ae34595 | ||
|
|
81a17b803d | ||
|
|
cc6afb0971 | ||
|
|
d9a9a23a3c | ||
|
|
dd1fddf79c | ||
|
|
4df7522629 | ||
|
|
9c83a6cef9 | ||
|
|
910819c71c | ||
|
|
a0e047d560 | ||
|
|
34322ba491 | ||
|
|
801dd74ff6 | ||
|
|
129453214f | ||
|
|
ac82fead82 | ||
|
|
9a59ff3c87 | ||
|
|
a16cf8ec0a | ||
|
|
4a2b143028 | ||
|
|
d737c2b84a | ||
|
|
1ad8e54035 | ||
|
|
83dd3e2201 | ||
|
|
d9634b7fc0 | ||
|
|
05b34b2710 | ||
|
|
2dab55a8f2 | ||
|
|
124ab090bc | ||
|
|
03c8216946 | ||
|
|
f72f27ff45 | ||
|
|
71188ad27a | ||
|
|
0d9e8b4f00 | ||
|
|
fbfb23abab | ||
|
|
622c71ce1c | ||
|
|
dbc9256945 | ||
|
|
6b20aaaa6a | ||
|
|
c2097ba5fe | ||
|
|
8884f4f288 | ||
|
|
ce741f541c | ||
|
|
783d6409af | ||
|
|
5bf25ce2cc | ||
|
|
6c2ddd9758 | ||
|
|
8760a298b1 | ||
|
|
c08933c9d9 | ||
|
|
c86c652006 | ||
|
|
c08ce82a04 | ||
|
|
b10178bd1e | ||
|
|
00a608dfab | ||
|
|
480ded0671 | ||
|
|
98c081f0ce | ||
|
|
e77d0ab5fd | ||
|
|
28ef1bc8b2 | ||
|
|
7ebf7014e0 | ||
|
|
89a649cc23 | ||
|
|
24b2991def | ||
|
|
87dde66e92 | ||
|
|
1b82ef905e | ||
|
|
02cc83b807 | ||
|
|
0d8fa795a0 | ||
|
|
b32f15ab1e | ||
|
|
26993e39f7 | ||
|
|
ddedb2d7f1 | ||
|
|
5c0d930dc3 | ||
|
|
13d62c5977 | ||
|
|
9799b6ae81 | ||
|
|
c1dd8f2050 | ||
|
|
853c328763 | ||
|
|
126753a1fe | ||
|
|
24e4fcc3b7 | ||
|
|
aae90a8480 | ||
|
|
11a37884f0 | ||
|
|
49f3579c1b | ||
|
|
259d811b95 | ||
|
|
7e01cce884 | ||
|
|
2e5333c1d4 | ||
|
|
e8e1bbffd9 | ||
|
|
723fe43d2e | ||
|
|
e70a6d41f4 | ||
|
|
9d4417eee3 | ||
|
|
67f41386ba | ||
|
|
b1af8db423 | ||
|
|
91656acabb | ||
|
|
5fa8c83ba4 | ||
|
|
683bc27b27 | ||
|
|
0b6a05cf82 | ||
|
|
2647935b96 | ||
|
|
2a4023c6c7 | ||
|
|
2a2630098b | ||
|
|
79472dce70 |
@@ -7,7 +7,7 @@ parameters:
|
||||
default: "ubuntu-latest"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 6.0.x
|
||||
default: 5.0.100
|
||||
|
||||
jobs:
|
||||
- job: CompatibilityCheck
|
||||
|
||||
59
.ci/azure-pipelines-api-client.yml
Normal file
59
.ci/azure-pipelines-api-client.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
parameters:
|
||||
LinuxImage: 'ubuntu-latest'
|
||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
DotNetSdkVersion: 6.0.x
|
||||
DotNetSdkVersion: 5.0.100
|
||||
|
||||
jobs:
|
||||
- job: Build
|
||||
@@ -91,10 +91,3 @@ 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'
|
||||
|
||||
@@ -39,10 +39,6 @@ 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: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
|
||||
displayName: 'Build Dockerfile'
|
||||
|
||||
@@ -84,10 +80,6 @@ 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')
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download OpenAPI Spec'
|
||||
inputs:
|
||||
@@ -168,6 +160,7 @@ jobs:
|
||||
dependsOn:
|
||||
- BuildPackage
|
||||
- BuildDocker
|
||||
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
@@ -189,10 +182,13 @@ jobs:
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'commands'
|
||||
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
|
||||
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
|
||||
|
||||
- job: PublishNuget
|
||||
displayName: 'Publish NuGet packages'
|
||||
dependsOn:
|
||||
- BuildPackage
|
||||
condition: succeeded('BuildPackage')
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
@@ -203,10 +199,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Use .NET 6.0 sdk'
|
||||
displayName: 'Use .NET 5.0 sdk'
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '6.0.x'
|
||||
version: '5.0.x'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build Stable Nuget packages'
|
||||
@@ -219,7 +215,6 @@ 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: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
|
||||
|
||||
@@ -234,7 +229,6 @@ 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'
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ parameters:
|
||||
default: "tests/**/*Tests.csproj"
|
||||
- name: DotNetSdkVersion
|
||||
type: string
|
||||
default: 6.0.x
|
||||
default: 5.0.100
|
||||
|
||||
jobs:
|
||||
- job: Test
|
||||
@@ -94,5 +94,5 @@ jobs:
|
||||
displayName: 'Publish OpenAPI Artifact'
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
inputs:
|
||||
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json"
|
||||
targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json"
|
||||
artifactName: 'OpenAPI Spec'
|
||||
|
||||
@@ -5,6 +5,8 @@ variables:
|
||||
value: 'tests/**/*Tests.csproj'
|
||||
- name: RestoreBuildProjects
|
||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
- name: DotNetSdkVersion
|
||||
value: 5.0.100
|
||||
|
||||
pr:
|
||||
autoCancel: true
|
||||
@@ -55,10 +57,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
|
||||
|
||||
1
.copr/Makefile
Symbolic link
1
.copr/Makefile
Symbolic link
@@ -0,0 +1 @@
|
||||
../fedora/Makefile
|
||||
30
.drone.yml
Normal file
30
.drone.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: build-debug
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: docker:git
|
||||
commands:
|
||||
- git submodule update --init --recursive
|
||||
|
||||
- name: build
|
||||
image: microsoft/dotnet:2-sdk
|
||||
commands:
|
||||
- dotnet publish "Jellyfin.Server" --configuration Debug --output "../ci/ci-debug"
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: build-release
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: docker:git
|
||||
commands:
|
||||
- git submodule update --init --recursive
|
||||
|
||||
- name: build
|
||||
image: microsoft/dotnet:2-sdk
|
||||
commands:
|
||||
- dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release"
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1,4 +1,3 @@
|
||||
# Joshua must review all changes to deployment and build.sh
|
||||
.ci/* @joshuaboniface
|
||||
deployment/* @joshuaboniface
|
||||
build.sh @joshuaboniface
|
||||
|
||||
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**System (please complete the following information):**
|
||||
- OS: [e.g. Debian, Windows]
|
||||
- Virtualization: [e.g. Docker, KVM, LXC]
|
||||
- Clients: [Browser, Android, Fire Stick, etc.]
|
||||
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
|
||||
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
|
||||
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
|
||||
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
|
||||
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
|
||||
- Base URL: [e.g. none, yes: /example]
|
||||
- Networking: [e.g. Host, Bridge/NAT]
|
||||
- Storage: [e.g. local, NFS, cloud]
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Logs**
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
106
.github/ISSUE_TEMPLATE/issue report.yml
vendored
106
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -1,106 +0,0 @@
|
||||
name: Issue Report
|
||||
description: File an issue report
|
||||
title: "[Issue]: "
|
||||
labels: [bug, triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Please describe your bug
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: |
|
||||
The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
|
||||
|
||||
This is my issue.
|
||||
|
||||
Steps to Reproduce
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Jellyfin Version
|
||||
description: What version of Jellyfin are you running?
|
||||
options:
|
||||
- 10.7.7
|
||||
- 10.7.z
|
||||
- 10.6.4
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version-other
|
||||
attributes:
|
||||
label: "if other:"
|
||||
placeholder: Other
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
Examples:
|
||||
- **OS**: [e.g. Debian, Windows]
|
||||
- **Virtualization**: [e.g. Docker, KVM, LXC]
|
||||
- **Clients**: [Browser, Android, Fire Stick, etc.]
|
||||
- **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
|
||||
- **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
|
||||
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
|
||||
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
|
||||
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
|
||||
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
||||
- **Base URL**: [e.g. none, yes: /example]
|
||||
- **Networking**: [e.g. Host, Bridge/NAT]
|
||||
- **Storage**: [e.g. local, NFS, cloud]
|
||||
value: |
|
||||
- OS:
|
||||
- Virtualization:
|
||||
- Clients:
|
||||
- Browser:
|
||||
- FFmpeg Version:
|
||||
- Playback Method:
|
||||
- Hardware Acceleration:
|
||||
- Plugins:
|
||||
- Reverse Proxy:
|
||||
- Base URL:
|
||||
- Networking:
|
||||
- Storage:
|
||||
render: markdown
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Jellyfin logs
|
||||
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||
placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: ffmpeg-logs
|
||||
attributes:
|
||||
label: FFmpeg logs
|
||||
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||
placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: browserlogs
|
||||
attributes:
|
||||
label: Please attach any browser or client logs here
|
||||
placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Please attach any screenshots here
|
||||
placeholder: Images can be pasted directly into the textbox and will be hosted by github.
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -6,10 +6,4 @@ 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
|
||||
|
||||
|
||||
25
.github/stale.yml
vendored
Normal file
25
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 120
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 21
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- regression
|
||||
- security
|
||||
- dotnet-3.0-future
|
||||
- roadmap
|
||||
- future
|
||||
- feature
|
||||
- enhancement
|
||||
- confirmed
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
76
.github/workflows/automation.yml
vendored
76
.github/workflows/automation.yml
vendored
@@ -1,76 +0,0 @@
|
||||
name: Automation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
issue_comment:
|
||||
|
||||
jobs:
|
||||
label:
|
||||
name: Labeling
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
project:
|
||||
name: Project board
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||
steps:
|
||||
- name: Remove from 'Current Release' project
|
||||
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Current Release
|
||||
action: delete
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add to 'Release Next' project
|
||||
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Release Next
|
||||
column: In progress
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add to 'Current Release' project
|
||||
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Current Release
|
||||
column: In progress
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Check number of comments from the team member
|
||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
|
||||
id: member_comments
|
||||
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
||||
|
||||
- name: Move issue to needs triage
|
||||
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Issue Triage for Main Repo
|
||||
column: Needs triage
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add issue to triage project
|
||||
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Issue Triage for Main Repo
|
||||
column: Pending response
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
7
.github/workflows/codeql-analysis.yml
vendored
7
.github/workflows/codeql-analysis.yml
vendored
@@ -20,12 +20,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
dotnet-version: '5.0.x'
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
|
||||
119
.github/workflows/commands.yml
vendored
119
.github/workflows/commands.yml
vendored
@@ -1,119 +0,0 @@
|
||||
name: Commands
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
- edited
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
rebase:
|
||||
name: Rebase
|
||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
check-backport:
|
||||
name: Check Backport
|
||||
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ github.event.comment != null }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: eyes
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Notify as running
|
||||
id: comment_running
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ github.event.comment != null }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Running backport tests...
|
||||
|
||||
- name: Perform test backport
|
||||
id: run_tests
|
||||
run: |
|
||||
set +o errexit
|
||||
git config --global user.name "Jellyfin Bot"
|
||||
git config --global user.email "team@jellyfin.org"
|
||||
CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
|
||||
git checkout master
|
||||
git merge --no-ff ${CURRENT_BRANCH}
|
||||
MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
|
||||
git fetch --all
|
||||
CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
|
||||
stable_branch="Current stable release branch: ${CURRENT_STABLE}"
|
||||
echo ${stable_branch}
|
||||
echo ::set-output name=branch::${stable_branch}
|
||||
git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
|
||||
git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
|
||||
retcode=$?
|
||||
cat output.txt | grep -v 'hint:'
|
||||
output="$( grep -v 'hint:' output.txt )"
|
||||
output="${output//'%'/'%25'}"
|
||||
output="${output//$'\n'/'%0A'}"
|
||||
output="${output//$'\r'/'%0D'}"
|
||||
echo ::set-output name=output::$output
|
||||
exit ${retcode}
|
||||
|
||||
- name: Notify with result success
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ github.event.comment != null && success() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
||||
body: |
|
||||
${{ steps.run_tests.outputs.branch }}
|
||||
Output from `git cherry-pick`:
|
||||
|
||||
---
|
||||
|
||||
${{ steps.run_tests.outputs.output }}
|
||||
reactions: hooray
|
||||
|
||||
- name: Notify with result failure
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ github.event.comment != null && failure() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
||||
body: |
|
||||
${{ steps.run_tests.outputs.branch }}
|
||||
Output from `git cherry-pick`:
|
||||
|
||||
---
|
||||
|
||||
${{ steps.run_tests.outputs.output }}
|
||||
reactions: confused
|
||||
124
.github/workflows/openapi.yml
vendored
124
.github/workflows/openapi.yml
vendored
@@ -1,124 +0,0 @@
|
||||
name: OpenAPI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
openapi-head:
|
||||
name: OpenAPI - HEAD
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
|
||||
|
||||
openapi-base:
|
||||
name: OpenAPI - BASE
|
||||
if: ${{ github.base_ref != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
|
||||
|
||||
openapi-diff:
|
||||
name: OpenAPI - Difference
|
||||
if: ${{ github.event_name == 'pull_request_target' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- openapi-head
|
||||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
- name: Workaround openapi-diff issue
|
||||
run: |
|
||||
sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
|
||||
sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
|
||||
- name: Calculate OpenAPI difference
|
||||
uses: docker://openapitools/openapi-diff
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
|
||||
- id: read-diff
|
||||
name: Read openapi-diff output
|
||||
run: |
|
||||
body=$(cat openapi-changes.md)
|
||||
body="${body//'%'/'%25'}"
|
||||
body="${body//$'\n'/'%0A'}"
|
||||
body="${body//$'\r'/'%0D'}"
|
||||
echo ::set-output name=body::$body
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
direction: last
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ steps.read-diff.outputs.body != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!--openapi-diff-workflow-comment-->
|
||||
<details>
|
||||
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
|
||||
|
||||
${{ steps.read-diff.outputs.body }}
|
||||
|
||||
</details>
|
||||
- name: Edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!--openapi-diff-workflow-comment-->
|
||||
|
||||
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
||||
27
.github/workflows/repo-stale.yaml
vendored
27
.github/workflows/repo-stale.yaml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Issue Stale Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
days-before-stale: 120
|
||||
days-before-pr-stale: -1
|
||||
days-before-close: 21
|
||||
days-before-pr-close: -1
|
||||
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
|
||||
stale-issue-label: stale
|
||||
stale-issue-message: |-
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -268,7 +268,6 @@ doc/
|
||||
# Deployment artifacts
|
||||
dist
|
||||
*.exe
|
||||
*.dll
|
||||
|
||||
# BenchmarkDotNet artifacts
|
||||
BenchmarkDotNet.Artifacts
|
||||
@@ -278,6 +277,3 @@ web/
|
||||
web-src.*
|
||||
MediaBrowser.WebDashboard/jellyfin-web
|
||||
apiclient/generated
|
||||
|
||||
# Omnisharp crash logs
|
||||
mono_crash.*.json
|
||||
|
||||
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
|
||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||
"console": "internalConsole",
|
||||
@@ -22,7 +22,7 @@
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
|
||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
|
||||
"args": ["--nowebclient"],
|
||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||
"console": "internalConsole",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
P:System.Threading.Tasks.Task`1.Result
|
||||
M:System.Guid.op_Equality(System.Guid,System.Guid)
|
||||
M:System.Guid.op_Inequality(System.Guid,System.Guid)
|
||||
M:System.Guid.Equals(System.Object)
|
||||
@@ -1,6 +1,5 @@
|
||||
# Jellyfin Contributors
|
||||
|
||||
- [1337joe](https://github.com/1337joe)
|
||||
- [97carmine](https://github.com/97carmine)
|
||||
- [Abbe98](https://github.com/Abbe98)
|
||||
- [agrenott](https://github.com/agrenott)
|
||||
@@ -18,7 +17,6 @@
|
||||
- [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)
|
||||
@@ -46,14 +44,11 @@
|
||||
- [Froghut](https://github.com/Froghut)
|
||||
- [fruhnow](https://github.com/fruhnow)
|
||||
- [geilername](https://github.com/geilername)
|
||||
- [GermanCoding](https://github.com/GermanCoding)
|
||||
- [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)
|
||||
@@ -73,12 +68,10 @@
|
||||
- [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)
|
||||
- [n8225](https://github.com/n8225)
|
||||
- [Nalsai](https://github.com/Nalsai)
|
||||
- [Narfinger](https://github.com/Narfinger)
|
||||
- [NathanPickard](https://github.com/NathanPickard)
|
||||
- [neilsb](https://github.com/neilsb)
|
||||
@@ -87,7 +80,6 @@
|
||||
- [nvllsvm](https://github.com/nvllsvm)
|
||||
- [nyanmisaka](https://github.com/nyanmisaka)
|
||||
- [OancaAndrei](https://github.com/OancaAndrei)
|
||||
- [obradovichv](https://github.com/obradovichv)
|
||||
- [oddstr13](https://github.com/oddstr13)
|
||||
- [orryverducci](https://github.com/orryverducci)
|
||||
- [petermcneil](https://github.com/petermcneil)
|
||||
@@ -111,14 +103,12 @@
|
||||
- [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)
|
||||
- [StollD](https://github.com/StollD)
|
||||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||
- [tbraeutigam](https://github.com/tbraeutigam)
|
||||
- [teacupx](https://github.com/teacupx)
|
||||
@@ -152,11 +142,6 @@
|
||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||
- [skyfrk](https://github.com/skyfrk)
|
||||
- [ianjazz246](https://github.com/ianjazz246)
|
||||
- [peterspenler](https://github.com/peterspenler)
|
||||
- [MBR-0001](https://github.com/MBR-0001)
|
||||
- [jonas-resch](https://github.com/jonas-resch)
|
||||
- [vgambier](https://github.com/vgambier)
|
||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
@@ -221,7 +206,3 @@
|
||||
- [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)
|
||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||
- [Jakob Kukla](https://github.com/jakobkukla)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<Project>
|
||||
<!-- Sets defaults for all projects in the repo -->
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="$(SolutionDir)/BannedSymbols.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
61
Dockerfile
61
Dockerfile
@@ -1,18 +1,22 @@
|
||||
# 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
|
||||
ARG DOTNET_VERSION=5.0
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
FROM node: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 python3 \
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& yarn install \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM debian:stable-slim as app
|
||||
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
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
@@ -21,17 +25,19 @@ 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=22.0.2
|
||||
ARG IGC_VERSION=1.0.10395
|
||||
ARG NEO_VERSION=22.08.22549
|
||||
ARG LEVEL_ZERO_VERSION=1.3.22549
|
||||
ARG GMMLIB_VERSION=20.3.2
|
||||
ARG IGC_VERSION=1.0.5435
|
||||
ARG NEO_VERSION=20.46.18421
|
||||
ARG LEVEL_ZERO_VERSION=1.0.18421
|
||||
|
||||
# Install dependencies:
|
||||
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 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 curl \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
|
||||
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
|
||||
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
|
||||
&& apt-get update \
|
||||
@@ -48,7 +54,8 @@ RUN apt-get update \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl_${NEO_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-ocloc_${NEO_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
|
||||
&& dpkg -i *.deb \
|
||||
&& cd .. \
|
||||
@@ -61,32 +68,14 @@ 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
|
||||
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
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
# DESIGNED FOR BUILDING ON ARM ONLY
|
||||
# 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
|
||||
ARG DOTNET_VERSION=5.0
|
||||
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
FROM node: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 python3 \
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& yarn install \
|
||||
&& 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:stable-slim as app
|
||||
FROM arm32v7/debian:buster-slim
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
@@ -24,8 +35,6 @@ 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 - && \
|
||||
@@ -44,7 +53,7 @@ RUN apt-get update \
|
||||
vainfo \
|
||||
libva2 \
|
||||
locales \
|
||||
&& apt-get remove gnupg -y \
|
||||
&& apt-get remove curl gnupg -y \
|
||||
&& apt-get clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
@@ -52,33 +61,17 @@ 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
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
|
||||
|
||||
FROM app
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1,52 +1,18 @@
|
||||
# DESIGNED FOR BUILDING ON ARM64 ONLY
|
||||
# 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
|
||||
ARG DOTNET_VERSION=5.0
|
||||
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
FROM node: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 python3 \
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& yarn install \
|
||||
&& 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
|
||||
@@ -57,19 +23,44 @@ RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
|
||||
|
||||
FROM app
|
||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||
FROM arm64v8/debian:buster-slim
|
||||
|
||||
ENV HEALTHCHECK_URL=http://localhost:8096/health
|
||||
# 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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -10,11 +10,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||
<Nullable>disable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
@@ -77,7 +76,7 @@ namespace DvdLib.Ifo
|
||||
|
||||
private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
|
||||
{
|
||||
var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
|
||||
var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
|
||||
|
||||
var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
|
||||
allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Configuration
|
||||
public DlnaOptions()
|
||||
{
|
||||
EnablePlayTo = true;
|
||||
EnableServer = false;
|
||||
EnableServer = true;
|
||||
BlastAliveMessages = true;
|
||||
SendOnlyMatchedHost = true;
|
||||
ClientDiscoveryIntervalSeconds = 60;
|
||||
@@ -72,7 +72,7 @@ namespace Emby.Dlna.Configuration
|
||||
/// <summary>
|
||||
/// Gets or sets the default user account that the dlna server uses.
|
||||
/// </summary>
|
||||
public string? DefaultUserId { get; set; }
|
||||
public string DefaultUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether playTo device profiles should be created.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using Emby.Dlna.Configuration;
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
{
|
||||
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
||||
@@ -138,7 +138,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
/// </summary>
|
||||
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
||||
/// <returns>The <see cref="User"/>.</returns>
|
||||
private User? GetUser(DeviceProfile profile)
|
||||
private User GetUser(DeviceProfile profile)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(profile.UserId))
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
@@ -11,29 +13,24 @@ namespace Emby.Dlna.ContentDirectory
|
||||
/// Initializes a new instance of the <see cref="ServerItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The <see cref="BaseItem"/>.</param>
|
||||
/// <param name="stubType">The stub type.</param>
|
||||
public ServerItem(BaseItem item, StubType? stubType)
|
||||
public ServerItem(BaseItem item)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
if (stubType.HasValue)
|
||||
{
|
||||
StubType = stubType;
|
||||
}
|
||||
else if (item is IItemByName and not Folder)
|
||||
if (item is IItemByName && !(item is Folder))
|
||||
{
|
||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying base item.
|
||||
/// Gets or sets the underlying base item.
|
||||
/// </summary>
|
||||
public BaseItem Item { get; }
|
||||
public BaseItem Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DLNA item type.
|
||||
/// Gets or sets the DLNA item type.
|
||||
/// </summary>
|
||||
public StubType? StubType { get; }
|
||||
public StubType? StubType { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1602
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
|
||||
@@ -6,11 +6,9 @@ namespace Emby.Dlna
|
||||
{
|
||||
public class ControlResponse
|
||||
{
|
||||
public ControlResponse(string xml, bool isSuccessful)
|
||||
public ControlResponse()
|
||||
{
|
||||
Headers = new Dictionary<string, string>();
|
||||
Xml = xml;
|
||||
IsSuccessful = isSuccessful;
|
||||
}
|
||||
|
||||
public IDictionary<string, string> Headers { get; }
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -41,6 +39,8 @@ 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;
|
||||
@@ -160,7 +160,7 @@ namespace Emby.Dlna.Didl
|
||||
else
|
||||
{
|
||||
var parent = item.DisplayParentId;
|
||||
if (!parent.Equals(default))
|
||||
if (!parent.Equals(Guid.Empty))
|
||||
{
|
||||
writer.WriteAttributeString("parentID", GetClientId(parent, null));
|
||||
}
|
||||
@@ -208,8 +208,7 @@ namespace Emby.Dlna.Didl
|
||||
var targetWidth = streamInfo.TargetWidth;
|
||||
var targetHeight = streamInfo.TargetHeight;
|
||||
|
||||
var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
|
||||
_profile,
|
||||
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
@@ -221,7 +220,6 @@ namespace Emby.Dlna.Didl
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
@@ -316,7 +314,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
if (mediaSource.RunTimeTicks.HasValue)
|
||||
{
|
||||
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
|
||||
}
|
||||
|
||||
if (filter.Contains("res@size"))
|
||||
@@ -327,7 +325,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
if (size.HasValue)
|
||||
{
|
||||
writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,7 +339,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
if (targetChannels.HasValue)
|
||||
{
|
||||
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
|
||||
}
|
||||
|
||||
if (filter.Contains("res@resolution"))
|
||||
@@ -360,12 +358,12 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
if (targetSampleRate.HasValue)
|
||||
{
|
||||
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
|
||||
}
|
||||
|
||||
if (totalBitrate.HasValue)
|
||||
{
|
||||
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
|
||||
}
|
||||
|
||||
var mediaProfile = _profile.GetVideoMediaProfile(
|
||||
@@ -377,7 +375,6 @@ namespace Emby.Dlna.Didl
|
||||
targetHeight,
|
||||
streamInfo.TargetVideoBitDepth,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
@@ -552,7 +549,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
if (mediaSource.RunTimeTicks.HasValue)
|
||||
{
|
||||
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
|
||||
}
|
||||
|
||||
if (filter.Contains("res@size"))
|
||||
@@ -563,7 +560,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
if (size.HasValue)
|
||||
{
|
||||
writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,17 +572,17 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
if (targetChannels.HasValue)
|
||||
{
|
||||
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
|
||||
}
|
||||
|
||||
if (targetSampleRate.HasValue)
|
||||
{
|
||||
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
|
||||
}
|
||||
|
||||
if (targetAudioBitrate.HasValue)
|
||||
{
|
||||
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
|
||||
}
|
||||
|
||||
var mediaProfile = _profile.GetAudioMediaProfile(
|
||||
@@ -602,8 +599,7 @@ namespace Emby.Dlna.Didl
|
||||
? MimeTypes.GetMimeType(filename)
|
||||
: mediaProfile.MimeType;
|
||||
|
||||
var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
|
||||
_profile,
|
||||
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
targetAudioBitrate,
|
||||
@@ -639,7 +635,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
writer.WriteAttributeString("restricted", "1");
|
||||
writer.WriteAttributeString("searchable", "1");
|
||||
writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("childCount", childCount.ToString(_usCulture));
|
||||
|
||||
var clientId = GetClientId(folder, stubType);
|
||||
|
||||
@@ -659,7 +655,7 @@ namespace Emby.Dlna.Didl
|
||||
else
|
||||
{
|
||||
var parent = folder.DisplayParentId;
|
||||
if (parent.Equals(default))
|
||||
if (parent.Equals(Guid.Empty))
|
||||
{
|
||||
writer.WriteAttributeString("parentID", "0");
|
||||
}
|
||||
@@ -731,7 +727,7 @@ namespace Emby.Dlna.Didl
|
||||
{
|
||||
if (item.PremiereDate.HasValue)
|
||||
{
|
||||
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc);
|
||||
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -748,7 +744,7 @@ namespace Emby.Dlna.Didl
|
||||
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
|
||||
}
|
||||
|
||||
if (item is not Folder)
|
||||
if (!(item is Folder))
|
||||
{
|
||||
if (filter.Contains("dc:description"))
|
||||
{
|
||||
@@ -931,11 +927,11 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
if (item.IndexNumber.HasValue)
|
||||
{
|
||||
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
|
||||
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
|
||||
|
||||
if (item is Episode)
|
||||
{
|
||||
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
|
||||
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -978,29 +974,16 @@ namespace Emby.Dlna.Didl
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Remove these default values
|
||||
var albumArtUrlInfo = GetImageUrl(
|
||||
imageInfo,
|
||||
_profile.MaxAlbumArtWidth ?? 10000,
|
||||
_profile.MaxAlbumArtHeight ?? 10000,
|
||||
"jpg");
|
||||
var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
|
||||
|
||||
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
|
||||
if (!string.IsNullOrEmpty(_profile.AlbumArtPn))
|
||||
{
|
||||
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
|
||||
}
|
||||
|
||||
writer.WriteString(albumArtUrlInfo.Url);
|
||||
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
|
||||
writer.WriteString(albumartUrlInfo.url);
|
||||
writer.WriteFullEndElement();
|
||||
|
||||
// TODO: Remove these default values
|
||||
var iconUrlInfo = GetImageUrl(
|
||||
imageInfo,
|
||||
_profile.MaxIconWidth ?? 48,
|
||||
_profile.MaxIconHeight ?? 48,
|
||||
"jpg");
|
||||
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.Url);
|
||||
// TOOD: 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)
|
||||
{
|
||||
@@ -1047,10 +1030,11 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
// Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail
|
||||
// rather than using a larger one when available
|
||||
var width = albumartUrlInfo.Width ?? maxWidth;
|
||||
var height = albumartUrlInfo.Height ?? maxHeight;
|
||||
var width = albumartUrlInfo.width ?? maxWidth;
|
||||
var height = albumartUrlInfo.height ?? maxHeight;
|
||||
|
||||
var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
|
||||
var contentFeatures = new ContentFeatureBuilder(_profile)
|
||||
.BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
|
||||
|
||||
writer.WriteAttributeString(
|
||||
"protocolInfo",
|
||||
@@ -1064,7 +1048,7 @@ namespace Emby.Dlna.Didl
|
||||
"resolution",
|
||||
string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
|
||||
|
||||
writer.WriteString(albumartUrlInfo.Url);
|
||||
writer.WriteString(albumartUrlInfo.url);
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
}
|
||||
@@ -1202,7 +1186,7 @@ namespace Emby.Dlna.Didl
|
||||
return id;
|
||||
}
|
||||
|
||||
private (string Url, int? Width, int? Height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
|
||||
private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
|
||||
{
|
||||
var url = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@@ -1222,7 +1206,8 @@ 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;
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace Emby.Dlna.Didl
|
||||
public Filter(string filter)
|
||||
{
|
||||
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
||||
_fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
_fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public bool Contains(string field)
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
|
||||
{
|
||||
public class StringWriterWithEncoding : StringWriter
|
||||
{
|
||||
private readonly Encoding? _encoding;
|
||||
private readonly Encoding _encoding;
|
||||
|
||||
public StringWriterWithEncoding()
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
#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.Json;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Profiles;
|
||||
using Emby.Dlna.Server;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
@@ -32,9 +32,9 @@ namespace Emby.Dlna
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<DlnaManager> _logger;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
|
||||
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
||||
|
||||
@@ -43,12 +43,14 @@ namespace Emby.Dlna
|
||||
IFileSystem fileSystem,
|
||||
IApplicationPaths appPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IServerApplicationHost appHost)
|
||||
{
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
_logger = loggerFactory.CreateLogger<DlnaManager>();
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
@@ -83,7 +85,8 @@ namespace Emby.Dlna
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
return _profiles.Values
|
||||
var list = _profiles.Values.ToList();
|
||||
return list
|
||||
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Item1.Info.Name)
|
||||
.Select(i => i.Item2)
|
||||
@@ -91,14 +94,12 @@ namespace Emby.Dlna
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile GetDefaultProfile()
|
||||
{
|
||||
return new DefaultProfile();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
|
||||
public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
|
||||
{
|
||||
if (deviceInfo == null)
|
||||
{
|
||||
@@ -108,57 +109,109 @@ namespace Emby.Dlna
|
||||
var profile = GetProfiles()
|
||||
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
|
||||
|
||||
if (profile == null)
|
||||
if (profile != null)
|
||||
{
|
||||
_logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
|
||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||
LogUnmatchedProfile(deviceInfo);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to match a device with a profile.
|
||||
/// Rules:
|
||||
/// - If the profile field has no value, the field matches irregardless of its contents.
|
||||
/// - the profile field can be an exact match, or a reg exp.
|
||||
/// </summary>
|
||||
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
|
||||
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
|
||||
/// <returns><b>True</b> if they match.</returns>
|
||||
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||
private void LogUnmatchedProfile(DeviceIdentification profile)
|
||||
{
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
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.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|
||||
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
@@ -167,8 +220,7 @@ namespace Emby.Dlna
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(IHeaderDictionary headers)
|
||||
public DeviceProfile GetProfile(IHeaderDictionary headers)
|
||||
{
|
||||
if (headers == null)
|
||||
{
|
||||
@@ -176,13 +228,15 @@ namespace Emby.Dlna
|
||||
}
|
||||
|
||||
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
|
||||
if (profile == null)
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
_logger.LogDebug("No matching device profile found. {@Headers}", headers);
|
||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||
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);
|
||||
}
|
||||
|
||||
return profile;
|
||||
@@ -225,31 +279,37 @@ namespace Emby.Dlna
|
||||
{
|
||||
try
|
||||
{
|
||||
return _fileSystem.GetFilePaths(path)
|
||||
var xmlFies = _fileSystem.GetFilePaths(path)
|
||||
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
return xmlFies
|
||||
.Select(i => ParseProfileFile(i, type))
|
||||
.Where(i => i != null)
|
||||
.ToList()!; // We just filtered out all the nulls
|
||||
.ToList();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return Array.Empty<DeviceProfile>();
|
||||
return new List<DeviceProfile>();
|
||||
}
|
||||
}
|
||||
|
||||
private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
|
||||
private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
|
||||
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple))
|
||||
{
|
||||
return profileTuple.Item2;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DeviceProfile profile;
|
||||
|
||||
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
|
||||
var profile = ReserializeProfile(tempProfile);
|
||||
|
||||
profile = ReserializeProfile(tempProfile);
|
||||
|
||||
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -266,20 +326,14 @@ namespace Emby.Dlna
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(string id)
|
||||
public DeviceProfile GetProfile(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return ParseProfileFile(info.Path, info.Info.Type);
|
||||
}
|
||||
@@ -288,14 +342,14 @@ namespace Emby.Dlna
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
return _profiles.Values
|
||||
var list = _profiles.Values.ToList();
|
||||
return list
|
||||
.Select(i => i.Item1)
|
||||
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Info.Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
|
||||
{
|
||||
return GetProfileInfosInternal().Select(i => i.Info);
|
||||
@@ -303,14 +357,17 @@ namespace Emby.Dlna
|
||||
|
||||
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
|
||||
{
|
||||
return new InternalProfileInfo(
|
||||
new DeviceProfileInfo
|
||||
return new InternalProfileInfo
|
||||
{
|
||||
Path = file.FullName,
|
||||
|
||||
Info = new DeviceProfileInfo
|
||||
{
|
||||
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
|
||||
Name = _fileSystem.GetFileNameWithoutExtension(file),
|
||||
Type = type
|
||||
},
|
||||
file.FullName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ExtractSystemProfilesAsync()
|
||||
@@ -330,21 +387,15 @@ namespace Emby.Dlna
|
||||
systemProfilesPath,
|
||||
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
|
||||
|
||||
// The stream should exist as we just got its name from GetManifestResourceNames
|
||||
using (var stream = _assembly.GetManifestResourceStream(name)!)
|
||||
using (var stream = _assembly.GetManifestResourceStream(name))
|
||||
{
|
||||
var length = stream.Length;
|
||||
var fileInfo = _fileSystem.GetFileInfo(path);
|
||||
|
||||
if (!fileInfo.Exists || fileInfo.Length != length)
|
||||
if (!fileInfo.Exists || fileInfo.Length != stream.Length)
|
||||
{
|
||||
Directory.CreateDirectory(systemProfilesPath);
|
||||
|
||||
var fileOptions = AsyncFile.WriteOptions;
|
||||
fileOptions.Mode = FileMode.Create;
|
||||
fileOptions.PreallocationSize = length;
|
||||
var fileStream = new FileStream(path, fileOptions);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||
{
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
@@ -356,7 +407,6 @@ namespace Emby.Dlna
|
||||
Directory.CreateDirectory(UserProfilesPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteProfile(string id)
|
||||
{
|
||||
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
|
||||
@@ -374,7 +424,6 @@ namespace Emby.Dlna
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateProfile(DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
@@ -390,8 +439,7 @@ namespace Emby.Dlna
|
||||
SaveProfile(profile, path, DeviceProfileType.User);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateProfile(string profileId, DeviceProfile profile)
|
||||
public void UpdateProfile(DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
|
||||
@@ -405,7 +453,7 @@ namespace Emby.Dlna
|
||||
throw new ArgumentException("Profile is missing Name");
|
||||
}
|
||||
|
||||
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
|
||||
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
|
||||
var path = Path.Combine(UserProfilesPath, newFilename);
|
||||
@@ -447,53 +495,40 @@ namespace Emby.Dlna
|
||||
return profile;
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(profile, _jsonOptions);
|
||||
var json = _jsonSerializer.SerializeToString(profile);
|
||||
|
||||
// Output can't be null if the input isn't null
|
||||
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
|
||||
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
|
||||
{
|
||||
var profile = GetProfile(headers) ?? GetDefaultProfile();
|
||||
var profile = GetDefaultProfile();
|
||||
|
||||
var serverId = _appHost.SystemId;
|
||||
|
||||
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageStream? GetIcon(string filename)
|
||||
public ImageStream GetIcon(string filename)
|
||||
{
|
||||
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
||||
? ImageFormat.Png
|
||||
: ImageFormat.Jpg;
|
||||
|
||||
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
|
||||
var stream = _assembly.GetManifestResourceStream(resource);
|
||||
if (stream == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ImageStream(stream)
|
||||
return new ImageStream
|
||||
{
|
||||
Format = format
|
||||
Format = format,
|
||||
Stream = _assembly.GetManifestResourceStream(resource)
|
||||
};
|
||||
}
|
||||
|
||||
private class InternalProfileInfo
|
||||
{
|
||||
internal InternalProfileInfo(DeviceProfileInfo info, string path)
|
||||
{
|
||||
Info = info;
|
||||
Path = path;
|
||||
}
|
||||
internal DeviceProfileInfo Info { get; set; }
|
||||
|
||||
internal DeviceProfileInfo Info { get; }
|
||||
|
||||
internal string Path { get; }
|
||||
internal string Path { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,7 +553,7 @@ namespace Emby.Dlna
|
||||
|
||||
private void DumpProfiles()
|
||||
{
|
||||
DeviceProfile[] list = new[]
|
||||
DeviceProfile[] list = new []
|
||||
{
|
||||
new SamsungSmartTvProfile(),
|
||||
new XboxOneProfile(),
|
||||
|
||||
@@ -17,26 +17,24 @@
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Images\logo120.jpg" />
|
||||
<EmbeddedResource Include="Images\logo120.png" />
|
||||
@@ -80,7 +78,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,10 +6,8 @@ namespace Emby.Dlna
|
||||
{
|
||||
public class EventSubscriptionResponse
|
||||
{
|
||||
public EventSubscriptionResponse(string content, string contentType)
|
||||
public EventSubscriptionResponse()
|
||||
{
|
||||
Content = content;
|
||||
ContentType = contentType;
|
||||
Headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -11,7 +9,6 @@ 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;
|
||||
@@ -26,6 +23,8 @@ 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;
|
||||
@@ -50,7 +49,11 @@ namespace Emby.Dlna.Eventing
|
||||
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
||||
}
|
||||
|
||||
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
return new EventSubscriptionResponse
|
||||
{
|
||||
Content = string.Empty,
|
||||
ContentType = "text/plain"
|
||||
};
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
@@ -81,7 +84,9 @@ namespace Emby.Dlna.Eventing
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
// Starts with SECOND-
|
||||
if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
header = header.Split('-')[^1];
|
||||
|
||||
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
@@ -96,15 +101,23 @@ namespace Emby.Dlna.Eventing
|
||||
|
||||
_subscriptions.TryRemove(subscriptionId, out _);
|
||||
|
||||
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
return new EventSubscriptionResponse
|
||||
{
|
||||
Content = string.Empty,
|
||||
ContentType = "text/plain"
|
||||
};
|
||||
}
|
||||
|
||||
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
|
||||
{
|
||||
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
var response = new EventSubscriptionResponse
|
||||
{
|
||||
Content = string.Empty,
|
||||
ContentType = "text/plain"
|
||||
};
|
||||
|
||||
response.Headers["SID"] = subscriptionId;
|
||||
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
|
||||
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -161,7 +174,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(CultureInfo.InvariantCulture));
|
||||
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -7,6 +5,7 @@ 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;
|
||||
@@ -27,9 +26,11 @@ 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
|
||||
{
|
||||
@@ -52,6 +53,7 @@ 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;
|
||||
@@ -124,10 +126,9 @@ namespace Emby.Dlna.Main
|
||||
config);
|
||||
Current = this;
|
||||
|
||||
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
|
||||
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
|
||||
|
||||
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
||||
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
||||
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
|
||||
if (_disabled)
|
||||
{
|
||||
_logger.LogError("The DLNA specification does not support HTTPS.");
|
||||
}
|
||||
@@ -201,8 +202,8 @@ namespace Emby.Dlna.Main
|
||||
{
|
||||
if (_communicationsServer == null)
|
||||
{
|
||||
var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
|
||||
OperatingSystem.IsLinux();
|
||||
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
|
||||
OperatingSystem.Id == OperatingSystemId.Linux;
|
||||
|
||||
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||
{
|
||||
@@ -218,14 +219,16 @@ namespace Emby.Dlna.Main
|
||||
}
|
||||
}
|
||||
|
||||
private void LogMessage(string msg)
|
||||
{
|
||||
_logger.LogDebug(msg);
|
||||
}
|
||||
|
||||
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (communicationsServer != null)
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||
}
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -260,13 +263,9 @@ namespace Emby.Dlna.Main
|
||||
|
||||
try
|
||||
{
|
||||
_publisher = new SsdpDevicePublisher(
|
||||
_communicationsServer,
|
||||
MediaBrowser.Common.System.OperatingSystem.Name,
|
||||
Environment.OSVersion.VersionString,
|
||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
_publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
{
|
||||
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
|
||||
LogFunction = LogMessage,
|
||||
SupportPnpRootDevice = false
|
||||
};
|
||||
|
||||
@@ -311,9 +310,12 @@ namespace Emby.Dlna.Main
|
||||
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
||||
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri);
|
||||
var 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 device = new SsdpRootDevice
|
||||
{
|
||||
@@ -362,7 +364,7 @@ namespace Emby.Dlna.Main
|
||||
guid = text.GetMD5();
|
||||
}
|
||||
|
||||
return guid.ToString("D", CultureInfo.InvariantCulture);
|
||||
return guid.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void SetProperies(SsdpDevice device, string fullDeviceType)
|
||||
@@ -399,6 +401,7 @@ namespace Emby.Dlna.Main
|
||||
_imageProcessor,
|
||||
_deviceDiscovery,
|
||||
_httpClientFactory,
|
||||
_config,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
{
|
||||
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -20,6 +18,8 @@ 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;
|
||||
@@ -69,11 +69,11 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
public TransportState TransportState { get; private set; }
|
||||
|
||||
public bool IsPlaying => TransportState == TransportState.PLAYING;
|
||||
public bool IsPlaying => TransportState == TransportState.Playing;
|
||||
|
||||
public bool IsPaused => TransportState == TransportState.PAUSED_PLAYBACK;
|
||||
public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback;
|
||||
|
||||
public bool IsStopped => TransportState == TransportState.STOPPED;
|
||||
public bool IsStopped => TransportState == TransportState.Stopped;
|
||||
|
||||
public Action OnDeviceUnavailable { get; set; }
|
||||
|
||||
@@ -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,13 +235,7 @@ 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),
|
||||
cancellationToken: cancellationToken)
|
||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
IsMuted = mute;
|
||||
@@ -259,7 +253,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;
|
||||
@@ -276,13 +270,7 @@ 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),
|
||||
cancellationToken: cancellationToken)
|
||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -290,7 +278,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;
|
||||
@@ -303,13 +291,7 @@ 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"),
|
||||
cancellationToken: cancellationToken)
|
||||
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"))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
RestartTimer(true);
|
||||
@@ -323,7 +305,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;
|
||||
@@ -343,21 +325,14 @@ 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,
|
||||
cancellationToken: cancellationToken)
|
||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
||||
await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -368,42 +343,6 @@ 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<string, string>
|
||||
{
|
||||
{ "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))
|
||||
@@ -439,10 +378,6 @@ 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);
|
||||
|
||||
@@ -453,7 +388,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;
|
||||
@@ -461,13 +396,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var service = GetAvTransportService();
|
||||
|
||||
await new SsdpHttpClient(_httpClientFactory)
|
||||
.SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
command.Name,
|
||||
avCommands.BuildPost(command, service.ServiceType, 1),
|
||||
cancellationToken: cancellationToken)
|
||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
RestartTimer(true);
|
||||
@@ -477,7 +406,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;
|
||||
@@ -485,16 +414,10 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var service = GetAvTransportService();
|
||||
|
||||
await new SsdpHttpClient(_httpClientFactory)
|
||||
.SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
command.Name,
|
||||
avCommands.BuildPost(command, service.ServiceType, 1),
|
||||
cancellationToken: cancellationToken)
|
||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
TransportState = TransportState.PAUSED_PLAYBACK;
|
||||
TransportState = TransportState.Paused;
|
||||
|
||||
RestartTimer(true);
|
||||
}
|
||||
@@ -527,7 +450,7 @@ namespace Emby.Dlna.PlayTo
|
||||
if (transportState.HasValue)
|
||||
{
|
||||
// If we're not playing anything no need to get additional data
|
||||
if (transportState.Value == TransportState.STOPPED)
|
||||
if (transportState.Value == TransportState.Stopped)
|
||||
{
|
||||
UpdateMediaInfo(null, transportState.Value);
|
||||
}
|
||||
@@ -535,9 +458,9 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var tuple = await GetPositionInfo(avCommands, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var currentObject = tuple.Track;
|
||||
var currentObject = tuple.Item2;
|
||||
|
||||
if (tuple.Success && currentObject == null)
|
||||
if (tuple.Item1 && currentObject == null)
|
||||
{
|
||||
currentObject = await GetMediaInfo(avCommands, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -556,7 +479,7 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
|
||||
// If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive
|
||||
if (transportState.Value == TransportState.STOPPED)
|
||||
if (transportState.Value == TransportState.Stopped)
|
||||
{
|
||||
RestartTimerInactive();
|
||||
}
|
||||
@@ -605,7 +528,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;
|
||||
@@ -638,7 +561,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return;
|
||||
}
|
||||
|
||||
Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
|
||||
Volume = int.Parse(volumeValue, UsCulture);
|
||||
|
||||
if (Volume > 0)
|
||||
{
|
||||
@@ -655,7 +578,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;
|
||||
@@ -742,10 +665,6 @@ 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,
|
||||
@@ -797,7 +716,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
|
||||
if (command == null)
|
||||
@@ -814,11 +733,6 @@ 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,
|
||||
@@ -840,7 +754,7 @@ namespace Emby.Dlna.PlayTo
|
||||
if (!string.IsNullOrWhiteSpace(duration)
|
||||
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
|
||||
Duration = TimeSpan.Parse(duration, UsCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -852,7 +766,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture);
|
||||
Position = TimeSpan.Parse(position, UsCulture);
|
||||
}
|
||||
|
||||
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
|
||||
@@ -1000,10 +914,6 @@ 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;
|
||||
@@ -1032,10 +942,6 @@ 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;
|
||||
@@ -1067,10 +973,6 @@ 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<string>();
|
||||
|
||||
@@ -1088,7 +990,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)
|
||||
};
|
||||
|
||||
@@ -1179,7 +1081,6 @@ namespace Emby.Dlna.PlayTo
|
||||
return new Device(deviceProperties, httpClientFactory, logger);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private static DeviceIcon CreateIcon(XElement element)
|
||||
{
|
||||
if (element == null)
|
||||
@@ -1187,61 +1088,69 @@ namespace Emby.Dlna.PlayTo
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
|
||||
var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
|
||||
var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
|
||||
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
|
||||
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
|
||||
|
||||
_ = int.TryParse(width, NumberStyles.Integer, CultureInfo.InvariantCulture, out var widthValue);
|
||||
_ = int.TryParse(height, NumberStyles.Integer, CultureInfo.InvariantCulture, out var heightValue);
|
||||
var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
|
||||
var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
|
||||
|
||||
return new DeviceIcon
|
||||
{
|
||||
Depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")) ?? string.Empty,
|
||||
Depth = depth,
|
||||
Height = heightValue,
|
||||
MimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")) ?? string.Empty,
|
||||
Url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")) ?? string.Empty,
|
||||
MimeType = mimeType,
|
||||
Url = url,
|
||||
Width = widthValue
|
||||
};
|
||||
}
|
||||
|
||||
private static DeviceService Create(XElement element)
|
||||
=> new DeviceService()
|
||||
{
|
||||
ControlUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")) ?? string.Empty,
|
||||
EventSubUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")) ?? string.Empty,
|
||||
ScpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")) ?? string.Empty,
|
||||
ServiceId = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")) ?? string.Empty,
|
||||
ServiceType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType")) ?? string.Empty
|
||||
};
|
||||
{
|
||||
var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
|
||||
var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
|
||||
var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
|
||||
var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
|
||||
var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
|
||||
|
||||
private void UpdateMediaInfo(UBaseObject? mediaInfo, TransportState state)
|
||||
return new DeviceService
|
||||
{
|
||||
ControlUrl = controlURL,
|
||||
EventSubUrl = eventSubURL,
|
||||
ScpdUrl = scpdUrl,
|
||||
ServiceId = id,
|
||||
ServiceType = type
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
|
||||
{
|
||||
TransportState = state;
|
||||
|
||||
var previousMediaInfo = CurrentMediaInfo;
|
||||
CurrentMediaInfo = mediaInfo;
|
||||
|
||||
if (mediaInfo == null)
|
||||
if (previousMediaInfo == null && mediaInfo != null)
|
||||
{
|
||||
if (previousMediaInfo != null)
|
||||
{
|
||||
OnPlaybackStop(previousMediaInfo);
|
||||
}
|
||||
}
|
||||
else if (previousMediaInfo == null)
|
||||
{
|
||||
if (state != TransportState.STOPPED)
|
||||
if (state != TransportState.Stopped)
|
||||
{
|
||||
OnPlaybackStart(mediaInfo);
|
||||
}
|
||||
}
|
||||
else if (mediaInfo.Equals(previousMediaInfo))
|
||||
{
|
||||
OnPlaybackProgress(mediaInfo);
|
||||
}
|
||||
else
|
||||
else if (mediaInfo != null && previousMediaInfo != null && !mediaInfo.Equals(previousMediaInfo))
|
||||
{
|
||||
OnMediaChanged(previousMediaInfo, mediaInfo);
|
||||
}
|
||||
else if (mediaInfo == null && previousMediaInfo != null)
|
||||
{
|
||||
OnPlaybackStop(previousMediaInfo);
|
||||
}
|
||||
else if (mediaInfo != null && mediaInfo.Equals(previousMediaInfo))
|
||||
{
|
||||
OnPlaybackProgress(mediaInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlaybackStart(UBaseObject mediaInfo)
|
||||
@@ -1251,7 +1160,10 @@ namespace Emby.Dlna.PlayTo
|
||||
return;
|
||||
}
|
||||
|
||||
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
|
||||
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
|
||||
{
|
||||
MediaInfo = mediaInfo
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPlaybackProgress(UBaseObject mediaInfo)
|
||||
@@ -1261,17 +1173,27 @@ namespace Emby.Dlna.PlayTo
|
||||
return;
|
||||
}
|
||||
|
||||
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo));
|
||||
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
|
||||
{
|
||||
MediaInfo = mediaInfo
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPlaybackStop(UBaseObject mediaInfo)
|
||||
{
|
||||
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo));
|
||||
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
|
||||
{
|
||||
MediaInfo = mediaInfo
|
||||
});
|
||||
}
|
||||
|
||||
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
|
||||
{
|
||||
MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia));
|
||||
MediaChanged?.Invoke(this, new MediaChangedEventArgs
|
||||
{
|
||||
OldMediaInfo = old,
|
||||
NewMediaInfo = newMedia
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -6,12 +6,6 @@ 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; }
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -30,6 +28,8 @@ 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,22 +102,6 @@ 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
|
||||
@@ -148,7 +132,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
||||
{
|
||||
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -172,15 +156,6 @@ 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.Equals(streamInfo.ItemId));
|
||||
if (currentItemIndex >= 0)
|
||||
{
|
||||
_currentPlaylistIndex = currentItemIndex;
|
||||
}
|
||||
|
||||
await SendNextTrackMessage(currentItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -210,9 +185,9 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var duration = mediaSource == null
|
||||
? _device.Duration?.Ticks
|
||||
: mediaSource.RunTimeTicks;
|
||||
var duration = mediaSource == null ?
|
||||
(_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
|
||||
mediaSource.RunTimeTicks;
|
||||
|
||||
var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
|
||||
|
||||
@@ -349,9 +324,7 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
_logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
|
||||
|
||||
var user = command.ControllingUserId.Equals(default)
|
||||
? null :
|
||||
_userManager.GetUserById(command.ControllingUserId);
|
||||
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
|
||||
|
||||
var items = new List<BaseItem>();
|
||||
foreach (var id in command.ItemIds)
|
||||
@@ -394,7 +367,7 @@ namespace Emby.Dlna.PlayTo
|
||||
_playlist.AddRange(playlist);
|
||||
}
|
||||
|
||||
if (!command.ControllingUserId.Equals(default))
|
||||
if (!command.ControllingUserId.Equals(Guid.Empty))
|
||||
{
|
||||
_sessionManager.LogSessionActivity(
|
||||
_session.Client,
|
||||
@@ -448,17 +421,10 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
if (info.Item != null && !EnableClientSideSeek(info))
|
||||
{
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null;
|
||||
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).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -533,8 +499,8 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
||||
{
|
||||
return ContentFeatureBuilder.BuildAudioHeader(
|
||||
profile,
|
||||
return new ContentFeatureBuilder(profile)
|
||||
.BuildAudioHeader(
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioBitrate,
|
||||
@@ -548,8 +514,8 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
if (streamInfo.MediaType == DlnaProfileType.Video)
|
||||
{
|
||||
var list = ContentFeatureBuilder.BuildVideoHeader(
|
||||
profile,
|
||||
var list = new ContentFeatureBuilder(profile)
|
||||
.BuildVideoHeader(
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
@@ -561,7 +527,6 @@ namespace Emby.Dlna.PlayTo
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
@@ -574,7 +539,7 @@ namespace Emby.Dlna.PlayTo
|
||||
streamInfo.TargetVideoCodecTag,
|
||||
streamInfo.IsTargetAVC);
|
||||
|
||||
return list.FirstOrDefault();
|
||||
return list.Count == 0 ? null : list[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -658,9 +623,6 @@ 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).ConfigureAwait(false);
|
||||
|
||||
var streamInfo = currentitem.StreamInfo;
|
||||
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
||||
{
|
||||
@@ -719,7 +681,7 @@ namespace Emby.Dlna.PlayTo
|
||||
case GeneralCommandType.SetAudioStreamIndex:
|
||||
if (command.Arguments.TryGetValue("Index", out string index))
|
||||
{
|
||||
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
|
||||
{
|
||||
return SetAudioStreamIndex(val);
|
||||
}
|
||||
@@ -731,7 +693,7 @@ namespace Emby.Dlna.PlayTo
|
||||
case GeneralCommandType.SetSubtitleStreamIndex:
|
||||
if (command.Arguments.TryGetValue("Index", out index))
|
||||
{
|
||||
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
|
||||
{
|
||||
return SetSubtitleStreamIndex(val);
|
||||
}
|
||||
@@ -743,7 +705,7 @@ namespace Emby.Dlna.PlayTo
|
||||
case GeneralCommandType.SetVolume:
|
||||
if (command.Arguments.TryGetValue("Volume", out string vol))
|
||||
{
|
||||
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
|
||||
if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
|
||||
{
|
||||
return _device.SetVolume(volume, cancellationToken);
|
||||
}
|
||||
@@ -769,17 +731,11 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var newPosition = GetProgressPositionTicks(info) ?? 0;
|
||||
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null;
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, 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).ConfigureAwait(false);
|
||||
|
||||
if (EnableClientSideSeek(newItem.StreamInfo))
|
||||
{
|
||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -800,17 +756,11 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var newPosition = GetProgressPositionTicks(info) ?? 0;
|
||||
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null;
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex);
|
||||
|
||||
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).ConfigureAwait(false);
|
||||
|
||||
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
||||
{
|
||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -825,9 +775,9 @@ namespace Emby.Dlna.PlayTo
|
||||
const int Interval = 500;
|
||||
|
||||
var currentWait = 0;
|
||||
while (_device.TransportState != TransportState.PLAYING && currentWait < MaxWait)
|
||||
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
|
||||
{
|
||||
await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
|
||||
await Task.Delay(Interval).ConfigureAwait(false);
|
||||
currentWait += Interval;
|
||||
}
|
||||
|
||||
@@ -892,7 +842,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
private class StreamParams
|
||||
{
|
||||
private MediaSourceInfo _mediaSource;
|
||||
private MediaSourceInfo mediaSource;
|
||||
private IMediaSourceManager _mediaSourceManager;
|
||||
|
||||
public Guid ItemId { get; set; }
|
||||
@@ -917,22 +867,24 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_mediaSource != null)
|
||||
if (mediaSource != null)
|
||||
{
|
||||
return _mediaSource;
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
if (Item is not IHasMediaSources)
|
||||
var hasMediaSources = Item as IHasMediaSources;
|
||||
|
||||
if (hasMediaSources == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_mediaSourceManager != null)
|
||||
{
|
||||
_mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
|
||||
mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return _mediaSource;
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
private static Guid GetItemId(string url)
|
||||
@@ -958,7 +910,7 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
|
||||
@@ -973,7 +925,7 @@ namespace Emby.Dlna.PlayTo
|
||||
ItemId = GetItemId(url)
|
||||
};
|
||||
|
||||
if (request.ItemId.Equals(default))
|
||||
if (request.ItemId.Equals(Guid.Empty))
|
||||
{
|
||||
return request;
|
||||
}
|
||||
@@ -991,7 +943,11 @@ namespace Emby.Dlna.PlayTo
|
||||
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
||||
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
||||
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
||||
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 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.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
||||
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
||||
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -11,6 +9,7 @@ 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;
|
||||
@@ -34,6 +33,7 @@ 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, 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, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
@@ -56,6 +56,7 @@ namespace Emby.Dlna.PlayTo
|
||||
_imageProcessor = imageProcessor;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_config = config;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
@@ -170,26 +171,19 @@ namespace Emby.Dlna.PlayTo
|
||||
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var sessionInfo = await _sessionManager
|
||||
.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
|
||||
.ConfigureAwait(false);
|
||||
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
|
||||
|
||||
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().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.RemoteIpAddress);
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
|
||||
|
||||
controller = new PlayToController(
|
||||
sessionInfo,
|
||||
|
||||
@@ -6,11 +6,6 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackProgressEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackProgressEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackStartEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackStartEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackStoppedEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#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;
|
||||
@@ -20,6 +19,8 @@ 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)
|
||||
@@ -43,13 +44,11 @@ namespace Emby.Dlna.PlayTo
|
||||
header,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return XDocument.Parse(
|
||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
||||
LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
|
||||
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||
@@ -78,15 +77,14 @@ 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(CultureInfo.InvariantCulture));
|
||||
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
|
||||
options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
|
||||
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
|
||||
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
|
||||
options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
|
||||
options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
|
||||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
|
||||
@@ -95,19 +93,11 @@ 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);
|
||||
try
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return XDocument.Parse(
|
||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
||||
LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
||||
|
||||
@@ -13,10 +13,12 @@ namespace Emby.Dlna.PlayTo
|
||||
public class TransportCommands
|
||||
{
|
||||
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
|
||||
private List<StateVariable> _stateVariables = new List<StateVariable>();
|
||||
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
|
||||
|
||||
public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
|
||||
public List<StateVariable> StateVariables => _stateVariables;
|
||||
|
||||
public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
|
||||
public List<ServiceAction> ServiceActions => _serviceActions;
|
||||
|
||||
public static TransportCommands Create(XDocument document)
|
||||
{
|
||||
@@ -46,7 +48,7 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var serviceAction = new ServiceAction
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||
};
|
||||
|
||||
var argumentList = serviceAction.ArgumentList;
|
||||
@@ -68,9 +70,9 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
return new Argument
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
|
||||
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
|
||||
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,8 +91,8 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
return new StateVariable
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
|
||||
AllowedValues = allowedValues
|
||||
};
|
||||
}
|
||||
@@ -166,7 +168,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));
|
||||
|
||||
@@ -175,7 +177,7 @@ namespace Emby.Dlna.PlayTo
|
||||
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
|
||||
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType, sendValue);
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1602
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
/// <summary>
|
||||
/// Core of the AVTransport service. It defines the conceptually top-
|
||||
/// level state of the transport, for example, whether it is playing, recording, etc.
|
||||
/// </summary>
|
||||
public enum TransportState
|
||||
{
|
||||
STOPPED,
|
||||
PLAYING,
|
||||
TRANSITIONING,
|
||||
PAUSED_PLAYBACK
|
||||
Stopped,
|
||||
Playing,
|
||||
Transitioning,
|
||||
PausedPlayback,
|
||||
Paused
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
@@ -12,7 +10,6 @@ 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:*";
|
||||
@@ -167,7 +164,8 @@ namespace Emby.Dlna.Profiles
|
||||
|
||||
public void AddXmlRootAttribute(string name, string value)
|
||||
{
|
||||
var list = XmlRootAttributes.ToList();
|
||||
var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
|
||||
var list = atts.ToList();
|
||||
|
||||
list.Add(new XmlAttribute
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
FriendlyName = @"KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*",
|
||||
FriendlyName = @"KDL-\d{2}[EHLNPB]X\d[01]\d.*",
|
||||
Manufacturer = "Sony",
|
||||
|
||||
Headers = new[]
|
||||
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
||||
new HttpHeaderInfo
|
||||
{
|
||||
Name = "X-AV-Client-Info",
|
||||
Value = @".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*",
|
||||
Value = @".*KDL-\d{2}[EHLNPB]X\d[01]\d.*",
|
||||
Match = HeaderMatchType.Regex
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
FriendlyName = @"KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*",
|
||||
FriendlyName = @"KDL-\d{2}([A-Z]X\d2\d|CX400).*",
|
||||
Manufacturer = "Sony",
|
||||
|
||||
Headers = new[]
|
||||
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
||||
new HttpHeaderInfo
|
||||
{
|
||||
Name = "X-AV-Client-Info",
|
||||
Value = @".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*",
|
||||
Value = @".*KDL-\d{2}([A-Z]X\d2\d|CX400).*",
|
||||
Match = HeaderMatchType.Regex
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
FriendlyName = @"KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*",
|
||||
FriendlyName = @"KDL-\d{2}[A-Z]X\d5(\d|G).*",
|
||||
Manufacturer = "Sony",
|
||||
|
||||
Headers = new[]
|
||||
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
||||
new HttpHeaderInfo
|
||||
{
|
||||
Name = "X-AV-Client-Info",
|
||||
Value = @".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*",
|
||||
Value = @".*KDL-\d{2}[A-Z]X\d5(\d|G).*",
|
||||
Match = HeaderMatchType.Regex
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
FriendlyName = @"KDL-[0-9]{2}[WR][5689][0-9]{2}A.*",
|
||||
FriendlyName = @"KDL-\d{2}[WR][5689]\d{2}A.*",
|
||||
Manufacturer = "Sony",
|
||||
|
||||
Headers = new[]
|
||||
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
||||
new HttpHeaderInfo
|
||||
{
|
||||
Name = "X-AV-Client-Info",
|
||||
Value = @".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*",
|
||||
Value = @".*KDL-\d{2}[WR][5689]\d{2}A.*",
|
||||
Match = HeaderMatchType.Regex
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
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).*",
|
||||
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).*",
|
||||
Manufacturer = "Sony",
|
||||
|
||||
Headers = new[]
|
||||
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
||||
new HttpHeaderInfo
|
||||
{
|
||||
Name = "X-AV-Client-Info",
|
||||
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).*",
|
||||
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).*",
|
||||
Match = HeaderMatchType.Regex
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2010)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*</FriendlyName>
|
||||
<FriendlyName>KDL-\d{2}[EHLNPB]X\d[01]\d.*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*" match="Regex" />
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[EHLNPB]X\d[01]\d.*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2011)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*</FriendlyName>
|
||||
<FriendlyName>KDL-\d{2}([A-Z]X\d2\d|CX400).*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*" match="Regex" />
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}([A-Z]X\d2\d|CX400).*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2012)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*</FriendlyName>
|
||||
<FriendlyName>KDL-\d{2}[A-Z]X\d5(\d|G).*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*" match="Regex" />
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[A-Z]X\d5(\d|G).*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2013)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>KDL-[0-9]{2}[WR][5689][0-9]{2}A.*</FriendlyName>
|
||||
<FriendlyName>KDL-\d{2}[WR][5689]\d{2}A.*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*" match="Regex" />
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[WR][5689]\d{2}A.*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2014)</Name>
|
||||
<Identification>
|
||||
<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).*</FriendlyName>
|
||||
<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>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" 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="Regex" />
|
||||
<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).*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
|
||||
@@ -15,6 +15,7 @@ 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;
|
||||
@@ -189,16 +190,16 @@ namespace Emby.Dlna.Server
|
||||
builder.Append("<icon>");
|
||||
|
||||
builder.Append("<mimetype>")
|
||||
.Append(SecurityElement.Escape(icon.MimeType))
|
||||
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
|
||||
.Append("</mimetype>");
|
||||
builder.Append("<width>")
|
||||
.Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
|
||||
.Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
|
||||
.Append("</width>");
|
||||
builder.Append("<height>")
|
||||
.Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
|
||||
.Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
|
||||
.Append("</height>");
|
||||
builder.Append("<depth>")
|
||||
.Append(SecurityElement.Escape(icon.Depth))
|
||||
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
|
||||
.Append("</depth>");
|
||||
builder.Append("<url>")
|
||||
.Append(BuildUrl(icon.Url))
|
||||
@@ -219,10 +220,10 @@ namespace Emby.Dlna.Server
|
||||
builder.Append("<service>");
|
||||
|
||||
builder.Append("<serviceType>")
|
||||
.Append(SecurityElement.Escape(service.ServiceType))
|
||||
.Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
|
||||
.Append("</serviceType>");
|
||||
builder.Append("<serviceId>")
|
||||
.Append(SecurityElement.Escape(service.ServiceId))
|
||||
.Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
|
||||
.Append("</serviceId>");
|
||||
builder.Append("<SCPDURL>")
|
||||
.Append(BuildUrl(service.ScpdUrl))
|
||||
|
||||
@@ -7,8 +7,8 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Emby.Dlna.Didl;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.Service
|
||||
@@ -47,9 +47,9 @@ namespace Emby.Dlna.Service
|
||||
|
||||
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
|
||||
{
|
||||
ControlRequestInfo requestInfo;
|
||||
ControlRequestInfo requestInfo = null;
|
||||
|
||||
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
|
||||
using (var streamReader = new StreamReader(request.InputXml))
|
||||
{
|
||||
var readerSettings = new XmlReaderSettings()
|
||||
{
|
||||
@@ -64,13 +64,8 @@ namespace Emby.Dlna.Service
|
||||
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
|
||||
Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
|
||||
|
||||
return CreateControlResponse(requestInfo);
|
||||
}
|
||||
|
||||
private ControlResponse CreateControlResponse(ControlRequestInfo requestInfo)
|
||||
{
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
Encoding = Encoding.UTF8,
|
||||
@@ -100,7 +95,11 @@ namespace Emby.Dlna.Service
|
||||
|
||||
var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
|
||||
|
||||
var controlResponse = new ControlResponse(xml, true);
|
||||
var controlResponse = new ControlResponse
|
||||
{
|
||||
Xml = xml,
|
||||
IsSuccessful = true
|
||||
};
|
||||
|
||||
controlResponse.Headers.Add("EXT", string.Empty);
|
||||
|
||||
@@ -117,19 +116,29 @@ namespace Emby.Dlna.Service
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
if (string.Equals(reader.LocalName, "Body", StringComparison.Ordinal))
|
||||
switch (reader.LocalName)
|
||||
{
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
case "Body":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using var subReader = reader.ReadSubtree();
|
||||
return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
using var subReader = reader.ReadSubtree();
|
||||
return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
await reader.SkipAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await reader.SkipAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -142,7 +151,7 @@ namespace Emby.Dlna.Service
|
||||
|
||||
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
||||
{
|
||||
string? namespaceURI = null, localName = null;
|
||||
string namespaceURI = null, localName = null;
|
||||
|
||||
await reader.MoveToContentAsync().ConfigureAwait(false);
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
@@ -155,17 +164,17 @@ namespace Emby.Dlna.Service
|
||||
localName = reader.LocalName;
|
||||
namespaceURI = reader.NamespaceURI;
|
||||
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
var result = new ControlRequestInfo(localName, namespaceURI);
|
||||
using var subReader = reader.ReadSubtree();
|
||||
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -201,7 +210,7 @@ namespace Emby.Dlna.Service
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter);
|
||||
protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
|
||||
|
||||
private void LogRequest(ControlRequest request)
|
||||
{
|
||||
|
||||
@@ -23,14 +23,14 @@ namespace Emby.Dlna.Service
|
||||
return EventManager.CancelEventSubscription(subscriptionId);
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string timeoutString, string callbackUrl)
|
||||
{
|
||||
return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl);
|
||||
return EventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callbackUrl);
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string timeoutString, string callbackUrl)
|
||||
{
|
||||
return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl);
|
||||
return EventManager.CreateEventSubscription(notificationType, timeoutString, callbackUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,11 @@ namespace Emby.Dlna.Service
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
|
||||
return new ControlResponse(builder.ToString(), false);
|
||||
return new ControlResponse
|
||||
{
|
||||
Xml = builder.ToString(),
|
||||
IsSuccessful = false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Emby.Dlna.Service
|
||||
builder.Append("<action>");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(item.Name))
|
||||
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
|
||||
.Append("</name>");
|
||||
|
||||
builder.Append("<argumentList>");
|
||||
@@ -48,13 +48,13 @@ namespace Emby.Dlna.Service
|
||||
builder.Append("<argument>");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(argument.Name))
|
||||
.Append(SecurityElement.Escape(argument.Name ?? string.Empty))
|
||||
.Append("</name>");
|
||||
builder.Append("<direction>")
|
||||
.Append(SecurityElement.Escape(argument.Direction))
|
||||
.Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
|
||||
.Append("</direction>");
|
||||
builder.Append("<relatedStateVariable>")
|
||||
.Append(SecurityElement.Escape(argument.RelatedStateVariable))
|
||||
.Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
|
||||
.Append("</relatedStateVariable>");
|
||||
|
||||
builder.Append("</argument>");
|
||||
@@ -81,10 +81,10 @@ namespace Emby.Dlna.Service
|
||||
.Append("\">");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(item.Name))
|
||||
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
|
||||
.Append("</name>");
|
||||
builder.Append("<dataType>")
|
||||
.Append(SecurityElement.Escape(item.DataType))
|
||||
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
|
||||
.Append("</dataType>");
|
||||
|
||||
if (item.AllowedValues.Count > 0)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -71,7 +69,7 @@ namespace Emby.Dlna.Ssdp
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
|
||||
if (_listenerCount > 0 && _deviceLocator == null)
|
||||
{
|
||||
_deviceLocator = new SsdpDeviceLocator(_commsServer);
|
||||
|
||||
@@ -106,7 +104,7 @@ namespace Emby.Dlna.Ssdp
|
||||
{
|
||||
Location = e.DiscoveredDevice.DescriptionLocation,
|
||||
Headers = headers,
|
||||
RemoteIpAddress = e.RemoteIpAddress
|
||||
LocalIpAddress = e.LocalIpAddress
|
||||
});
|
||||
|
||||
DeviceDiscoveredInternal?.Invoke(this, args);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -27,13 +25,14 @@
|
||||
|
||||
<!-- Code analysers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -3,8 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
@@ -27,7 +25,7 @@ namespace Emby.Drawing
|
||||
public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||
{
|
||||
// Increment this when there's a change requiring caches to be invalidated
|
||||
private const char Version = '3';
|
||||
private const string Version = "3";
|
||||
|
||||
private static readonly HashSet<string> _transparentImageTypes
|
||||
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
|
||||
@@ -102,7 +100,8 @@ namespace Emby.Drawing
|
||||
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
|
||||
{
|
||||
var file = await ProcessImage(options).ConfigureAwait(false);
|
||||
using (var fileStream = AsyncFile.OpenRead(file.Path))
|
||||
|
||||
using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
|
||||
{
|
||||
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
||||
}
|
||||
@@ -117,7 +116,7 @@ namespace Emby.Drawing
|
||||
=> _transparentImageTypes.Contains(Path.GetExtension(path));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
|
||||
public async Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
|
||||
{
|
||||
ItemImageInfo originalImage = options.Image;
|
||||
BaseItem item = options.Item;
|
||||
@@ -130,22 +129,20 @@ namespace Emby.Drawing
|
||||
originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height);
|
||||
}
|
||||
|
||||
var mimeType = MimeTypes.GetMimeType(originalImagePath);
|
||||
if (!_imageEncoder.SupportsImageEncoding)
|
||||
{
|
||||
return (originalImagePath, mimeType, dateModified);
|
||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||
}
|
||||
|
||||
var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
|
||||
originalImagePath = supportedImageInfo.Path;
|
||||
originalImagePath = supportedImageInfo.path;
|
||||
|
||||
// Original file doesn't exist, or original file is gif.
|
||||
if (!File.Exists(originalImagePath) || string.Equals(mimeType, MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase))
|
||||
if (!File.Exists(originalImagePath))
|
||||
{
|
||||
return (originalImagePath, mimeType, dateModified);
|
||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||
}
|
||||
|
||||
dateModified = supportedImageInfo.DateModified;
|
||||
dateModified = supportedImageInfo.dateModified;
|
||||
bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
|
||||
|
||||
bool autoOrient = false;
|
||||
@@ -174,31 +171,21 @@ 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,
|
||||
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);
|
||||
string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, 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))
|
||||
@@ -245,7 +232,7 @@ namespace Emby.Drawing
|
||||
return ImageFormat.Jpg;
|
||||
}
|
||||
|
||||
private string GetMimeType(ImageFormat format, string path)
|
||||
private string? GetMimeType(ImageFormat format, string path)
|
||||
=> format switch
|
||||
{
|
||||
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
|
||||
@@ -259,111 +246,48 @@ namespace Emby.Drawing
|
||||
/// <summary>
|
||||
/// Gets the cache file path based on a set of parameters.
|
||||
/// </summary>
|
||||
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)
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
var filename = originalPath
|
||||
+ "width=" + outputSize.Width
|
||||
+ "height=" + outputSize.Height
|
||||
+ "quality=" + quality
|
||||
+ "datemodified=" + dateModified.Ticks
|
||||
+ "f=" + format;
|
||||
|
||||
if (addPlayedIndicator)
|
||||
{
|
||||
filename.Append(",pl=true");
|
||||
filename += "pl=true";
|
||||
}
|
||||
|
||||
if (percentPlayed > 0)
|
||||
{
|
||||
filename.Append(",p=");
|
||||
filename.Append(percentPlayed);
|
||||
filename += "p=" + percentPlayed;
|
||||
}
|
||||
|
||||
if (unwatchedCount.HasValue)
|
||||
{
|
||||
filename.Append(",p=");
|
||||
filename.Append(unwatchedCount.Value);
|
||||
filename += "p=" + unwatchedCount.Value;
|
||||
}
|
||||
|
||||
if (blur.HasValue)
|
||||
{
|
||||
filename.Append(",blur=");
|
||||
filename.Append(blur.Value);
|
||||
filename += "blur=" + blur.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(backgroundColor))
|
||||
{
|
||||
filename.Append(",b=");
|
||||
filename.Append(backgroundColor);
|
||||
filename += "b=" + backgroundColor;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(foregroundLayer))
|
||||
{
|
||||
filename.Append(",fl=");
|
||||
filename.Append(foregroundLayer);
|
||||
filename += "fl=" + foregroundLayer;
|
||||
}
|
||||
|
||||
filename.Append(",v=");
|
||||
filename.Append(Version);
|
||||
filename += "v=" + Version;
|
||||
|
||||
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
|
||||
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -395,13 +319,7 @@ namespace Emby.Drawing
|
||||
public string GetImageBlurHash(string path)
|
||||
{
|
||||
var size = GetImageDimensions(path);
|
||||
return GetImageBlurHash(path, size);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetImageBlurHash(string path, ImageDimensions imageDimensions)
|
||||
{
|
||||
if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0)
|
||||
if (size.Width <= 0 || size.Height <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
@@ -409,8 +327,8 @@ namespace Emby.Drawing
|
||||
// We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
|
||||
// One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
|
||||
// See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
|
||||
float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height);
|
||||
float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width;
|
||||
float xCompF = MathF.Sqrt(16.0f * size.Width / size.Height);
|
||||
float yCompF = xCompF * size.Height / size.Width;
|
||||
|
||||
int xComp = Math.Min((int)xCompF + 1, 9);
|
||||
int yComp = Math.Min((int)yCompF + 1, 9);
|
||||
@@ -434,57 +352,53 @@ namespace Emby.Drawing
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
|
||||
private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
|
||||
{
|
||||
var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString();
|
||||
var inputFormat = Path.GetExtension(originalImagePath)
|
||||
.TrimStart('.')
|
||||
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// These are just jpg files renamed as tbn
|
||||
if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult((originalImagePath, dateModified));
|
||||
return (originalImagePath, dateModified);
|
||||
}
|
||||
|
||||
// TODO _mediaEncoder.ConvertImage is not implemented
|
||||
// if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
//
|
||||
// string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
||||
// var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
||||
//
|
||||
// var file = _fileSystem.GetFileInfo(outputPath);
|
||||
// if (!file.Exists)
|
||||
// {
|
||||
// await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
||||
// dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// dateModified = file.LastWriteTimeUtc;
|
||||
// }
|
||||
//
|
||||
// originalImagePath = outputPath;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
||||
// }
|
||||
// }
|
||||
if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
||||
{
|
||||
try
|
||||
{
|
||||
string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
return Task.FromResult((originalImagePath, dateModified));
|
||||
string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
||||
var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
||||
|
||||
var file = _fileSystem.GetFileInfo(outputPath);
|
||||
if (!file.Exists)
|
||||
{
|
||||
await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
||||
dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
dateModified = file.LastWriteTimeUtc;
|
||||
}
|
||||
|
||||
originalImagePath = outputPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
||||
}
|
||||
}
|
||||
|
||||
return (originalImagePath, dateModified);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Emby.Drawing
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
|
||||
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -43,12 +43,6 @@ namespace Emby.Drawing
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetImageBlurHash(int xComp, int yComp, string path)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
/// <returns>True if file at path is audio file.</returns>
|
||||
public static bool IsAudioFile(string path, NamingOptions options)
|
||||
{
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||
var extension = Path.GetExtension(path);
|
||||
return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ namespace Emby.Naming.AudioBook
|
||||
/// <param name="files">List of files composing the actual audiobook.</param>
|
||||
/// <param name="extras">List of extra files.</param>
|
||||
/// <param name="alternateVersions">Alternative version of files.</param>
|
||||
public AudioBookInfo(string name, int? year, IReadOnlyList<AudioBookFileInfo> files, IReadOnlyList<AudioBookFileInfo> extras, IReadOnlyList<AudioBookFileInfo> alternateVersions)
|
||||
public AudioBookInfo(string name, int? year, List<AudioBookFileInfo>? files, List<AudioBookFileInfo>? extras, List<AudioBookFileInfo>? alternateVersions)
|
||||
{
|
||||
Name = name;
|
||||
Year = year;
|
||||
Files = files;
|
||||
Extras = extras;
|
||||
AlternateVersions = alternateVersions;
|
||||
Files = files ?? new List<AudioBookFileInfo>();
|
||||
Extras = extras ?? new List<AudioBookFileInfo>();
|
||||
AlternateVersions = alternateVersions ?? new List<AudioBookFileInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -39,18 +39,18 @@ namespace Emby.Naming.AudioBook
|
||||
/// Gets or sets the files.
|
||||
/// </summary>
|
||||
/// <value>The files.</value>
|
||||
public IReadOnlyList<AudioBookFileInfo> Files { get; set; }
|
||||
public List<AudioBookFileInfo> Files { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extras.
|
||||
/// </summary>
|
||||
/// <value>The extras.</value>
|
||||
public IReadOnlyList<AudioBookFileInfo> Extras { get; set; }
|
||||
public List<AudioBookFileInfo> Extras { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alternate versions.
|
||||
/// </summary>
|
||||
/// <value>The alternate versions.</value>
|
||||
public IReadOnlyList<AudioBookFileInfo> AlternateVersions { get; set; }
|
||||
public List<AudioBookFileInfo> AlternateVersions { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace Emby.Naming.AudioBook
|
||||
public class AudioBookListResolver
|
||||
{
|
||||
private readonly NamingOptions _options;
|
||||
private readonly AudioBookResolver _audioBookResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
|
||||
@@ -23,7 +22,6 @@ namespace Emby.Naming.AudioBook
|
||||
public AudioBookListResolver(NamingOptions options)
|
||||
{
|
||||
_options = options;
|
||||
_audioBookResolver = new AudioBookResolver(_options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -33,18 +31,21 @@ namespace Emby.Naming.AudioBook
|
||||
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
|
||||
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> 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<AudioBookFileInfo>()
|
||||
.ToList();
|
||||
|
||||
var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
|
||||
var stackResult = new StackResolver(_options)
|
||||
.ResolveAudioBooks(audiobookFileInfos);
|
||||
|
||||
foreach (var stack in stackResult)
|
||||
{
|
||||
var stackFiles = stack.Files
|
||||
.Select(i => _audioBookResolver.Resolve(i))
|
||||
.Select(i => audioBookResolver.Resolve(i))
|
||||
.OfType<AudioBookFileInfo>()
|
||||
.ToList();
|
||||
|
||||
@@ -72,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)
|
||||
{
|
||||
@@ -86,7 +87,7 @@ namespace Emby.Naming.AudioBook
|
||||
foreach (var audioFile in group)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
|
||||
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
|
||||
if (name.Equals("audiobook") ||
|
||||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
||||
@@ -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, StringComparison.OrdinalIgnoreCase))
|
||||
if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#pragma warning disable CA1819
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Video;
|
||||
@@ -23,60 +20,47 @@ namespace Emby.Naming.Common
|
||||
{
|
||||
VideoFileExtensions = new[]
|
||||
{
|
||||
".001",
|
||||
".3g2",
|
||||
".3gp",
|
||||
".amv",
|
||||
".asf",
|
||||
".asx",
|
||||
".avi",
|
||||
".bin",
|
||||
".bivx",
|
||||
".divx",
|
||||
".dv",
|
||||
".dvr-ms",
|
||||
".f4v",
|
||||
".fli",
|
||||
".flv",
|
||||
".ifo",
|
||||
".img",
|
||||
".iso",
|
||||
".m2t",
|
||||
".m2ts",
|
||||
".m2v",
|
||||
".m4v",
|
||||
".mkv",
|
||||
".mk3d",
|
||||
".mov",
|
||||
".mp4",
|
||||
".mpe",
|
||||
".mpeg",
|
||||
".mpg",
|
||||
".mts",
|
||||
".mxf",
|
||||
".nrg",
|
||||
".3gp",
|
||||
".nsv",
|
||||
".nuv",
|
||||
".ogg",
|
||||
".ogm",
|
||||
".ogv",
|
||||
".pva",
|
||||
".qt",
|
||||
".rec",
|
||||
".rm",
|
||||
".rmvb",
|
||||
".strm",
|
||||
".svq3",
|
||||
".tp",
|
||||
".ts",
|
||||
".ty",
|
||||
".viv",
|
||||
".strm",
|
||||
".rm",
|
||||
".rmvb",
|
||||
".ifo",
|
||||
".mov",
|
||||
".qt",
|
||||
".divx",
|
||||
".xvid",
|
||||
".bivx",
|
||||
".vob",
|
||||
".vp3",
|
||||
".webm",
|
||||
".nrg",
|
||||
".img",
|
||||
".iso",
|
||||
".pva",
|
||||
".wmv",
|
||||
".wtv",
|
||||
".xvid"
|
||||
".asf",
|
||||
".asx",
|
||||
".ogm",
|
||||
".m2v",
|
||||
".avi",
|
||||
".bin",
|
||||
".dvr-ms",
|
||||
".mpg",
|
||||
".mpeg",
|
||||
".mp4",
|
||||
".mkv",
|
||||
".avc",
|
||||
".vp3",
|
||||
".svq3",
|
||||
".nuv",
|
||||
".viv",
|
||||
".dv",
|
||||
".fli",
|
||||
".flv",
|
||||
".001",
|
||||
".tp"
|
||||
};
|
||||
|
||||
VideoFlagDelimiters = new[]
|
||||
@@ -138,11 +122,11 @@ namespace Emby.Naming.Common
|
||||
token: "DSR")
|
||||
};
|
||||
|
||||
VideoFileStackingRules = new[]
|
||||
VideoFileStackingExpressions = new[]
|
||||
{
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
|
||||
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
||||
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
||||
"(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
|
||||
};
|
||||
|
||||
CleanDateTimes = new[]
|
||||
@@ -153,29 +137,38 @@ namespace Emby.Naming.Common
|
||||
|
||||
CleanStrings = new[]
|
||||
{
|
||||
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](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|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
||||
@"^(?<cleaned>.+?)(\[.*\])",
|
||||
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
||||
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
||||
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\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|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
||||
@"(\[.*\])"
|
||||
};
|
||||
|
||||
SubtitleFileExtensions = new[]
|
||||
{
|
||||
".ass",
|
||||
".mks",
|
||||
".sami",
|
||||
".smi",
|
||||
".srt",
|
||||
".ssa",
|
||||
".sub",
|
||||
".vtt",
|
||||
".ass",
|
||||
".sub"
|
||||
};
|
||||
|
||||
SubtitleFlagDelimiters = new[]
|
||||
{
|
||||
'.'
|
||||
};
|
||||
|
||||
SubtitleForcedFlags = new[]
|
||||
{
|
||||
"foreign",
|
||||
"forced"
|
||||
};
|
||||
|
||||
SubtitleDefaultFlags = new[]
|
||||
{
|
||||
"default"
|
||||
};
|
||||
|
||||
AlbumStackingPrefixes = new[]
|
||||
{
|
||||
"cd",
|
||||
"disc",
|
||||
"cd",
|
||||
"disk",
|
||||
"vol",
|
||||
"volume"
|
||||
@@ -183,101 +176,68 @@ namespace Emby.Naming.Common
|
||||
|
||||
AudioFileExtensions = new[]
|
||||
{
|
||||
".669",
|
||||
".3gp",
|
||||
".aa",
|
||||
".aac",
|
||||
".aax",
|
||||
".ac3",
|
||||
".act",
|
||||
".adp",
|
||||
".adplug",
|
||||
".adx",
|
||||
".afc",
|
||||
".amf",
|
||||
".aif",
|
||||
".aiff",
|
||||
".alac",
|
||||
".amr",
|
||||
".ape",
|
||||
".ast",
|
||||
".au",
|
||||
".awb",
|
||||
".cda",
|
||||
".cue",
|
||||
".dmf",
|
||||
".dsf",
|
||||
".dsm",
|
||||
".dsp",
|
||||
".dts",
|
||||
".dvf",
|
||||
".far",
|
||||
".nsv",
|
||||
".m4a",
|
||||
".flac",
|
||||
".aac",
|
||||
".strm",
|
||||
".pls",
|
||||
".rm",
|
||||
".mpa",
|
||||
".wav",
|
||||
".wma",
|
||||
".ogg",
|
||||
".opus",
|
||||
".mp3",
|
||||
".mp2",
|
||||
".mod",
|
||||
".amf",
|
||||
".669",
|
||||
".dmf",
|
||||
".dsm",
|
||||
".far",
|
||||
".gdm",
|
||||
".gsm",
|
||||
".gym",
|
||||
".hps",
|
||||
".imf",
|
||||
".it",
|
||||
".m15",
|
||||
".m4a",
|
||||
".m4b",
|
||||
".mac",
|
||||
".med",
|
||||
".mka",
|
||||
".mmf",
|
||||
".mod",
|
||||
".mogg",
|
||||
".mp2",
|
||||
".mp3",
|
||||
".mpa",
|
||||
".mpc",
|
||||
".mpp",
|
||||
".mp+",
|
||||
".msv",
|
||||
".nmf",
|
||||
".nsf",
|
||||
".nsv",
|
||||
".oga",
|
||||
".ogg",
|
||||
".okt",
|
||||
".opus",
|
||||
".pls",
|
||||
".ra",
|
||||
".rf64",
|
||||
".rm",
|
||||
".s3m",
|
||||
".sfx",
|
||||
".shn",
|
||||
".sid",
|
||||
".spc",
|
||||
".stm",
|
||||
".strm",
|
||||
".sfx",
|
||||
".ult",
|
||||
".uni",
|
||||
".vox",
|
||||
".wav",
|
||||
".wma",
|
||||
".wv",
|
||||
".xm",
|
||||
".sid",
|
||||
".ac3",
|
||||
".dts",
|
||||
".cue",
|
||||
".aif",
|
||||
".aiff",
|
||||
".ape",
|
||||
".mac",
|
||||
".mpc",
|
||||
".mp+",
|
||||
".mpp",
|
||||
".shn",
|
||||
".wv",
|
||||
".nsf",
|
||||
".spc",
|
||||
".gym",
|
||||
".adplug",
|
||||
".adx",
|
||||
".dsp",
|
||||
".adp",
|
||||
".ymf",
|
||||
".ast",
|
||||
".afc",
|
||||
".hps",
|
||||
".xsp",
|
||||
".ymf"
|
||||
};
|
||||
|
||||
MediaFlagDelimiters = new[]
|
||||
{
|
||||
'.'
|
||||
};
|
||||
|
||||
MediaForcedFlags = new[]
|
||||
{
|
||||
"foreign",
|
||||
"forced"
|
||||
};
|
||||
|
||||
MediaDefaultFlags = new[]
|
||||
{
|
||||
"default"
|
||||
".acc",
|
||||
".m4b",
|
||||
".oga",
|
||||
".dsf",
|
||||
".mka"
|
||||
};
|
||||
|
||||
EpisodeExpressions = new[]
|
||||
@@ -290,8 +250,6 @@ namespace Emby.Naming.Common
|
||||
},
|
||||
// <!-- foo.ep01, foo.EP_01 -->
|
||||
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
|
||||
// <!-- foo.E01., foo.e01. -->
|
||||
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
|
||||
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
|
||||
{
|
||||
DateTimeFormats = new[]
|
||||
@@ -314,23 +272,17 @@ namespace Emby.Naming.Common
|
||||
// This isn't a Kodi naming rule, but the expression below causes false positives,
|
||||
// so we make sure this one gets tested first.
|
||||
// "Foo Bar 889"
|
||||
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
|
||||
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
|
||||
{
|
||||
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
|
||||
},
|
||||
|
||||
// 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(@".*[\\\/]?.*?(\[.*?\])+.*?(?<seriesname>[-\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// Case Closed (1996-2007)/Case Closed - 317.mkv
|
||||
// /server/anything_102.mp4
|
||||
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
|
||||
// /server/anything_1996.11.14.mp4
|
||||
@@ -347,12 +299,11 @@ namespace Emby.Naming.Common
|
||||
|
||||
// *** End Kodi Standard Naming
|
||||
|
||||
// "Episode 16", "Episode 16 - Title"
|
||||
new EpisodeExpression(@"[Ee]pisode (?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))?[^\\\/]*$")
|
||||
// [bar] Foo - 1 [baz]
|
||||
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
|
||||
{
|
||||
IsNamed = true
|
||||
@@ -410,20 +361,12 @@ namespace Emby.Naming.Common
|
||||
IsOptimistic = true,
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// Series and season only expression
|
||||
// "the show/season 1", "the show/s01"
|
||||
new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)\/[Ss](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
|
||||
// "Episode 16", "Episode 16 - Title"
|
||||
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
|
||||
{
|
||||
IsOptimistic = true,
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// Series and season only expression
|
||||
// "the show S01", "the show season 1"
|
||||
new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)[\. _\-]+[sS](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
EpisodeWithoutSeasonExpressions = new[]
|
||||
@@ -438,72 +381,6 @@ namespace Emby.Naming.Common
|
||||
|
||||
VideoExtraRules = new[]
|
||||
{
|
||||
new ExtraRule(
|
||||
ExtraType.Trailer,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"trailers",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.ThemeVideo,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"backdrops",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.ThemeSong,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"theme-music",
|
||||
MediaType.Audio),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.BehindTheScenes,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"behind the scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.DeletedScene,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"deleted scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Interview,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"interviews",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Scene,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Sample,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"samples",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Clip,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"shorts",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Clip,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"featurettes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Unknown,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"extras",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Trailer,
|
||||
ExtraRuleType.Filename,
|
||||
@@ -600,12 +477,6 @@ namespace Emby.Naming.Common
|
||||
"-deleted",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.DeletedScene,
|
||||
ExtraRuleType.Suffix,
|
||||
"-deletedscene",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Clip,
|
||||
ExtraRuleType.Suffix,
|
||||
@@ -619,15 +490,53 @@ namespace Emby.Naming.Common
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Unknown,
|
||||
ExtraRuleType.Suffix,
|
||||
"-extra",
|
||||
MediaType.Video)
|
||||
};
|
||||
ExtraType.BehindTheScenes,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"behind the scenes",
|
||||
MediaType.Video),
|
||||
|
||||
AllExtrasTypesFolderNames = VideoExtraRules
|
||||
.Where(i => i.RuleType == ExtraRuleType.DirectoryName)
|
||||
.ToDictionary(i => i.Token, i => i.ExtraType, StringComparer.OrdinalIgnoreCase);
|
||||
new ExtraRule(
|
||||
ExtraType.DeletedScene,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"deleted scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Interview,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"interviews",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Scene,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Sample,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"samples",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Clip,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"shorts",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Clip,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"featurettes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Unknown,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"extras",
|
||||
MediaType.Video),
|
||||
};
|
||||
|
||||
Format3DRules = new[]
|
||||
{
|
||||
@@ -678,10 +587,45 @@ namespace Emby.Naming.Common
|
||||
AudioBookNamesExpressions = new[]
|
||||
{
|
||||
// Detect year usually in brackets after name Batman (2020)
|
||||
@"^(?<name>.+?)\s*\(\s*(?<year>[0-9]{4})\s*\)\s*$",
|
||||
@"^(?<name>.+?)\s*\(\s*(?<year>\d{4})\s*\)\s*$",
|
||||
@"^\s*(?<name>[^ ].*?)\s*$"
|
||||
};
|
||||
|
||||
var extensions = VideoFileExtensions.ToList();
|
||||
|
||||
extensions.AddRange(new[]
|
||||
{
|
||||
".mkv",
|
||||
".m2t",
|
||||
".m2ts",
|
||||
".img",
|
||||
".iso",
|
||||
".mk3d",
|
||||
".ts",
|
||||
".rmvb",
|
||||
".mov",
|
||||
".avi",
|
||||
".mpg",
|
||||
".mpeg",
|
||||
".wmv",
|
||||
".mp4",
|
||||
".divx",
|
||||
".dvr-ms",
|
||||
".wtv",
|
||||
".ogm",
|
||||
".ogv",
|
||||
".asf",
|
||||
".m4v",
|
||||
".flv",
|
||||
".f4v",
|
||||
".3gp",
|
||||
".webm",
|
||||
".mts",
|
||||
".m2v",
|
||||
".rec",
|
||||
".mxf"
|
||||
});
|
||||
|
||||
MultipleEpisodeExpressions = new[]
|
||||
{
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||
@@ -699,34 +643,18 @@ namespace Emby.Naming.Common
|
||||
IsNamed = true
|
||||
}).ToArray();
|
||||
|
||||
VideoFileExtensions = extensions
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
Compile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the folder name to extra types mapping.
|
||||
/// </summary>
|
||||
public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of audio file extensions.
|
||||
/// </summary>
|
||||
public string[] AudioFileExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of external media flag delimiters.
|
||||
/// </summary>
|
||||
public char[] MediaFlagDelimiters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of external media forced flags.
|
||||
/// </summary>
|
||||
public string[] MediaForcedFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of external media default flags.
|
||||
/// </summary>
|
||||
public string[] MediaDefaultFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of album stacking prefixes.
|
||||
/// </summary>
|
||||
@@ -737,6 +665,21 @@ namespace Emby.Naming.Common
|
||||
/// </summary>
|
||||
public string[] SubtitleFileExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of subtitles flag delimiters.
|
||||
/// </summary>
|
||||
public char[] SubtitleFlagDelimiters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of subtitle forced flags.
|
||||
/// </summary>
|
||||
public string[] SubtitleForcedFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of subtitle default flags.
|
||||
/// </summary>
|
||||
public string[] SubtitleDefaultFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of episode regular expressions.
|
||||
/// </summary>
|
||||
@@ -788,9 +731,9 @@ namespace Emby.Naming.Common
|
||||
public Format3DRule[] Format3DRules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file stacking rules.
|
||||
/// Gets or sets list of raw video file-stacking expressions strings.
|
||||
/// </summary>
|
||||
public FileStackRule[] VideoFileStackingRules { get; }
|
||||
public string[] VideoFileStackingExpressions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of raw clean DateTimes regular expressions strings.
|
||||
@@ -812,6 +755,11 @@ namespace Emby.Naming.Common
|
||||
/// </summary>
|
||||
public ExtraRule[] VideoExtraRules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets list of video file-stack regular expressions.
|
||||
/// </summary>
|
||||
public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets list of clean datetime regular expressions.
|
||||
/// </summary>
|
||||
@@ -837,6 +785,7 @@ namespace Emby.Naming.Common
|
||||
/// </summary>
|
||||
public void Compile()
|
||||
{
|
||||
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
|
||||
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
|
||||
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
|
||||
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
@@ -6,17 +6,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||
@@ -25,35 +23,35 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="../SharedVersion.cs" />
|
||||
<Compile Include="..\SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.8.2</VersionPrefix>
|
||||
<VersionPrefix>10.7.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace Emby.Naming.ExternalFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// External media file parser class.
|
||||
/// </summary>
|
||||
public class ExternalPathParser
|
||||
{
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly DlnaProfileType _type;
|
||||
private readonly ILocalizationManager _localizationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExternalPathParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localizationManager">The localization manager.</param>
|
||||
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
|
||||
/// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
|
||||
public ExternalPathParser(NamingOptions namingOptions, ILocalizationManager localizationManager, DlnaProfileType type)
|
||||
{
|
||||
_localizationManager = localizationManager;
|
||||
_namingOptions = namingOptions;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse filename and extract information.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to file.</param>
|
||||
/// <param name="extraString">Part of the filename only containing the extra information.</param>
|
||||
/// <returns>Returns null or an <see cref="ExternalPathParserResult"/> object if parsing is successful.</returns>
|
||||
public ExternalPathParserResult? ParseFile(string path, string? extraString)
|
||||
{
|
||||
if (path.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var pathInfo = new ExternalPathParserResult(path);
|
||||
|
||||
if (string.IsNullOrEmpty(extraString))
|
||||
{
|
||||
return pathInfo;
|
||||
}
|
||||
|
||||
foreach (var separator in _namingOptions.MediaFlagDelimiters)
|
||||
{
|
||||
var languageString = extraString;
|
||||
var titleString = string.Empty;
|
||||
const int SeparatorLength = 1;
|
||||
|
||||
while (languageString.Length > 0)
|
||||
{
|
||||
int lastSeparator = languageString.LastIndexOf(separator);
|
||||
|
||||
if (lastSeparator == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
string currentSlice = languageString[lastSeparator..];
|
||||
string currentSliceWithoutSeparator = currentSlice[SeparatorLength..];
|
||||
|
||||
if (_namingOptions.MediaDefaultFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
pathInfo.IsDefault = true;
|
||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
languageString = languageString[..lastSeparator];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_namingOptions.MediaForcedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
pathInfo.IsForced = true;
|
||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
languageString = languageString[..lastSeparator];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to translate to three character code
|
||||
var culture = _localizationManager.FindLanguageInfo(currentSliceWithoutSeparator);
|
||||
|
||||
if (culture != null && pathInfo.Language == null)
|
||||
{
|
||||
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
titleString = currentSlice + titleString;
|
||||
}
|
||||
|
||||
languageString = languageString[..lastSeparator];
|
||||
}
|
||||
|
||||
pathInfo.Title = titleString.Length >= SeparatorLength ? titleString[SeparatorLength..] : null;
|
||||
}
|
||||
|
||||
return pathInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
namespace Emby.Naming.ExternalFiles
|
||||
namespace Emby.Naming.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// Class holding information about external files.
|
||||
/// Class holding information about subtitle.
|
||||
/// </summary>
|
||||
public class ExternalPathParserResult
|
||||
public class SubtitleInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExternalPathParserResult"/> class.
|
||||
/// Initializes a new instance of the <see cref="SubtitleInfo"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to file.</param>
|
||||
/// <param name="isDefault">Is default.</param>
|
||||
/// <param name="isForced">Is forced.</param>
|
||||
public ExternalPathParserResult(string path, bool isDefault = false, bool isForced = false)
|
||||
/// <param name="isDefault">Is subtitle default.</param>
|
||||
/// <param name="isForced">Is subtitle forced.</param>
|
||||
public SubtitleInfo(string path, bool isDefault, bool isForced)
|
||||
{
|
||||
Path = path;
|
||||
IsDefault = isDefault;
|
||||
@@ -30,12 +30,6 @@ namespace Emby.Naming.ExternalFiles
|
||||
/// <value>The language.</value>
|
||||
public string? Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
/// <value>The title.</value>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is default.
|
||||
/// </summary>
|
||||
70
Emby.Naming/Subtitles/SubtitleParser.cs
Normal file
70
Emby.Naming/Subtitles/SubtitleParser.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
|
||||
namespace Emby.Naming.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// Subtitle Parser class.
|
||||
/// </summary>
|
||||
public class SubtitleParser
|
||||
{
|
||||
private readonly NamingOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubtitleParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.</param>
|
||||
public SubtitleParser(NamingOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse file to determine if is subtitle and <see cref="SubtitleInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to file.</param>
|
||||
/// <returns>Returns null or <see cref="SubtitleInfo"/> object if parsing is successful.</returns>
|
||||
public SubtitleInfo? ParseFile(string path)
|
||||
{
|
||||
if (path.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
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)));
|
||||
|
||||
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
|
||||
&& !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
// Should have a name, language and file extension
|
||||
if (parts.Count >= 3)
|
||||
{
|
||||
info.Language = parts[^2];
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private string[] GetFlags(string path)
|
||||
{
|
||||
// Note: the tags need be surrounded be either a space ( ), hyphen -, dot . or underscore _.
|
||||
|
||||
var file = Path.GetFileName(path);
|
||||
|
||||
return file.Split(_options.SubtitleFlagDelimiters, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
|
||||
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, StringComparison.OrdinalIgnoreCase))
|
||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
// It's not supported. Check stub extensions
|
||||
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
||||
@@ -62,16 +62,12 @@ namespace Emby.Naming.TV
|
||||
container = extension.TrimStart('.');
|
||||
}
|
||||
|
||||
var format3DResult = Format3DParser.Parse(path, _options);
|
||||
var flags = new FlagParser(_options).GetFlags(path);
|
||||
var format3DResult = new Format3DParser(_options).Parse(flags);
|
||||
|
||||
var parsingResult = new EpisodePathParser(_options)
|
||||
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
||||
|
||||
if (!parsingResult.Success && !isStub)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new EpisodeInfo(path)
|
||||
{
|
||||
Container = container,
|
||||
|
||||
@@ -55,12 +55,12 @@ namespace Emby.Naming.TV
|
||||
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
|
||||
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
|
||||
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPath(
|
||||
string path,
|
||||
bool supportSpecialAliases,
|
||||
bool supportNumericSeasonFolders)
|
||||
{
|
||||
string filename = Path.GetFileName(path);
|
||||
var filename = Path.GetFileName(path) ?? string.Empty;
|
||||
|
||||
if (supportSpecialAliases)
|
||||
{
|
||||
@@ -99,7 +99,7 @@ namespace Emby.Naming.TV
|
||||
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
|
||||
if (result.SeasonNumber.HasValue)
|
||||
if (result.seasonNumber.HasValue)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
@@ -142,7 +142,7 @@ namespace Emby.Naming.TV
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
|
||||
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
|
||||
{
|
||||
var numericStart = -1;
|
||||
var length = 0;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Holder object for Series information.
|
||||
/// </summary>
|
||||
public class SeriesInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeriesInfo"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the file.</param>
|
||||
public SeriesInfo(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the series.
|
||||
/// </summary>
|
||||
/// <value>The name of the series.</value>
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using Emby.Naming.Common;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class SeriesPathParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses information about series from path.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
|
||||
/// <param name="path">Path.</param>
|
||||
/// <returns>Returns <see cref="SeriesPathParserResult"/> object.</returns>
|
||||
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) && !match.Groups["seasonnumber"].ValueSpan.IsEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Holder object for <see cref="SeriesPathParser"/> result.
|
||||
/// </summary>
|
||||
public class SeriesPathParserResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the series.
|
||||
/// </summary>
|
||||
/// <value>The name of the series.</value>
|
||||
public string? SeriesName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether parsing was successful.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to resolve information about series from path.
|
||||
/// </summary>
|
||||
public static class SeriesResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 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".
|
||||
/// </summary>
|
||||
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
|
||||
|
||||
/// <summary>
|
||||
/// Resolve information about series from path.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object passed to <see cref="SeriesPathParser"/>.</param>
|
||||
/// <param name="path">Path to series.</param>
|
||||
/// <returns>SeriesInfo.</returns>
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user