Compare commits
231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd102abd81 | |||
| f8f7767cc5 | |||
| e8436814dc | |||
| 14f63e8f2f | |||
| e764de0c80 | |||
| 40147c9bb7 | |||
| 3566d21ad1 | |||
| 4c8df4c5bb | |||
| 9798bf29f3 | |||
| 93ce087fc9 | |||
| e39495354b | |||
| ee94fad8f7 | |||
| 53239b0529 | |||
| cf0da1de86 | |||
| 093510ae58 | |||
| c3fafe9289 | |||
| bd914acd16 | |||
| 81f9bec101 | |||
| 7db8601fbc | |||
| 1ec247f5d8 | |||
| 11e9173fbc | |||
| 34508286a8 | |||
| a82eded845 | |||
| fcb729ff6b | |||
| 69f30bc52c | |||
| 3b605b6280 | |||
| e8a359f97b | |||
| dbfaafc08a | |||
| de6747f6c5 | |||
| 100fe40b0a | |||
| 2197d20783 | |||
| f4f9ab777f | |||
| 9ca7d62709 | |||
| 7aad16b6ec | |||
| f77673438e | |||
| bc2eb9fa79 | |||
| df69ce55f7 | |||
| 93cca4d50e | |||
| 3c64bcffe3 | |||
| 6ece01d425 | |||
| 53f333bd64 | |||
| 9e459090ed | |||
| 95a4fc0f18 | |||
| 62bf3db885 | |||
| 42d702c091 | |||
| d07fe14814 | |||
| 970eaf8dfb | |||
| bc27c2b7da | |||
| 37b969304a | |||
| c6b5c4dda5 | |||
| 51f5da8015 | |||
| 6e89ca9a34 | |||
| de1896828f | |||
| 7d1d159b8a | |||
| c3c98331d9 | |||
| e78fa8c3ef | |||
| d63fb437c6 | |||
| 25c6388e23 | |||
| 8f16e10fc6 | |||
| 1f07586d1c | |||
| 5e0f480e48 | |||
| 210d10400a | |||
| 3dda25412c | |||
| 0183ef8e89 | |||
| 75f39f0f2a | |||
| 966217e6a9 | |||
| 328bcadabf | |||
| 0f38b2ffb2 | |||
| 40f4780825 | |||
| 546ffbe4f7 | |||
| d00218c370 | |||
| 679d3f5873 | |||
| 787ad44323 | |||
| 2ce6b347f5 | |||
| 318c1f7f0c | |||
| ed15cb1571 | |||
| c171bac71a | |||
| be5f511fc7 | |||
| a65c97c8f7 | |||
| 3fbe10364b | |||
| 88ab008112 | |||
| 1518f6d325 | |||
| 53576fe1b8 | |||
| da3b7bb684 | |||
| 4a320b26b5 | |||
| 63868eca40 | |||
| f6e8493d69 | |||
| 3c3b536e81 | |||
| a10eea41ac | |||
| 42d0c1ac5f | |||
| b01290013e | |||
| 132335a747 | |||
| 75d3d120d3 | |||
| e8890cc682 | |||
| e4bf57c739 | |||
| 046dd7fa60 | |||
| 5e18ab3604 | |||
| 7545b1286b | |||
| b99db64f8f | |||
| 20810eedbe | |||
| 2d88b8346d | |||
| eafaccae5d | |||
| d2851979d4 | |||
| 4b6ff7ffa5 | |||
| 153123278b | |||
| 25c19f79d4 | |||
| 0f139e8857 | |||
| e6cc8d5015 | |||
| ef864e24b9 | |||
| ab054d6239 | |||
| 19ff447e51 | |||
| 4220808b96 | |||
| eb0621a354 | |||
| 621c0b9d15 | |||
| fecab1d549 | |||
| bd89cdf8d2 | |||
| 557a091865 | |||
| a1773ce97b | |||
| be7411dc58 | |||
| b2a8fd82d8 | |||
| d53120602c | |||
| da09257d58 | |||
| 706ac0fafd | |||
| ebd4328f02 | |||
| 39b0d69786 | |||
| 9f3cebf493 | |||
| 20e985a0d1 | |||
| 5dbd6f076c | |||
| 19a01ccdf3 | |||
| d816995d27 | |||
| a934477850 | |||
| a7f65bd205 | |||
| 8138fc3003 | |||
| 46a6cd8d1f | |||
| 524df2e45d | |||
| a486cd27a9 | |||
| 34053b7259 | |||
| 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"
|
default: "ubuntu-latest"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 6.0.x
|
default: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: CompatibilityCheck
|
- job: CompatibilityCheck
|
||||||
@@ -34,7 +34,6 @@ jobs:
|
|||||||
inputs:
|
inputs:
|
||||||
packageType: sdk
|
packageType: sdk
|
||||||
version: ${{ parameters.DotNetSdkVersion }}
|
version: ${{ parameters.DotNetSdkVersion }}
|
||||||
includePreviewVersions: true
|
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
- task: DotNetCoreCLI@2
|
||||||
displayName: 'Install ABI CompatibilityChecker Tool'
|
displayName: 'Install ABI CompatibilityChecker Tool'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
parameters:
|
parameters:
|
||||||
LinuxImage: 'ubuntu-latest'
|
LinuxImage: 'ubuntu-latest'
|
||||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
DotNetSdkVersion: 6.0.x
|
DotNetSdkVersion: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Build
|
- job: Build
|
||||||
@@ -54,7 +54,6 @@ jobs:
|
|||||||
inputs:
|
inputs:
|
||||||
packageType: sdk
|
packageType: sdk
|
||||||
version: ${{ parameters.DotNetSdkVersion }}
|
version: ${{ parameters.DotNetSdkVersion }}
|
||||||
includePreviewVersions: true
|
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
- task: DotNetCoreCLI@2
|
||||||
displayName: 'Publish Server'
|
displayName: 'Publish Server'
|
||||||
@@ -92,10 +91,3 @@ jobs:
|
|||||||
inputs:
|
inputs:
|
||||||
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
|
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
|
||||||
artifactName: 'Jellyfin.Common'
|
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'
|
|
||||||
|
|||||||
@@ -195,11 +195,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Use .NET 6.0 sdk'
|
displayName: 'Use .NET 5.0 sdk'
|
||||||
inputs:
|
inputs:
|
||||||
packageType: 'sdk'
|
packageType: 'sdk'
|
||||||
version: '6.0.x'
|
version: '5.0.x'
|
||||||
includePreviewVersions: true
|
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
- task: DotNetCoreCLI@2
|
||||||
displayName: 'Build Stable Nuget packages'
|
displayName: 'Build Stable Nuget packages'
|
||||||
@@ -212,7 +211,6 @@ jobs:
|
|||||||
MediaBrowser.Controller/MediaBrowser.Controller.csproj
|
MediaBrowser.Controller/MediaBrowser.Controller.csproj
|
||||||
MediaBrowser.Model/MediaBrowser.Model.csproj
|
MediaBrowser.Model/MediaBrowser.Model.csproj
|
||||||
Emby.Naming/Emby.Naming.csproj
|
Emby.Naming/Emby.Naming.csproj
|
||||||
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
|
|
||||||
custom: 'pack'
|
custom: 'pack'
|
||||||
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
|
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
|
||||||
|
|
||||||
@@ -227,7 +225,6 @@ jobs:
|
|||||||
MediaBrowser.Controller/MediaBrowser.Controller.csproj
|
MediaBrowser.Controller/MediaBrowser.Controller.csproj
|
||||||
MediaBrowser.Model/MediaBrowser.Model.csproj
|
MediaBrowser.Model/MediaBrowser.Model.csproj
|
||||||
Emby.Naming/Emby.Naming.csproj
|
Emby.Naming/Emby.Naming.csproj
|
||||||
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
|
|
||||||
custom: 'pack'
|
custom: 'pack'
|
||||||
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
|
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ parameters:
|
|||||||
default: "tests/**/*Tests.csproj"
|
default: "tests/**/*Tests.csproj"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 6.0.x
|
default: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Test
|
- job: Test
|
||||||
@@ -41,7 +41,6 @@ jobs:
|
|||||||
inputs:
|
inputs:
|
||||||
packageType: sdk
|
packageType: sdk
|
||||||
version: ${{ parameters.DotNetSdkVersion }}
|
version: ${{ parameters.DotNetSdkVersion }}
|
||||||
includePreviewVersions: true
|
|
||||||
|
|
||||||
- task: SonarCloudPrepare@1
|
- task: SonarCloudPrepare@1
|
||||||
displayName: 'Prepare analysis on SonarCloud'
|
displayName: 'Prepare analysis on SonarCloud'
|
||||||
@@ -95,5 +94,5 @@ jobs:
|
|||||||
displayName: 'Publish OpenAPI Artifact'
|
displayName: 'Publish OpenAPI Artifact'
|
||||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||||
inputs:
|
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'
|
artifactName: 'OpenAPI Spec'
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ variables:
|
|||||||
value: 'tests/**/*Tests.csproj'
|
value: 'tests/**/*Tests.csproj'
|
||||||
- name: RestoreBuildProjects
|
- name: RestoreBuildProjects
|
||||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
|
- name: DotNetSdkVersion
|
||||||
|
value: 5.0.103
|
||||||
|
|
||||||
pr:
|
pr:
|
||||||
autoCancel: true
|
autoCancel: true
|
||||||
@@ -55,9 +57,6 @@ jobs:
|
|||||||
Common:
|
Common:
|
||||||
NugetPackageName: Jellyfin.Common
|
NugetPackageName: Jellyfin.Common
|
||||||
AssemblyFileName: MediaBrowser.Common.dll
|
AssemblyFileName: MediaBrowser.Common.dll
|
||||||
Extensions:
|
|
||||||
NugetPackageName: Jellyfin.Extensions
|
|
||||||
AssemblyFileName: Jellyfin.Extensions.dll
|
|
||||||
LinuxImage: 'ubuntu-latest'
|
LinuxImage: 'ubuntu-latest'
|
||||||
|
|
||||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
||||||
|
|||||||
+30
@@ -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,4 +1,3 @@
|
|||||||
# Joshua must review all changes to deployment and build.sh
|
# Joshua must review all changes to deployment and build.sh
|
||||||
.ci/* @joshuaboniface
|
|
||||||
deployment/* @joshuaboniface
|
deployment/* @joshuaboniface
|
||||||
build.sh @joshuaboniface
|
build.sh @joshuaboniface
|
||||||
|
|||||||
@@ -14,11 +14,9 @@ assignees: ''
|
|||||||
- OS: [e.g. Debian, Windows]
|
- OS: [e.g. Debian, Windows]
|
||||||
- Virtualization: [e.g. Docker, KVM, LXC]
|
- Virtualization: [e.g. Docker, KVM, LXC]
|
||||||
- Clients: [Browser, Android, Fire Stick, etc.]
|
- Clients: [Browser, Android, Fire Stick, etc.]
|
||||||
- Browser: [e.g. Firefox 91, Chrome 93, Safari 13]
|
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
|
||||||
- Jellyfin Version: [e.g. 10.7.6, unstable 20191231]
|
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
|
||||||
- FFmpeg Version: [e.g. 4.3.2-Jellyfin]
|
|
||||||
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
|
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
|
||||||
- Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
|
|
||||||
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
|
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
|
||||||
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
|
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
|
||||||
- Base URL: [e.g. none, yes: /example]
|
- Base URL: [e.g. none, yes: /example]
|
||||||
@@ -35,13 +33,7 @@ assignees: ''
|
|||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
<!-- A clear and concise description of what you expected to happen. -->
|
<!-- A clear and concise description of what you expected to happen. -->
|
||||||
|
|
||||||
**Server Logs**
|
**Logs**
|
||||||
<!-- Please paste any log errors. -->
|
|
||||||
|
|
||||||
**FFmpeg Logs**
|
|
||||||
<!-- Please paste any log errors. -->
|
|
||||||
|
|
||||||
**Browser Console Logs**
|
|
||||||
<!-- Please paste any log errors. -->
|
<!-- Please paste any log errors. -->
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
|
|||||||
@@ -6,10 +6,4 @@ updates:
|
|||||||
interval: weekly
|
interval: weekly
|
||||||
time: '12:00'
|
time: '12:00'
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
time: '12:00'
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
|
|||||||
+1
-5
@@ -17,13 +17,9 @@ staleLabel: stale
|
|||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
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.
|
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.
|
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).
|
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
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
closeComment: false
|
closeComment: false
|
||||||
|
|
||||||
# Disable automatic closing of pull requests
|
|
||||||
pulls:
|
|
||||||
daysUntilClose: false
|
|
||||||
|
|||||||
@@ -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 }}
|
|
||||||
@@ -24,9 +24,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '6.0.x'
|
dotnet-version: '5.0.x'
|
||||||
include-prerelease: true
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -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@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Automatic Rebase
|
|
||||||
uses: cirrus-actions/rebase@1.5
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
|
|
||||||
check-backport:
|
|
||||||
name: Check Backport
|
|
||||||
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Notify as seen
|
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
|
||||||
if: ${{ github.event.comment != null }}
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
comment-id: ${{ github.event.comment.id }}
|
|
||||||
reactions: eyes
|
|
||||||
|
|
||||||
- name: Checkout the latest code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Notify as running
|
|
||||||
id: comment_running
|
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
|
||||||
if: ${{ github.event.comment != null }}
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
issue-number: ${{ github.event.issue.number }}
|
|
||||||
body: |
|
|
||||||
Running backport tests...
|
|
||||||
|
|
||||||
- name: Perform test backport
|
|
||||||
id: run_tests
|
|
||||||
run: |
|
|
||||||
set +o errexit
|
|
||||||
git config --global user.name "Jellyfin Bot"
|
|
||||||
git config --global user.email "team@jellyfin.org"
|
|
||||||
CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
|
|
||||||
git checkout master
|
|
||||||
git merge --no-ff ${CURRENT_BRANCH}
|
|
||||||
MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
|
|
||||||
git fetch --all
|
|
||||||
CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
|
|
||||||
stable_branch="Current stable release branch: ${CURRENT_STABLE}"
|
|
||||||
echo ${stable_branch}
|
|
||||||
echo ::set-output name=branch::${stable_branch}
|
|
||||||
git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
|
|
||||||
git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
|
|
||||||
retcode=$?
|
|
||||||
cat output.txt | grep -v 'hint:'
|
|
||||||
output="$( grep -v 'hint:' output.txt )"
|
|
||||||
output="${output//'%'/'%25'}"
|
|
||||||
output="${output//$'\n'/'%0A'}"
|
|
||||||
output="${output//$'\r'/'%0D'}"
|
|
||||||
echo ::set-output name=output::$output
|
|
||||||
exit ${retcode}
|
|
||||||
|
|
||||||
- name: Notify with result success
|
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
|
||||||
if: ${{ github.event.comment != null && success() }}
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
|
||||||
body: |
|
|
||||||
${{ steps.run_tests.outputs.branch }}
|
|
||||||
Output from `git cherry-pick`:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
${{ steps.run_tests.outputs.output }}
|
|
||||||
reactions: hooray
|
|
||||||
|
|
||||||
- name: Notify with result failure
|
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
|
||||||
if: ${{ github.event.comment != null && failure() }}
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
|
||||||
body: |
|
|
||||||
${{ steps.run_tests.outputs.branch }}
|
|
||||||
Output from `git cherry-pick`:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
${{ steps.run_tests.outputs.output }}
|
|
||||||
reactions: confused
|
|
||||||
@@ -268,7 +268,6 @@ doc/
|
|||||||
# Deployment artifacts
|
# Deployment artifacts
|
||||||
dist
|
dist
|
||||||
*.exe
|
*.exe
|
||||||
*.dll
|
|
||||||
|
|
||||||
# BenchmarkDotNet artifacts
|
# BenchmarkDotNet artifacts
|
||||||
BenchmarkDotNet.Artifacts
|
BenchmarkDotNet.Artifacts
|
||||||
@@ -278,6 +277,3 @@ web/
|
|||||||
web-src.*
|
web-src.*
|
||||||
MediaBrowser.WebDashboard/jellyfin-web
|
MediaBrowser.WebDashboard/jellyfin-web
|
||||||
apiclient/generated
|
apiclient/generated
|
||||||
|
|
||||||
# Omnisharp crash logs
|
|
||||||
mono_crash.*.json
|
|
||||||
|
|||||||
Vendored
+2
-2
@@ -6,7 +6,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
|
||||||
"args": ["--nowebclient"],
|
"args": ["--nowebclient"],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
|
|||||||
+1
-9
@@ -17,7 +17,6 @@
|
|||||||
- [bugfixin](https://github.com/bugfixin)
|
- [bugfixin](https://github.com/bugfixin)
|
||||||
- [chaosinnovator](https://github.com/chaosinnovator)
|
- [chaosinnovator](https://github.com/chaosinnovator)
|
||||||
- [ckcr4lyf](https://github.com/ckcr4lyf)
|
- [ckcr4lyf](https://github.com/ckcr4lyf)
|
||||||
- [cocool97](https://github.com/cocool97)
|
|
||||||
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
||||||
- [crankdoofus](https://github.com/crankdoofus)
|
- [crankdoofus](https://github.com/crankdoofus)
|
||||||
- [crobibero](https://github.com/crobibero)
|
- [crobibero](https://github.com/crobibero)
|
||||||
@@ -46,7 +45,6 @@
|
|||||||
- [fruhnow](https://github.com/fruhnow)
|
- [fruhnow](https://github.com/fruhnow)
|
||||||
- [geilername](https://github.com/geilername)
|
- [geilername](https://github.com/geilername)
|
||||||
- [gnattu](https://github.com/gnattu)
|
- [gnattu](https://github.com/gnattu)
|
||||||
- [GodTamIt](https://github.com/GodTamIt)
|
|
||||||
- [grafixeyehero](https://github.com/grafixeyehero)
|
- [grafixeyehero](https://github.com/grafixeyehero)
|
||||||
- [h1nk](https://github.com/h1nk)
|
- [h1nk](https://github.com/h1nk)
|
||||||
- [hawken93](https://github.com/hawken93)
|
- [hawken93](https://github.com/hawken93)
|
||||||
@@ -71,7 +69,6 @@
|
|||||||
- [marius-luca-87](https://github.com/marius-luca-87)
|
- [marius-luca-87](https://github.com/marius-luca-87)
|
||||||
- [mark-monteiro](https://github.com/mark-monteiro)
|
- [mark-monteiro](https://github.com/mark-monteiro)
|
||||||
- [Matt07211](https://github.com/Matt07211)
|
- [Matt07211](https://github.com/Matt07211)
|
||||||
- [Maxr1998](https://github.com/Maxr1998)
|
|
||||||
- [mcarlton00](https://github.com/mcarlton00)
|
- [mcarlton00](https://github.com/mcarlton00)
|
||||||
- [mitchfizz05](https://github.com/mitchfizz05)
|
- [mitchfizz05](https://github.com/mitchfizz05)
|
||||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||||
@@ -84,7 +81,6 @@
|
|||||||
- [nvllsvm](https://github.com/nvllsvm)
|
- [nvllsvm](https://github.com/nvllsvm)
|
||||||
- [nyanmisaka](https://github.com/nyanmisaka)
|
- [nyanmisaka](https://github.com/nyanmisaka)
|
||||||
- [OancaAndrei](https://github.com/OancaAndrei)
|
- [OancaAndrei](https://github.com/OancaAndrei)
|
||||||
- [obradovichv](https://github.com/obradovichv)
|
|
||||||
- [oddstr13](https://github.com/oddstr13)
|
- [oddstr13](https://github.com/oddstr13)
|
||||||
- [orryverducci](https://github.com/orryverducci)
|
- [orryverducci](https://github.com/orryverducci)
|
||||||
- [petermcneil](https://github.com/petermcneil)
|
- [petermcneil](https://github.com/petermcneil)
|
||||||
@@ -112,7 +108,7 @@
|
|||||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||||
- [sparky8251](https://github.com/sparky8251)
|
- [sparky8251](https://github.com/sparky8251)
|
||||||
- [spookbits](https://github.com/spookbits)
|
- [spookbits](https://github.com/spookbits)
|
||||||
- [ssenart](https://github.com/ssenart)
|
- [ssenart] (https://github.com/ssenart)
|
||||||
- [stanionascu](https://github.com/stanionascu)
|
- [stanionascu](https://github.com/stanionascu)
|
||||||
- [stevehayles](https://github.com/stevehayles)
|
- [stevehayles](https://github.com/stevehayles)
|
||||||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||||
@@ -148,8 +144,6 @@
|
|||||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||||
- [skyfrk](https://github.com/skyfrk)
|
- [skyfrk](https://github.com/skyfrk)
|
||||||
- [ianjazz246](https://github.com/ianjazz246)
|
- [ianjazz246](https://github.com/ianjazz246)
|
||||||
- [peterspenler](https://github.com/peterspenler)
|
|
||||||
- [MBR-0001](https://github.com/MBR-0001)
|
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
@@ -214,5 +208,3 @@
|
|||||||
- [Tim Hobbs](https://github.com/timhobbs)
|
- [Tim Hobbs](https://github.com/timhobbs)
|
||||||
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
||||||
- [olsh](https://github.com/olsh)
|
- [olsh](https://github.com/olsh)
|
||||||
- [lbenini](https://github.com/lbenini)
|
|
||||||
- [gnuyent](https://github.com/gnuyent)
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<Project>
|
|
||||||
<!-- Sets defaults for all projects in the repo -->
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
+22
-34
@@ -1,18 +1,22 @@
|
|||||||
# DESIGNED FOR BUILDING ON AMD64 ONLY
|
ARG DOTNET_VERSION=5.0
|
||||||
#####################################
|
|
||||||
# Requires binfm_misc registration
|
|
||||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
|
||||||
ARG DOTNET_VERSION=6.0
|
|
||||||
|
|
||||||
FROM node:lts-alpine as web-builder
|
FROM node:alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=master
|
||||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm 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 - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& npm ci --no-audit --unsafe-perm \
|
&& yarn install \
|
||||||
&& mv dist /dist
|
&& 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
|
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||||
ARG DEBIAN_FRONTEND="noninteractive"
|
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)
|
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||||
|
|
||||||
|
COPY --from=builder /jellyfin /jellyfin
|
||||||
|
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||||
|
|
||||||
# https://github.com/intel/compute-runtime/releases
|
# https://github.com/intel/compute-runtime/releases
|
||||||
ARG GMMLIB_VERSION=21.2.1
|
ARG GMMLIB_VERSION=20.3.2
|
||||||
ARG IGC_VERSION=1.0.8517
|
ARG IGC_VERSION=1.0.5435
|
||||||
ARG NEO_VERSION=21.35.20826
|
ARG NEO_VERSION=20.46.18421
|
||||||
ARG LEVEL_ZERO_VERSION=1.2.20826
|
ARG LEVEL_ZERO_VERSION=1.0.18421
|
||||||
|
|
||||||
# Install dependencies:
|
# Install dependencies:
|
||||||
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
|
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
|
||||||
# curl: healthcheck
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https 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 - \
|
&& 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 \
|
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
@@ -62,32 +68,14 @@ RUN apt-get update \
|
|||||||
&& chmod 777 /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
|
&& 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 LC_ALL en_US.UTF-8
|
||||||
ENV LANG en_US.UTF-8
|
ENV LANG en_US.UTF-8
|
||||||
ENV LANGUAGE en_US:en
|
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
|
EXPOSE 8096
|
||||||
VOLUME /cache /config /media
|
VOLUME /cache /config /media
|
||||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||||
"--datadir", "/config", \
|
"--datadir", "/config", \
|
||||||
"--cachedir", "/cache", \
|
"--cachedir", "/cache", \
|
||||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
|
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
|
||||||
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
|
|
||||||
|
|||||||
+22
-29
@@ -1,20 +1,31 @@
|
|||||||
# DESIGNED FOR BUILDING ON ARM ONLY
|
# DESIGNED FOR BUILDING ON AMD64 ONLY
|
||||||
#####################################
|
#####################################
|
||||||
# Requires binfm_misc registration
|
# Requires binfm_misc registration
|
||||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
# 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
|
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 - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& npm ci --no-audit --unsafe-perm \
|
&& yarn install \
|
||||||
&& mv dist /dist
|
&& 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 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
|
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||||
ARG DEBIAN_FRONTEND="noninteractive"
|
ARG DEBIAN_FRONTEND="noninteractive"
|
||||||
@@ -24,8 +35,6 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
|||||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||||
|
|
||||||
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||||
|
|
||||||
# curl: setup & healthcheck
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
|
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
|
||||||
curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
|
curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
|
||||||
@@ -44,7 +53,7 @@ RUN apt-get update \
|
|||||||
vainfo \
|
vainfo \
|
||||||
libva2 \
|
libva2 \
|
||||||
locales \
|
locales \
|
||||||
&& apt-get remove gnupg -y \
|
&& apt-get remove curl gnupg -y \
|
||||||
&& apt-get clean autoclean -y \
|
&& apt-get clean autoclean -y \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
@@ -52,33 +61,17 @@ RUN apt-get update \
|
|||||||
&& chmod 777 /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
|
&& 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 LC_ALL en_US.UTF-8
|
||||||
ENV LANG en_US.UTF-8
|
ENV LANG en_US.UTF-8
|
||||||
ENV LANGUAGE en_US:en
|
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
|
EXPOSE 8096
|
||||||
VOLUME /cache /config /media
|
VOLUME /cache /config /media
|
||||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||||
"--datadir", "/config", \
|
"--datadir", "/config", \
|
||||||
"--cachedir", "/cache", \
|
"--cachedir", "/cache", \
|
||||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
|
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
|
||||||
CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
|
|
||||||
|
|||||||
+35
-44
@@ -1,52 +1,18 @@
|
|||||||
# DESIGNED FOR BUILDING ON ARM64 ONLY
|
# DESIGNED FOR BUILDING ON AMD64 ONLY
|
||||||
#####################################
|
#####################################
|
||||||
# Requires binfm_misc registration
|
# Requires binfm_misc registration
|
||||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
# 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
|
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 - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& npm ci --no-audit --unsafe-perm \
|
&& yarn install \
|
||||||
&& mv dist /dist
|
&& 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
|
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
@@ -57,19 +23,44 @@ RUN find . -type d -name obj | xargs -r rm -r
|
|||||||
# Build
|
# Build
|
||||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
|
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=builder /jellyfin /jellyfin
|
||||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
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
|
EXPOSE 8096
|
||||||
VOLUME /cache /config /media
|
VOLUME /cache /config /media
|
||||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||||
"--datadir", "/config", \
|
"--datadir", "/config", \
|
||||||
"--cachedir", "/cache", \
|
"--cachedir", "/cache", \
|
||||||
"--ffmpeg", "/usr/bin/ffmpeg"]
|
"--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>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<Nullable>disable</Nullable>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+1
-2
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@ namespace DvdLib.Ifo
|
|||||||
|
|
||||||
private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
|
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)) ??
|
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));
|
allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ namespace Emby.Dlna.Configuration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the default user account that the dlna server uses.
|
/// Gets or sets the default user account that the dlna server uses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DefaultUserId { get; set; }
|
public string DefaultUserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether playTo device profiles should be created.
|
/// Gets or sets a value indicating whether playTo device profiles should be created.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#nullable enable
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using Emby.Dlna.Configuration;
|
using Emby.Dlna.Configuration;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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))
|
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
||||||
/// <returns>The <see cref="User"/>.</returns>
|
/// <returns>The <see cref="User"/>.</returns>
|
||||||
private User? GetUser(DeviceProfile profile)
|
private User GetUser(DeviceProfile profile)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(profile.UserId))
|
if (!string.IsNullOrEmpty(profile.UserId))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -8,6 +7,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
using Emby.Dlna.Configuration;
|
||||||
using Emby.Dlna.Didl;
|
using Emby.Dlna.Didl;
|
||||||
using Emby.Dlna.Service;
|
using Emby.Dlna.Service;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
@@ -121,7 +121,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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 (xmlWriter == null)
|
if (xmlWriter == null)
|
||||||
{
|
{
|
||||||
@@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a "XSetBookmark" element to the xml document.
|
/// Adds a "XSetBookmark" element to the xml document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sparams">The method parameters.</param>
|
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
||||||
private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams)
|
private void HandleXSetBookmark(IDictionary<string, string> sparams)
|
||||||
{
|
{
|
||||||
var id = sparams["ObjectID"];
|
var id = sparams["ObjectID"];
|
||||||
|
|
||||||
@@ -288,28 +288,52 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
/// <returns>The xml feature list.</returns>
|
/// <returns>The xml feature list.</returns>
|
||||||
private static string WriteFeatureListXml()
|
private static string WriteFeatureListXml()
|
||||||
{
|
{
|
||||||
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
// TODO: clean this up
|
||||||
+ "<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">"
|
var builder = new StringBuilder();
|
||||||
+ "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"
|
|
||||||
+ "<container id=\"I\" type=\"object.item.imageItem\"/>"
|
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||||
+ "<container id=\"A\" type=\"object.item.audioItem\"/>"
|
builder.Append("<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">");
|
||||||
+ "<container id=\"V\" type=\"object.item.videoItem\"/>"
|
|
||||||
+ "</Feature>"
|
builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">");
|
||||||
+ "</Features>";
|
builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>");
|
||||||
|
builder.Append("<container id=\"A\" type=\"object.item.audioItem\"/>");
|
||||||
|
builder.Append("<container id=\"V\" type=\"object.item.videoItem\"/>");
|
||||||
|
builder.Append("</Feature>");
|
||||||
|
|
||||||
|
builder.Append("</Features>");
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
||||||
|
/// <param name="key">The key.</param>
|
||||||
|
/// <param name="defaultValue">The defaultValue.</param>
|
||||||
|
/// <returns>The <see cref="string"/>.</returns>
|
||||||
|
public static string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue)
|
||||||
|
{
|
||||||
|
if (sparams != null && sparams.TryGetValue(key, out string val))
|
||||||
|
{
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds the "Browse" xml response.
|
/// Builds the "Browse" xml response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||||
/// <param name="sparams">The method parameters.</param>
|
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
||||||
/// <param name="deviceId">The device Id to use.</param>
|
/// <param name="deviceId">The device Id to use.</param>
|
||||||
private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
|
private void HandleBrowse(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
|
||||||
{
|
{
|
||||||
var id = sparams["ObjectID"];
|
var id = sparams["ObjectID"];
|
||||||
var flag = sparams["BrowseFlag"];
|
var flag = sparams["BrowseFlag"];
|
||||||
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
|
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
|
||||||
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
|
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
|
||||||
|
|
||||||
var provided = 0;
|
var provided = 0;
|
||||||
|
|
||||||
@@ -411,9 +435,9 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
/// Builds the response to the "X_BrowseByLetter request.
|
/// Builds the response to the "X_BrowseByLetter request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||||
/// <param name="sparams">The method parameters.</param>
|
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
||||||
/// <param name="deviceId">The device id.</param>
|
/// <param name="deviceId">The device id.</param>
|
||||||
private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
|
private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
|
||||||
{
|
{
|
||||||
// TODO: Implement this method
|
// TODO: Implement this method
|
||||||
HandleSearch(xmlWriter, sparams, deviceId);
|
HandleSearch(xmlWriter, sparams, deviceId);
|
||||||
@@ -423,13 +447,13 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
/// Builds a response to the "Search" request.
|
/// Builds a response to the "Search" request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param>
|
/// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param>
|
||||||
/// <param name="sparams">The method parameters.</param>
|
/// <param name="sparams">The sparams<see cref="IDictionary"/>.</param>
|
||||||
/// <param name="deviceId">The deviceId<see cref="string"/>.</param>
|
/// <param name="deviceId">The deviceId<see cref="string"/>.</param>
|
||||||
private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
|
private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
|
||||||
{
|
{
|
||||||
var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty));
|
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
|
||||||
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
|
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
|
||||||
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
|
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
|
||||||
|
|
||||||
// sort example: dc:title, dc:date
|
// sort example: dc:title, dc:date
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
{
|
{
|
||||||
Item = item;
|
Item = item;
|
||||||
|
|
||||||
if (item is IItemByName && item is not Folder)
|
if (item is IItemByName && !(item is Folder))
|
||||||
{
|
{
|
||||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1602
|
||||||
|
|
||||||
namespace Emby.Dlna.ContentDirectory
|
namespace Emby.Dlna.ContentDirectory
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
public class ControlResponse
|
public class ControlResponse
|
||||||
{
|
{
|
||||||
public ControlResponse(string xml, bool isSuccessful)
|
public ControlResponse()
|
||||||
{
|
{
|
||||||
Headers = new Dictionary<string, string>();
|
Headers = new Dictionary<string, string>();
|
||||||
Xml = xml;
|
|
||||||
IsSuccessful = isSuccessful;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<string, string> Headers { get; }
|
public IDictionary<string, string> Headers { get; }
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
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 NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||||
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
|
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 DeviceProfile _profile;
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
private readonly string _serverAddress;
|
private readonly string _serverAddress;
|
||||||
@@ -208,8 +208,7 @@ namespace Emby.Dlna.Didl
|
|||||||
var targetWidth = streamInfo.TargetWidth;
|
var targetWidth = streamInfo.TargetWidth;
|
||||||
var targetHeight = streamInfo.TargetHeight;
|
var targetHeight = streamInfo.TargetHeight;
|
||||||
|
|
||||||
var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
|
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
|
||||||
_profile,
|
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
@@ -315,7 +314,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (mediaSource.RunTimeTicks.HasValue)
|
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"))
|
if (filter.Contains("res@size"))
|
||||||
@@ -326,7 +325,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (size.HasValue)
|
if (size.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,7 +339,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (targetChannels.HasValue)
|
if (targetChannels.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.Contains("res@resolution"))
|
if (filter.Contains("res@resolution"))
|
||||||
@@ -359,12 +358,12 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (targetSampleRate.HasValue)
|
if (targetSampleRate.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalBitrate.HasValue)
|
if (totalBitrate.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaProfile = _profile.GetVideoMediaProfile(
|
var mediaProfile = _profile.GetVideoMediaProfile(
|
||||||
@@ -550,7 +549,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (mediaSource.RunTimeTicks.HasValue)
|
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"))
|
if (filter.Contains("res@size"))
|
||||||
@@ -561,7 +560,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (size.HasValue)
|
if (size.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -573,17 +572,17 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (targetChannels.HasValue)
|
if (targetChannels.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetSampleRate.HasValue)
|
if (targetSampleRate.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetAudioBitrate.HasValue)
|
if (targetAudioBitrate.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaProfile = _profile.GetAudioMediaProfile(
|
var mediaProfile = _profile.GetAudioMediaProfile(
|
||||||
@@ -600,8 +599,7 @@ namespace Emby.Dlna.Didl
|
|||||||
? MimeTypes.GetMimeType(filename)
|
? MimeTypes.GetMimeType(filename)
|
||||||
: mediaProfile.MimeType;
|
: mediaProfile.MimeType;
|
||||||
|
|
||||||
var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
|
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
|
||||||
_profile,
|
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
targetAudioBitrate,
|
targetAudioBitrate,
|
||||||
@@ -637,7 +635,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
writer.WriteAttributeString("restricted", "1");
|
writer.WriteAttributeString("restricted", "1");
|
||||||
writer.WriteAttributeString("searchable", "1");
|
writer.WriteAttributeString("searchable", "1");
|
||||||
writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("childCount", childCount.ToString(_usCulture));
|
||||||
|
|
||||||
var clientId = GetClientId(folder, stubType);
|
var clientId = GetClientId(folder, stubType);
|
||||||
|
|
||||||
@@ -746,7 +744,7 @@ namespace Emby.Dlna.Didl
|
|||||||
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
|
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is not Folder)
|
if (!(item is Folder))
|
||||||
{
|
{
|
||||||
if (filter.Contains("dc:description"))
|
if (filter.Contains("dc:description"))
|
||||||
{
|
{
|
||||||
@@ -929,11 +927,11 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (item.IndexNumber.HasValue)
|
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)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1048,7 +1046,8 @@ namespace Emby.Dlna.Didl
|
|||||||
var width = albumartUrlInfo.width ?? maxWidth;
|
var width = albumartUrlInfo.width ?? maxWidth;
|
||||||
var height = albumartUrlInfo.height ?? maxHeight;
|
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(
|
writer.WriteAttributeString(
|
||||||
"protocolInfo",
|
"protocolInfo",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
public class StringWriterWithEncoding : StringWriter
|
public class StringWriterWithEncoding : StringWriter
|
||||||
{
|
{
|
||||||
private readonly Encoding? _encoding;
|
private readonly Encoding _encoding;
|
||||||
|
|
||||||
public StringWriterWithEncoding()
|
public StringWriterWithEncoding()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#nullable enable
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|||||||
+47
-66
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -6,12 +7,10 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna.Profiles;
|
using Emby.Dlna.Profiles;
|
||||||
using Emby.Dlna.Server;
|
using Emby.Dlna.Server;
|
||||||
using Jellyfin.Extensions.Json;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
@@ -33,9 +32,9 @@ namespace Emby.Dlna
|
|||||||
private readonly IXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly ILogger<DlnaManager> _logger;
|
private readonly ILogger<DlnaManager> _logger;
|
||||||
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
||||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
|
||||||
|
|
||||||
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
||||||
|
|
||||||
@@ -44,12 +43,14 @@ namespace Emby.Dlna
|
|||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IApplicationPaths appPaths,
|
IApplicationPaths appPaths,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
|
IJsonSerializer jsonSerializer,
|
||||||
IServerApplicationHost appHost)
|
IServerApplicationHost appHost)
|
||||||
{
|
{
|
||||||
_xmlSerializer = xmlSerializer;
|
_xmlSerializer = xmlSerializer;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
_logger = loggerFactory.CreateLogger<DlnaManager>();
|
_logger = loggerFactory.CreateLogger<DlnaManager>();
|
||||||
|
_jsonSerializer = jsonSerializer;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,14 +94,12 @@ namespace Emby.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public DeviceProfile GetDefaultProfile()
|
public DeviceProfile GetDefaultProfile()
|
||||||
{
|
{
|
||||||
return new DefaultProfile();
|
return new DefaultProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
|
||||||
public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
|
|
||||||
{
|
{
|
||||||
if (deviceInfo == null)
|
if (deviceInfo == null)
|
||||||
{
|
{
|
||||||
@@ -110,13 +109,13 @@ namespace Emby.Dlna
|
|||||||
var profile = GetProfiles()
|
var profile = GetProfiles()
|
||||||
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
|
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
|
||||||
|
|
||||||
if (profile == null)
|
if (profile != null)
|
||||||
{
|
{
|
||||||
LogUnmatchedProfile(deviceInfo);
|
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
LogUnmatchedProfile(deviceInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
@@ -127,14 +126,14 @@ namespace Emby.Dlna
|
|||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
||||||
builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
|
builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
|
||||||
builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
|
builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
|
||||||
builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
|
builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
|
||||||
builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
|
builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
|
||||||
builder.Append("ModelName: ").AppendLine(profile.ModelName);
|
builder.Append("ModelName:").AppendLine(profile.ModelName);
|
||||||
builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
|
builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
|
||||||
builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
|
builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
|
||||||
builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
|
builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
|
||||||
|
|
||||||
_logger.LogInformation(builder.ToString());
|
_logger.LogInformation(builder.ToString());
|
||||||
}
|
}
|
||||||
@@ -186,8 +185,7 @@ namespace Emby.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
public DeviceProfile GetProfile(IHeaderDictionary headers)
|
||||||
public DeviceProfile? GetProfile(IHeaderDictionary headers)
|
|
||||||
{
|
{
|
||||||
if (headers == null)
|
if (headers == null)
|
||||||
{
|
{
|
||||||
@@ -195,13 +193,15 @@ namespace Emby.Dlna
|
|||||||
}
|
}
|
||||||
|
|
||||||
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
|
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
|
||||||
if (profile == null)
|
|
||||||
|
if (profile != null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("No matching device profile found. {@Headers}", headers);
|
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||||
}
|
}
|
||||||
else
|
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;
|
return profile;
|
||||||
@@ -251,19 +251,19 @@ namespace Emby.Dlna
|
|||||||
return xmlFies
|
return xmlFies
|
||||||
.Select(i => ParseProfileFile(i, type))
|
.Select(i => ParseProfileFile(i, type))
|
||||||
.Where(i => i != null)
|
.Where(i => i != null)
|
||||||
.ToList()!; // We just filtered out all the nulls
|
.ToList();
|
||||||
}
|
}
|
||||||
catch (IOException)
|
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)
|
lock (_profiles)
|
||||||
{
|
{
|
||||||
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
|
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple))
|
||||||
{
|
{
|
||||||
return profileTuple.Item2;
|
return profileTuple.Item2;
|
||||||
}
|
}
|
||||||
@@ -291,8 +291,7 @@ namespace Emby.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
public DeviceProfile GetProfile(string id)
|
||||||
public DeviceProfile? GetProfile(string id)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(id))
|
if (string.IsNullOrEmpty(id))
|
||||||
{
|
{
|
||||||
@@ -321,7 +320,6 @@ namespace Emby.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
|
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
|
||||||
{
|
{
|
||||||
return GetProfileInfosInternal().Select(i => i.Info);
|
return GetProfileInfosInternal().Select(i => i.Info);
|
||||||
@@ -329,14 +327,17 @@ namespace Emby.Dlna
|
|||||||
|
|
||||||
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
|
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
|
||||||
{
|
{
|
||||||
return new InternalProfileInfo(
|
return new InternalProfileInfo
|
||||||
new DeviceProfileInfo
|
{
|
||||||
|
Path = file.FullName,
|
||||||
|
|
||||||
|
Info = new DeviceProfileInfo
|
||||||
{
|
{
|
||||||
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
|
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
|
||||||
Name = _fileSystem.GetFileNameWithoutExtension(file),
|
Name = _fileSystem.GetFileNameWithoutExtension(file),
|
||||||
Type = type
|
Type = type
|
||||||
},
|
}
|
||||||
file.FullName);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExtractSystemProfilesAsync()
|
private async Task ExtractSystemProfilesAsync()
|
||||||
@@ -356,20 +357,16 @@ namespace Emby.Dlna
|
|||||||
systemProfilesPath,
|
systemProfilesPath,
|
||||||
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
|
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);
|
var fileInfo = _fileSystem.GetFileInfo(path);
|
||||||
|
|
||||||
if (!fileInfo.Exists || fileInfo.Length != length)
|
if (!fileInfo.Exists || fileInfo.Length != stream.Length)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(systemProfilesPath);
|
Directory.CreateDirectory(systemProfilesPath);
|
||||||
|
|
||||||
var fileOptions = AsyncFile.WriteOptions;
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
fileOptions.Mode = FileMode.CreateNew;
|
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
fileOptions.PreallocationSize = length;
|
|
||||||
using (var fileStream = new FileStream(path, fileOptions))
|
|
||||||
{
|
{
|
||||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -381,7 +378,6 @@ namespace Emby.Dlna
|
|||||||
Directory.CreateDirectory(UserProfilesPath);
|
Directory.CreateDirectory(UserProfilesPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void DeleteProfile(string id)
|
public void DeleteProfile(string id)
|
||||||
{
|
{
|
||||||
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
|
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -399,7 +395,6 @@ namespace Emby.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void CreateProfile(DeviceProfile profile)
|
public void CreateProfile(DeviceProfile profile)
|
||||||
{
|
{
|
||||||
profile = ReserializeProfile(profile);
|
profile = ReserializeProfile(profile);
|
||||||
@@ -415,7 +410,6 @@ namespace Emby.Dlna
|
|||||||
SaveProfile(profile, path, DeviceProfileType.User);
|
SaveProfile(profile, path, DeviceProfileType.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void UpdateProfile(DeviceProfile profile)
|
public void UpdateProfile(DeviceProfile profile)
|
||||||
{
|
{
|
||||||
profile = ReserializeProfile(profile);
|
profile = ReserializeProfile(profile);
|
||||||
@@ -472,13 +466,11 @@ namespace Emby.Dlna
|
|||||||
return profile;
|
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.DeserializeFromString<DeviceProfile>(json);
|
||||||
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
|
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
|
||||||
{
|
{
|
||||||
var profile = GetDefaultProfile();
|
var profile = GetDefaultProfile();
|
||||||
@@ -488,37 +480,26 @@ namespace Emby.Dlna
|
|||||||
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
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)
|
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
||||||
? ImageFormat.Png
|
? ImageFormat.Png
|
||||||
: ImageFormat.Jpg;
|
: ImageFormat.Jpg;
|
||||||
|
|
||||||
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
|
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
|
private class InternalProfileInfo
|
||||||
{
|
{
|
||||||
internal InternalProfileInfo(DeviceProfileInfo info, string path)
|
internal DeviceProfileInfo Info { get; set; }
|
||||||
{
|
|
||||||
Info = info;
|
|
||||||
Path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal DeviceProfileInfo Info { get; }
|
internal string Path { get; set; }
|
||||||
|
|
||||||
internal string Path { get; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,7 +524,7 @@ namespace Emby.Dlna
|
|||||||
|
|
||||||
private void DumpProfiles()
|
private void DumpProfiles()
|
||||||
{
|
{
|
||||||
DeviceProfile[] list = new[]
|
DeviceProfile[] list = new []
|
||||||
{
|
{
|
||||||
new SamsungSmartTvProfile(),
|
new SamsungSmartTvProfile(),
|
||||||
new XboxOneProfile(),
|
new XboxOneProfile(),
|
||||||
|
|||||||
@@ -17,18 +17,24 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Images\logo120.jpg" />
|
<EmbeddedResource Include="Images\logo120.jpg" />
|
||||||
<EmbeddedResource Include="Images\logo120.png" />
|
<EmbeddedResource Include="Images\logo120.png" />
|
||||||
@@ -72,7 +78,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" />
|
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
public class EventSubscriptionResponse
|
public class EventSubscriptionResponse
|
||||||
{
|
{
|
||||||
public EventSubscriptionResponse(string content, string contentType)
|
public EventSubscriptionResponse()
|
||||||
{
|
{
|
||||||
Content = content;
|
|
||||||
ContentType = contentType;
|
|
||||||
Headers = new Dictionary<string, string>();
|
Headers = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@@ -11,7 +9,6 @@ using System.Net.Http;
|
|||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Extensions;
|
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -26,6 +23,8 @@ namespace Emby.Dlna.Eventing
|
|||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
|
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||||
|
|
||||||
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
|
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
@@ -50,7 +49,11 @@ namespace Emby.Dlna.Eventing
|
|||||||
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
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)
|
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||||
@@ -81,7 +84,9 @@ namespace Emby.Dlna.Eventing
|
|||||||
if (!string.IsNullOrEmpty(header))
|
if (!string.IsNullOrEmpty(header))
|
||||||
{
|
{
|
||||||
// Starts with SECOND-
|
// 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;
|
return val;
|
||||||
}
|
}
|
||||||
@@ -96,15 +101,23 @@ namespace Emby.Dlna.Eventing
|
|||||||
|
|
||||||
_subscriptions.TryRemove(subscriptionId, out _);
|
_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)
|
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["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;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -161,7 +174,7 @@ namespace Emby.Dlna.Eventing
|
|||||||
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
|
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
|
||||||
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
|
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
|
||||||
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
|
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
|
||||||
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
|
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@@ -7,6 +5,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna.PlayTo;
|
using Emby.Dlna.PlayTo;
|
||||||
using Emby.Dlna.Ssdp;
|
using Emby.Dlna.Ssdp;
|
||||||
@@ -27,9 +26,11 @@ using MediaBrowser.Controller.TV;
|
|||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
|
using MediaBrowser.Model.System;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Rssdp;
|
using Rssdp;
|
||||||
using Rssdp.Infrastructure;
|
using Rssdp.Infrastructure;
|
||||||
|
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
||||||
|
|
||||||
namespace Emby.Dlna.Main
|
namespace Emby.Dlna.Main
|
||||||
{
|
{
|
||||||
@@ -202,8 +203,8 @@ namespace Emby.Dlna.Main
|
|||||||
{
|
{
|
||||||
if (_communicationsServer == null)
|
if (_communicationsServer == null)
|
||||||
{
|
{
|
||||||
var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
|
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
|
||||||
OperatingSystem.IsLinux();
|
OperatingSystem.Id == OperatingSystemId.Linux;
|
||||||
|
|
||||||
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||||
{
|
{
|
||||||
@@ -266,12 +267,7 @@ namespace Emby.Dlna.Main
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_publisher = new SsdpDevicePublisher(
|
_publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||||
_communicationsServer,
|
|
||||||
_networkManager,
|
|
||||||
MediaBrowser.Common.System.OperatingSystem.Name,
|
|
||||||
Environment.OSVersion.VersionString,
|
|
||||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
|
||||||
{
|
{
|
||||||
LogFunction = LogMessage,
|
LogFunction = LogMessage,
|
||||||
SupportPnpRootDevice = false
|
SupportPnpRootDevice = false
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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))
|
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
using Emby.Dlna.Service;
|
using Emby.Dlna.Service;
|
||||||
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
||||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||||
{
|
{
|
||||||
|
|||||||
+33
-93
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@@ -20,6 +18,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class Device : IDisposable
|
public class Device : IDisposable
|
||||||
{
|
{
|
||||||
|
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||||
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
@@ -235,13 +235,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_logger.LogDebug("Setting mute");
|
_logger.LogDebug("Setting mute");
|
||||||
var value = mute ? 1 : 0;
|
var value = mute ? 1 : 0;
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory)
|
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
||||||
.SendCommandAsync(
|
|
||||||
Properties.BaseUrl,
|
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
rendererCommands.BuildPost(command, service.ServiceType, value),
|
|
||||||
cancellationToken: cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
IsMuted = mute;
|
IsMuted = mute;
|
||||||
@@ -276,13 +270,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
// Remote control will perform better
|
// Remote control will perform better
|
||||||
Volume = value;
|
Volume = value;
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory)
|
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
||||||
.SendCommandAsync(
|
|
||||||
Properties.BaseUrl,
|
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
rendererCommands.BuildPost(command, service.ServiceType, value),
|
|
||||||
cancellationToken: cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,13 +291,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
throw new InvalidOperationException("Unable to find service");
|
throw new InvalidOperationException("Unable to find service");
|
||||||
}
|
}
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory)
|
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"))
|
||||||
.SendCommandAsync(
|
|
||||||
Properties.BaseUrl,
|
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
|
|
||||||
cancellationToken: cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
@@ -343,21 +325,14 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
||||||
await new SsdpHttpClient(_httpClientFactory)
|
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
|
||||||
.SendCommandAsync(
|
|
||||||
Properties.BaseUrl,
|
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
post,
|
|
||||||
header: header,
|
|
||||||
cancellationToken: cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(50).ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -368,42 +343,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
RestartTimer(true);
|
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)
|
private static string CreateDidlMeta(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
@@ -461,13 +400,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory)
|
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
||||||
.SendCommandAsync(
|
|
||||||
Properties.BaseUrl,
|
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
avCommands.BuildPost(command, service.ServiceType, 1),
|
|
||||||
cancellationToken: cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
@@ -485,13 +418,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory)
|
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
||||||
.SendCommandAsync(
|
|
||||||
Properties.BaseUrl,
|
|
||||||
service,
|
|
||||||
command.Name,
|
|
||||||
avCommands.BuildPost(command, service.ServiceType, 1),
|
|
||||||
cancellationToken: cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
TransportState = TransportState.Paused;
|
TransportState = TransportState.Paused;
|
||||||
@@ -638,7 +565,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
|
Volume = int.Parse(volumeValue, UsCulture);
|
||||||
|
|
||||||
if (Volume > 0)
|
if (Volume > 0)
|
||||||
{
|
{
|
||||||
@@ -840,7 +767,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
if (!string.IsNullOrWhiteSpace(duration)
|
if (!string.IsNullOrWhiteSpace(duration)
|
||||||
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
|
Duration = TimeSpan.Parse(duration, UsCulture);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -852,7 +779,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
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();
|
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
|
||||||
@@ -1088,7 +1015,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var deviceProperties = new DeviceInfo()
|
var deviceProperties = new DeviceInfo()
|
||||||
{
|
{
|
||||||
Name = string.Join(' ', friendlyNames),
|
Name = string.Join(" ", friendlyNames),
|
||||||
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
|
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1192,8 +1119,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
|
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
|
||||||
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
|
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
|
||||||
|
|
||||||
var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
|
||||||
var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
|
||||||
|
|
||||||
return new DeviceIcon
|
return new DeviceIcon
|
||||||
{
|
{
|
||||||
@@ -1258,7 +1185,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
|
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
|
||||||
|
{
|
||||||
|
MediaInfo = mediaInfo
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackProgress(UBaseObject mediaInfo)
|
private void OnPlaybackProgress(UBaseObject mediaInfo)
|
||||||
@@ -1268,17 +1198,27 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo));
|
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
|
||||||
|
{
|
||||||
|
MediaInfo = mediaInfo
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackStop(UBaseObject 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)
|
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
|
||||||
{
|
{
|
||||||
MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia));
|
MediaChanged?.Invoke(this, new MediaChangedEventArgs
|
||||||
|
{
|
||||||
|
OldMediaInfo = old,
|
||||||
|
NewMediaInfo = newMedia
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -6,12 +6,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class MediaChangedEventArgs : EventArgs
|
public class MediaChangedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
|
|
||||||
{
|
|
||||||
OldMediaInfo = oldMediaInfo;
|
|
||||||
NewMediaInfo = newMediaInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UBaseObject OldMediaInfo { get; set; }
|
public UBaseObject OldMediaInfo { get; set; }
|
||||||
|
|
||||||
public UBaseObject NewMediaInfo { get; set; }
|
public UBaseObject NewMediaInfo { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@@ -30,6 +28,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class PlayToController : ISessionController, IDisposable
|
public class PlayToController : ISessionController, IDisposable
|
||||||
{
|
{
|
||||||
|
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
|
||||||
|
|
||||||
private readonly SessionInfo _session;
|
private readonly SessionInfo _session;
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
@@ -102,22 +102,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
_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()
|
private void OnDeviceUnavailable()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -172,15 +156,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var newItemProgress = GetProgressInfo(streamInfo);
|
var newItemProgress = GetProgressInfo(streamInfo);
|
||||||
|
|
||||||
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
||||||
|
|
||||||
// Send a message to the DLNA device to notify what is the next track in the playlist.
|
|
||||||
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
|
|
||||||
if (currentItemIndex >= 0)
|
|
||||||
{
|
|
||||||
_currentPlaylistIndex = currentItemIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -450,11 +425,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
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);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
|
||||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
|
||||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,8 +499,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
||||||
{
|
{
|
||||||
return ContentFeatureBuilder.BuildAudioHeader(
|
return new ContentFeatureBuilder(profile)
|
||||||
profile,
|
.BuildAudioHeader(
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioBitrate,
|
streamInfo.TargetAudioBitrate,
|
||||||
@@ -544,8 +514,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
if (streamInfo.MediaType == DlnaProfileType.Video)
|
if (streamInfo.MediaType == DlnaProfileType.Video)
|
||||||
{
|
{
|
||||||
var list = ContentFeatureBuilder.BuildVideoHeader(
|
var list = new ContentFeatureBuilder(profile)
|
||||||
profile,
|
.BuildVideoHeader(
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
@@ -653,9 +623,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
|
||||||
await SendNextTrackMessage(index, cancellationToken);
|
|
||||||
|
|
||||||
var streamInfo = currentitem.StreamInfo;
|
var streamInfo = currentitem.StreamInfo;
|
||||||
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
||||||
{
|
{
|
||||||
@@ -714,7 +681,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
case GeneralCommandType.SetAudioStreamIndex:
|
case GeneralCommandType.SetAudioStreamIndex:
|
||||||
if (command.Arguments.TryGetValue("Index", out string index))
|
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);
|
return SetAudioStreamIndex(val);
|
||||||
}
|
}
|
||||||
@@ -726,7 +693,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
case GeneralCommandType.SetSubtitleStreamIndex:
|
case GeneralCommandType.SetSubtitleStreamIndex:
|
||||||
if (command.Arguments.TryGetValue("Index", out index))
|
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);
|
return SetSubtitleStreamIndex(val);
|
||||||
}
|
}
|
||||||
@@ -738,7 +705,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
case GeneralCommandType.SetVolume:
|
case GeneralCommandType.SetVolume:
|
||||||
if (command.Arguments.TryGetValue("Volume", out string vol))
|
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);
|
return _device.SetVolume(volume, cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -769,10 +736,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
|
||||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
|
||||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
|
||||||
|
|
||||||
if (EnableClientSideSeek(newItem.StreamInfo))
|
if (EnableClientSideSeek(newItem.StreamInfo))
|
||||||
{
|
{
|
||||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||||
@@ -798,10 +761,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
|
||||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
|
||||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
|
||||||
|
|
||||||
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
||||||
{
|
{
|
||||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||||
@@ -818,7 +777,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var currentWait = 0;
|
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;
|
currentWait += Interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@@ -173,9 +171,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessionInfo = await _sessionManager
|
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
|
||||||
.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
|
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class PlaybackProgressEventArgs : EventArgs
|
public class PlaybackProgressEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public PlaybackProgressEventArgs(UBaseObject mediaInfo)
|
|
||||||
{
|
|
||||||
MediaInfo = mediaInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UBaseObject MediaInfo { get; set; }
|
public UBaseObject MediaInfo { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class PlaybackStartEventArgs : EventArgs
|
public class PlaybackStartEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public PlaybackStartEventArgs(UBaseObject mediaInfo)
|
|
||||||
{
|
|
||||||
MediaInfo = mediaInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UBaseObject MediaInfo { get; set; }
|
public UBaseObject MediaInfo { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class PlaybackStoppedEventArgs : EventArgs
|
public class PlaybackStoppedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
|
|
||||||
{
|
|
||||||
MediaInfo = mediaInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UBaseObject MediaInfo { get; set; }
|
public UBaseObject MediaInfo { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Text;
|
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 USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
|
||||||
private const string FriendlyName = "Jellyfin";
|
private const string FriendlyName = "Jellyfin";
|
||||||
|
|
||||||
|
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||||
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
public SsdpHttpClient(IHttpClientFactory httpClientFactory)
|
public SsdpHttpClient(IHttpClientFactory httpClientFactory)
|
||||||
@@ -43,12 +44,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
header,
|
header,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
return await XDocument.LoadAsync(
|
return await XDocument.LoadAsync(
|
||||||
stream,
|
stream,
|
||||||
LoadOptions.None,
|
LoadOptions.PreserveWhitespace,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,15 +77,14 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
|
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
|
||||||
options.Headers.UserAgent.ParseAdd(USERAGENT);
|
options.Headers.UserAgent.ParseAdd(USERAGENT);
|
||||||
options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
|
options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
|
||||||
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
|
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
|
||||||
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
|
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)
|
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
|
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
|
public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
|
||||||
@@ -95,13 +93,12 @@ namespace Emby.Dlna.PlayTo
|
|||||||
options.Headers.UserAgent.ParseAdd(USERAGENT);
|
options.Headers.UserAgent.ParseAdd(USERAGENT);
|
||||||
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
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);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await XDocument.LoadAsync(
|
return await XDocument.LoadAsync(
|
||||||
stream,
|
stream,
|
||||||
LoadOptions.None,
|
LoadOptions.PreserveWhitespace,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
var serviceAction = new ServiceAction
|
var serviceAction = new ServiceAction
|
||||||
{
|
{
|
||||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||||
};
|
};
|
||||||
|
|
||||||
var argumentList = serviceAction.ArgumentList;
|
var argumentList = serviceAction.ArgumentList;
|
||||||
@@ -68,9 +68,9 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
return new Argument
|
return new Argument
|
||||||
{
|
{
|
||||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||||
Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
|
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
|
||||||
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
|
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +89,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
return new StateVariable
|
return new StateVariable
|
||||||
{
|
{
|
||||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||||
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
|
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
|
||||||
AllowedValues = allowedValues
|
AllowedValues = allowedValues
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
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));
|
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1602
|
||||||
|
|
||||||
namespace Emby.Dlna.PlayTo
|
namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
|
|
||||||
Identification = new DeviceIdentification
|
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",
|
Manufacturer = "Sony",
|
||||||
|
|
||||||
Headers = new[]
|
Headers = new[]
|
||||||
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
new HttpHeaderInfo
|
new HttpHeaderInfo
|
||||||
{
|
{
|
||||||
Name = "X-AV-Client-Info",
|
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
|
Match = HeaderMatchType.Regex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
|
|
||||||
Identification = new DeviceIdentification
|
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",
|
Manufacturer = "Sony",
|
||||||
|
|
||||||
Headers = new[]
|
Headers = new[]
|
||||||
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
new HttpHeaderInfo
|
new HttpHeaderInfo
|
||||||
{
|
{
|
||||||
Name = "X-AV-Client-Info",
|
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
|
Match = HeaderMatchType.Regex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
|
|
||||||
Identification = new DeviceIdentification
|
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",
|
Manufacturer = "Sony",
|
||||||
|
|
||||||
Headers = new[]
|
Headers = new[]
|
||||||
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
new HttpHeaderInfo
|
new HttpHeaderInfo
|
||||||
{
|
{
|
||||||
Name = "X-AV-Client-Info",
|
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
|
Match = HeaderMatchType.Regex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
|
|
||||||
Identification = new DeviceIdentification
|
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",
|
Manufacturer = "Sony",
|
||||||
|
|
||||||
Headers = new[]
|
Headers = new[]
|
||||||
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
new HttpHeaderInfo
|
new HttpHeaderInfo
|
||||||
{
|
{
|
||||||
Name = "X-AV-Client-Info",
|
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
|
Match = HeaderMatchType.Regex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
|
|
||||||
Identification = new DeviceIdentification
|
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",
|
Manufacturer = "Sony",
|
||||||
|
|
||||||
Headers = new[]
|
Headers = new[]
|
||||||
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
new HttpHeaderInfo
|
new HttpHeaderInfo
|
||||||
{
|
{
|
||||||
Name = "X-AV-Client-Info",
|
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
|
Match = HeaderMatchType.Regex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<Name>Sony Bravia (2010)</Name>
|
<Name>Sony Bravia (2010)</Name>
|
||||||
<Identification>
|
<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>
|
<Manufacturer>Sony</Manufacturer>
|
||||||
<Headers>
|
<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>
|
</Headers>
|
||||||
</Identification>
|
</Identification>
|
||||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<Name>Sony Bravia (2011)</Name>
|
<Name>Sony Bravia (2011)</Name>
|
||||||
<Identification>
|
<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>
|
<Manufacturer>Sony</Manufacturer>
|
||||||
<Headers>
|
<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>
|
</Headers>
|
||||||
</Identification>
|
</Identification>
|
||||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<Name>Sony Bravia (2012)</Name>
|
<Name>Sony Bravia (2012)</Name>
|
||||||
<Identification>
|
<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>
|
<Manufacturer>Sony</Manufacturer>
|
||||||
<Headers>
|
<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>
|
</Headers>
|
||||||
</Identification>
|
</Identification>
|
||||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<Name>Sony Bravia (2013)</Name>
|
<Name>Sony Bravia (2013)</Name>
|
||||||
<Identification>
|
<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>
|
<Manufacturer>Sony</Manufacturer>
|
||||||
<Headers>
|
<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>
|
</Headers>
|
||||||
</Identification>
|
</Identification>
|
||||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<Name>Sony Bravia (2014)</Name>
|
<Name>Sony Bravia (2014)</Name>
|
||||||
<Identification>
|
<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>
|
<Manufacturer>Sony</Manufacturer>
|
||||||
<Headers>
|
<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>
|
</Headers>
|
||||||
</Identification>
|
</Identification>
|
||||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace Emby.Dlna.Server
|
|||||||
{
|
{
|
||||||
private readonly DeviceProfile _profile;
|
private readonly DeviceProfile _profile;
|
||||||
|
|
||||||
|
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||||
private readonly string _serverUdn;
|
private readonly string _serverUdn;
|
||||||
private readonly string _serverAddress;
|
private readonly string _serverAddress;
|
||||||
private readonly string _serverName;
|
private readonly string _serverName;
|
||||||
@@ -192,10 +193,10 @@ namespace Emby.Dlna.Server
|
|||||||
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
|
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
|
||||||
.Append("</mimetype>");
|
.Append("</mimetype>");
|
||||||
builder.Append("<width>")
|
builder.Append("<width>")
|
||||||
.Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
|
.Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
|
||||||
.Append("</width>");
|
.Append("</width>");
|
||||||
builder.Append("<height>")
|
builder.Append("<height>")
|
||||||
.Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
|
.Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
|
||||||
.Append("</height>");
|
.Append("</height>");
|
||||||
builder.Append("<depth>")
|
builder.Append("<depth>")
|
||||||
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
|
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ using System.IO;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Diacritics.Extensions;
|
|
||||||
using Emby.Dlna.Didl;
|
using Emby.Dlna.Didl;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Controller.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Dlna.Service
|
namespace Emby.Dlna.Service
|
||||||
@@ -47,9 +47,9 @@ namespace Emby.Dlna.Service
|
|||||||
|
|
||||||
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
|
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
|
||||||
{
|
{
|
||||||
ControlRequestInfo? requestInfo = null;
|
ControlRequestInfo requestInfo = null;
|
||||||
|
|
||||||
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
|
using (var streamReader = new StreamReader(request.InputXml))
|
||||||
{
|
{
|
||||||
var readerSettings = new XmlReaderSettings()
|
var readerSettings = new XmlReaderSettings()
|
||||||
{
|
{
|
||||||
@@ -95,7 +95,11 @@ namespace Emby.Dlna.Service
|
|||||||
|
|
||||||
var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
|
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);
|
controlResponse.Headers.Add("EXT", string.Empty);
|
||||||
|
|
||||||
@@ -147,7 +151,7 @@ namespace Emby.Dlna.Service
|
|||||||
|
|
||||||
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
||||||
{
|
{
|
||||||
string? namespaceURI = null, localName = null;
|
string namespaceURI = null, localName = null;
|
||||||
|
|
||||||
await reader.MoveToContentAsync().ConfigureAwait(false);
|
await reader.MoveToContentAsync().ConfigureAwait(false);
|
||||||
await reader.ReadAsync().ConfigureAwait(false);
|
await reader.ReadAsync().ConfigureAwait(false);
|
||||||
@@ -206,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)
|
private void LogRequest(ControlRequest request)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ namespace Emby.Dlna.Service
|
|||||||
return EventManager.CancelEventSubscription(subscriptionId);
|
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();
|
writer.WriteEndDocument();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ControlResponse(builder.ToString(), false);
|
return new ControlResponse
|
||||||
|
{
|
||||||
|
Xml = builder.ToString(),
|
||||||
|
IsSuccessful = false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|||||||
@@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp
|
|||||||
{
|
{
|
||||||
public static class SsdpExtensions
|
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);
|
var node = container.Element(name);
|
||||||
|
|
||||||
return node?.Value;
|
return node?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string? GetAttributeValue(this XElement container, XName name)
|
public static string GetAttributeValue(this XElement container, XName name)
|
||||||
{
|
{
|
||||||
var node = container.Attribute(name);
|
var node = container.Attribute(name);
|
||||||
|
|
||||||
return node?.Value;
|
return node?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string? GetDescendantValue(this XElement container, XName name)
|
public static string GetDescendantValue(this XElement container, XName name)
|
||||||
=> container.Descendants(name).FirstOrDefault()?.Value;
|
=> container.Descendants(name).FirstOrDefault()?.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -23,9 +25,14 @@
|
|||||||
|
|
||||||
<!-- Code analysers-->
|
<!-- Code analysers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ namespace Emby.Drawing
|
|||||||
{
|
{
|
||||||
var file = await ProcessImage(options).ConfigureAwait(false);
|
var file = await ProcessImage(options).ConfigureAwait(false);
|
||||||
|
|
||||||
using (var fileStream = AsyncFile.OpenRead(file.Item1))
|
using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
|
||||||
{
|
{
|
||||||
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -197,6 +197,11 @@ namespace Emby.Drawing
|
|||||||
{
|
{
|
||||||
if (!File.Exists(cacheFilePath))
|
if (!File.Exists(cacheFilePath))
|
||||||
{
|
{
|
||||||
|
if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
|
||||||
|
{
|
||||||
|
options.CropWhiteSpace = false;
|
||||||
|
}
|
||||||
|
|
||||||
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
|
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
|
||||||
|
|
||||||
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -426,13 +431,8 @@ namespace Emby.Drawing
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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()
|
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
|
||||||
.ToString("N", CultureInfo.InvariantCulture);
|
.ToString("N", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace Emby.Drawing
|
|||||||
=> throw new NotImplementedException();
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using Jellyfin.Extensions;
|
|
||||||
|
|
||||||
namespace Emby.Naming.Audio
|
namespace Emby.Naming.Audio
|
||||||
{
|
{
|
||||||
@@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
|
|||||||
/// <returns>True if file at path is audio file.</returns>
|
/// <returns>True if file at path is audio file.</returns>
|
||||||
public static bool IsAudioFile(string path, NamingOptions options)
|
public static bool IsAudioFile(string path, NamingOptions options)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path.AsSpan());
|
var extension = Path.GetExtension(path);
|
||||||
return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
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="files">List of files composing the actual audiobook.</param>
|
||||||
/// <param name="extras">List of extra files.</param>
|
/// <param name="extras">List of extra files.</param>
|
||||||
/// <param name="alternateVersions">Alternative version of 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;
|
Name = name;
|
||||||
Year = year;
|
Year = year;
|
||||||
Files = files;
|
Files = files ?? new List<AudioBookFileInfo>();
|
||||||
Extras = extras;
|
Extras = extras ?? new List<AudioBookFileInfo>();
|
||||||
AlternateVersions = alternateVersions;
|
AlternateVersions = alternateVersions ?? new List<AudioBookFileInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -39,18 +39,18 @@ namespace Emby.Naming.AudioBook
|
|||||||
/// Gets or sets the files.
|
/// Gets or sets the files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The files.</value>
|
/// <value>The files.</value>
|
||||||
public IReadOnlyList<AudioBookFileInfo> Files { get; set; }
|
public List<AudioBookFileInfo> Files { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the extras.
|
/// Gets or sets the extras.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The extras.</value>
|
/// <value>The extras.</value>
|
||||||
public IReadOnlyList<AudioBookFileInfo> Extras { get; set; }
|
public List<AudioBookFileInfo> Extras { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the alternate versions.
|
/// Gets or sets the alternate versions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The alternate versions.</value>
|
/// <value>The alternate versions.</value>
|
||||||
public IReadOnlyList<AudioBookFileInfo> AlternateVersions { get; set; }
|
public List<AudioBookFileInfo> AlternateVersions { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook
|
|||||||
|
|
||||||
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
|
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
|
||||||
var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
|
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)
|
foreach (var group in groupedBy)
|
||||||
{
|
{
|
||||||
@@ -87,7 +87,7 @@ namespace Emby.Naming.AudioBook
|
|||||||
foreach (var audioFile in group)
|
foreach (var audioFile in group)
|
||||||
{
|
{
|
||||||
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
|
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
|
||||||
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
|
if (name.Equals("audiobook") ||
|
||||||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
|
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
|
||||||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
|
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -137,11 +137,8 @@ namespace Emby.Naming.Common
|
|||||||
|
|
||||||
CleanStrings = new[]
|
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|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
@"[ _\,\.\(\)\[\]\-](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|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
||||||
@"^(?<cleaned>.+?)(\[.*\])",
|
@"(\[.*\])"
|
||||||
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
|
||||||
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
|
||||||
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SubtitleFileExtensions = new[]
|
SubtitleFileExtensions = new[]
|
||||||
@@ -280,18 +277,12 @@ namespace Emby.Naming.Common
|
|||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
new EpisodeExpression(@"[\\\/\._ \[\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\.[1-9])(?![0-9]))?)([^\\\/]*)$")
|
new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$")
|
||||||
{
|
{
|
||||||
SupportsAbsoluteEpisodeNumbers = true
|
SupportsAbsoluteEpisodeNumbers = true
|
||||||
},
|
},
|
||||||
|
|
||||||
// Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
|
// Case Closed (1996-2007)/Case Closed - 317.mkv
|
||||||
// [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
|
|
||||||
},
|
|
||||||
|
|
||||||
// /server/anything_102.mp4
|
// /server/anything_102.mp4
|
||||||
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
|
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
|
||||||
// /server/anything_1996.11.14.mp4
|
// /server/anything_1996.11.14.mp4
|
||||||
@@ -308,12 +299,11 @@ namespace Emby.Naming.Common
|
|||||||
|
|
||||||
// *** End Kodi Standard Naming
|
// *** End Kodi Standard Naming
|
||||||
|
|
||||||
// "Episode 16", "Episode 16 - Title"
|
// [bar] Foo - 1 [baz]
|
||||||
new EpisodeExpression(@"[Ee]pisode (?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))?[^\\\/]*$")
|
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
|
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
@@ -371,6 +361,12 @@ namespace Emby.Naming.Common
|
|||||||
IsOptimistic = true,
|
IsOptimistic = true,
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
// "Episode 16", "Episode 16 - Title"
|
||||||
|
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
|
||||||
|
{
|
||||||
|
IsOptimistic = true,
|
||||||
|
IsNamed = true
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeWithoutSeasonExpressions = new[]
|
EpisodeWithoutSeasonExpressions = new[]
|
||||||
@@ -481,12 +477,6 @@ namespace Emby.Naming.Common
|
|||||||
"-deleted",
|
"-deleted",
|
||||||
MediaType.Video),
|
MediaType.Video),
|
||||||
|
|
||||||
new ExtraRule(
|
|
||||||
ExtraType.DeletedScene,
|
|
||||||
ExtraRuleType.Suffix,
|
|
||||||
"-deletedscene",
|
|
||||||
MediaType.Video),
|
|
||||||
|
|
||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.Clip,
|
ExtraType.Clip,
|
||||||
ExtraRuleType.Suffix,
|
ExtraRuleType.Suffix,
|
||||||
@@ -597,7 +587,7 @@ namespace Emby.Naming.Common
|
|||||||
AudioBookNamesExpressions = new[]
|
AudioBookNamesExpressions = new[]
|
||||||
{
|
{
|
||||||
// Detect year usually in brackets after name Batman (2020)
|
// 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*$"
|
@"^\s*(?<name>[^ ].*?)\s*$"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -6,13 +6,15 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||||
@@ -21,18 +23,17 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="../SharedVersion.cs" />
|
<Compile Include="..\SharedVersion.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||||
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.8.0</VersionPrefix>
|
<VersionPrefix>10.7.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -43,9 +44,14 @@
|
|||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace Emby.Naming.TV
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
||||||
/// </summary>
|
/// </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)
|
public EpisodeResolver(NamingOptions options)
|
||||||
{
|
{
|
||||||
_options = options;
|
_options = options;
|
||||||
@@ -62,16 +62,12 @@ namespace Emby.Naming.TV
|
|||||||
container = extension.TrimStart('.');
|
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)
|
var parsingResult = new EpisodePathParser(_options)
|
||||||
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
||||||
|
|
||||||
if (!parsingResult.Success && !isStub)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EpisodeInfo(path)
|
return new EpisodeInfo(path)
|
||||||
{
|
{
|
||||||
Container = container,
|
Container = container,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ namespace Emby.Naming.TV
|
|||||||
bool supportSpecialAliases,
|
bool supportSpecialAliases,
|
||||||
bool supportNumericSeasonFolders)
|
bool supportNumericSeasonFolders)
|
||||||
{
|
{
|
||||||
string filename = Path.GetFileName(path);
|
var filename = Path.GetFileName(path) ?? string.Empty;
|
||||||
|
|
||||||
if (supportSpecialAliases)
|
if (supportSpecialAliases)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
@@ -17,39 +16,38 @@ namespace Emby.Naming.Video
|
|||||||
/// <param name="expressions">List of regex to parse name and year from.</param>
|
/// <param name="expressions">List of regex to parse name and year from.</param>
|
||||||
/// <param name="newName">Parsing result string.</param>
|
/// <param name="newName">Parsing result string.</param>
|
||||||
/// <returns>True if parsing was successful.</returns>
|
/// <returns>True if parsing was successful.</returns>
|
||||||
public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out string newName)
|
public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(name))
|
var len = expressions.Count;
|
||||||
{
|
for (int i = 0; i < len; i++)
|
||||||
newName = string.Empty;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iteratively apply the regexps to clean the string.
|
|
||||||
bool cleaned = false;
|
|
||||||
for (int i = 0; i < expressions.Count; i++)
|
|
||||||
{
|
{
|
||||||
if (TryClean(name, expressions[i], out newName))
|
if (TryClean(name, expressions[i], out newName))
|
||||||
{
|
{
|
||||||
cleaned = true;
|
return true;
|
||||||
name = newName;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newName = cleaned ? name : string.Empty;
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
return cleaned;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryClean(string name, Regex expression, out string newName)
|
private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
|
||||||
{
|
{
|
||||||
var match = expression.Match(name);
|
if (string.IsNullOrEmpty(name))
|
||||||
if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
|
|
||||||
{
|
{
|
||||||
newName = cleaned.Value;
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = expression.Match(name);
|
||||||
|
int index = match.Index;
|
||||||
|
if (match.Success && index != 0)
|
||||||
|
{
|
||||||
|
newName = name.AsSpan().Slice(0, match.Index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
newName = string.Empty;
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Emby.Naming.Audio;
|
using Emby.Naming.Audio;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
@@ -11,7 +12,6 @@ namespace Emby.Naming.Video
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ExtraResolver
|
public class ExtraResolver
|
||||||
{
|
{
|
||||||
private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
|
|
||||||
private readonly NamingOptions _options;
|
private readonly NamingOptions _options;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -29,76 +29,72 @@ namespace Emby.Naming.Video
|
|||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||||
public ExtraResult GetExtraInfo(string path)
|
public ExtraResult GetExtraInfo(string path)
|
||||||
|
{
|
||||||
|
return _options.VideoExtraRules
|
||||||
|
.Select(i => GetExtraInfo(path, i))
|
||||||
|
.FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtraResult GetExtraInfo(string path, ExtraRule rule)
|
||||||
{
|
{
|
||||||
var result = new ExtraResult();
|
var result = new ExtraResult();
|
||||||
|
|
||||||
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
|
if (rule.MediaType == MediaType.Audio)
|
||||||
{
|
{
|
||||||
var rule = _options.VideoExtraRules[i];
|
if (!AudioFileParser.IsAudioFile(path, _options))
|
||||||
if (rule.MediaType == MediaType.Audio)
|
|
||||||
{
|
|
||||||
if (!AudioFileParser.IsAudioFile(path, _options))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.MediaType == MediaType.Video)
|
|
||||||
{
|
|
||||||
if (!VideoResolver.IsVideoFile(path, _options))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pathSpan = path.AsSpan();
|
|
||||||
if (rule.RuleType == ExtraRuleType.Filename)
|
|
||||||
{
|
|
||||||
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
|
||||||
|
|
||||||
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.Suffix)
|
|
||||||
{
|
|
||||||
// Trim the digits from the end of the filename so we can recognize things like -trailer2
|
|
||||||
var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
|
|
||||||
|
|
||||||
if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.Regex)
|
|
||||||
{
|
|
||||||
var filename = Path.GetFileName(path);
|
|
||||||
|
|
||||||
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
if (regex.IsMatch(filename))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
|
||||||
{
|
|
||||||
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
|
|
||||||
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.ExtraType != null)
|
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (rule.MediaType == MediaType.Video)
|
||||||
|
{
|
||||||
|
if (!new VideoResolver(_options).IsVideoFile(path))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.RuleType == ExtraRuleType.Filename)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.RuleType == ExtraRuleType.Suffix)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.RuleType == ExtraRuleType.Regex)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileName(path);
|
||||||
|
|
||||||
|
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
if (regex.IsMatch(filename))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
||||||
|
{
|
||||||
|
var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
|
||||||
|
if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Emby.Naming.Common;
|
||||||
|
|
||||||
|
namespace Emby.Naming.Video
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parses list of flags from filename based on delimiters.
|
||||||
|
/// </summary>
|
||||||
|
public class FlagParser
|
||||||
|
{
|
||||||
|
private readonly NamingOptions _options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FlagParser"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param>
|
||||||
|
public FlagParser(NamingOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path to file.</param>
|
||||||
|
/// <returns>List of found flags.</returns>
|
||||||
|
public string[] GetFlags(string path)
|
||||||
|
{
|
||||||
|
return GetFlags(path, _options.VideoFlagDelimiters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses flags from filename based on delimiters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path to file.</param>
|
||||||
|
/// <param name="delimiters">Delimiters used to extract flags.</param>
|
||||||
|
/// <returns>List of found flags.</returns>
|
||||||
|
public string[] GetFlags(string path, char[] delimiters)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
|
||||||
|
|
||||||
|
var file = Path.GetFileName(path);
|
||||||
|
|
||||||
|
return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,45 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse 3D format related flags.
|
/// Parste 3D format related flags.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Format3DParser
|
public class Format3DParser
|
||||||
{
|
{
|
||||||
// Static default result to save on allocation costs.
|
private readonly NamingOptions _options;
|
||||||
private static readonly Format3DResult _defaultResult = new (false, null);
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="Format3DParser"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param>
|
||||||
|
public Format3DParser(NamingOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse 3D format related flags.
|
/// Parse 3D format related flags.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
|
||||||
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
|
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
|
||||||
public static Format3DResult Parse(ReadOnlySpan<char> path, NamingOptions namingOptions)
|
public Format3DResult Parse(string path)
|
||||||
{
|
{
|
||||||
int oldLen = namingOptions.VideoFlagDelimiters.Length;
|
int oldLen = _options.VideoFlagDelimiters.Length;
|
||||||
Span<char> delimiters = stackalloc char[oldLen + 1];
|
var delimiters = new char[oldLen + 1];
|
||||||
namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
|
_options.VideoFlagDelimiters.CopyTo(delimiters, 0);
|
||||||
delimiters[oldLen] = ' ';
|
delimiters[oldLen] = ' ';
|
||||||
|
|
||||||
return Parse(path, delimiters, namingOptions);
|
return Parse(new FlagParser(_options).GetFlags(path, delimiters));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Format3DResult Parse(ReadOnlySpan<char> path, ReadOnlySpan<char> delimiters, NamingOptions namingOptions)
|
internal Format3DResult Parse(string[] videoFlags)
|
||||||
{
|
{
|
||||||
foreach (var rule in namingOptions.Format3DRules)
|
foreach (var rule in _options.Format3DRules)
|
||||||
{
|
{
|
||||||
var result = Parse(path, rule, delimiters);
|
var result = Parse(videoFlags, rule);
|
||||||
|
|
||||||
if (result.Is3D)
|
if (result.Is3D)
|
||||||
{
|
{
|
||||||
@@ -39,43 +47,51 @@ namespace Emby.Naming.Video
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _defaultResult;
|
return new Format3DResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Format3DResult Parse(ReadOnlySpan<char> path, Format3DRule rule, ReadOnlySpan<char> delimiters)
|
private static Format3DResult Parse(string[] videoFlags, Format3DRule rule)
|
||||||
{
|
{
|
||||||
bool is3D = false;
|
var result = new Format3DResult();
|
||||||
string? format3D = null;
|
|
||||||
|
|
||||||
// If there's no preceding token we just consider it found
|
if (string.IsNullOrEmpty(rule.PrecedingToken))
|
||||||
var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken);
|
|
||||||
while (path.Length > 0)
|
|
||||||
{
|
{
|
||||||
var index = path.IndexOfAny(delimiters);
|
result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
|
||||||
if (index == -1)
|
result.Is3D = !string.IsNullOrEmpty(result.Format3D);
|
||||||
|
|
||||||
|
if (result.Is3D)
|
||||||
{
|
{
|
||||||
index = path.Length - 1;
|
result.Tokens.Add(rule.Token);
|
||||||
}
|
|
||||||
|
|
||||||
var currentSlice = path[..index];
|
|
||||||
path = path[(index + 1)..];
|
|
||||||
|
|
||||||
if (!foundPrefix)
|
|
||||||
{
|
|
||||||
foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
if (is3D)
|
|
||||||
{
|
|
||||||
format3D = rule.Token;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var foundPrefix = false;
|
||||||
|
string? format = null;
|
||||||
|
|
||||||
return is3D ? new Format3DResult(true, format3D) : _defaultResult;
|
foreach (var flag in videoFlags)
|
||||||
|
{
|
||||||
|
if (foundPrefix)
|
||||||
|
{
|
||||||
|
result.Tokens.Add(rule.PrecedingToken);
|
||||||
|
|
||||||
|
if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
format = flag;
|
||||||
|
result.Tokens.Add(rule.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
|
||||||
|
result.Format3D = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -8,24 +10,27 @@ namespace Emby.Naming.Video
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
|
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="is3D">A value indicating whether the parsed string contains 3D tokens.</param>
|
public Format3DResult()
|
||||||
/// <param name="format3D">The 3D format. Value might be null if [is3D] is <c>false</c>.</param>
|
|
||||||
public Format3DResult(bool is3D, string? format3D)
|
|
||||||
{
|
{
|
||||||
Is3D = is3D;
|
Tokens = new List<string>();
|
||||||
Format3D = format3D;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether [is3 d].
|
/// Gets or sets a value indicating whether [is3 d].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
|
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
|
||||||
public bool Is3D { get; }
|
public bool Is3D { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the format3 d.
|
/// Gets or sets the format3 d.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The format3 d.</value>
|
/// <value>The format3 d.</value>
|
||||||
public string? Format3D { get; }
|
public string? Format3D { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the tokens.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The tokens.</value>
|
||||||
|
public List<string> Tokens { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,8 +85,10 @@ namespace Emby.Naming.Video
|
|||||||
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
||||||
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
|
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
|
||||||
{
|
{
|
||||||
|
var resolver = new VideoResolver(_options);
|
||||||
|
|
||||||
var list = files
|
var list = files
|
||||||
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
|
.Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))
|
||||||
.OrderBy(i => i.FullName)
|
.OrderBy(i => i.FullName)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
@@ -107,9 +106,9 @@ namespace Emby.Naming.Video
|
|||||||
/// Gets the file name without extension.
|
/// Gets the file name without extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The file name without extension.</value>
|
/// <value>The file name without extension.</value>
|
||||||
public ReadOnlySpan<char> FileNameWithoutExtension => !IsDirectory
|
public string FileNameWithoutExtension => !IsDirectory
|
||||||
? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan())
|
? System.IO.Path.GetFileNameWithoutExtension(Path)
|
||||||
: System.IO.Path.GetFileName(Path.AsSpan());
|
: System.IO.Path.GetFileName(Path);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|||||||
@@ -12,19 +12,31 @@ namespace Emby.Naming.Video
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves alternative versions and extras from list of video files.
|
/// Resolves alternative versions and extras from list of video files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class VideoListResolver
|
public class VideoListResolver
|
||||||
{
|
{
|
||||||
|
private readonly NamingOptions _options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="VideoListResolver"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
|
||||||
|
public VideoListResolver(NamingOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves alternative versions and extras from list of video files.
|
/// Resolves alternative versions and extras from list of video files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of related video files.</param>
|
/// <param name="files">List of related video files.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
|
||||||
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
||||||
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
||||||
public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
|
public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
|
||||||
{
|
{
|
||||||
|
var videoResolver = new VideoResolver(_options);
|
||||||
|
|
||||||
var videoInfos = files
|
var videoInfos = files
|
||||||
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
|
.Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
|
||||||
.OfType<VideoFileInfo>()
|
.OfType<VideoFileInfo>()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -34,7 +46,7 @@ namespace Emby.Naming.Video
|
|||||||
.Where(i => i.ExtraType == null)
|
.Where(i => i.ExtraType == null)
|
||||||
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
||||||
|
|
||||||
var stackResult = new StackResolver(namingOptions)
|
var stackResult = new StackResolver(_options)
|
||||||
.Resolve(nonExtras).ToList();
|
.Resolve(nonExtras).ToList();
|
||||||
|
|
||||||
var remainingFiles = videoInfos
|
var remainingFiles = videoInfos
|
||||||
@@ -47,17 +59,23 @@ namespace Emby.Naming.Video
|
|||||||
{
|
{
|
||||||
var info = new VideoInfo(stack.Name)
|
var info = new VideoInfo(stack.Name)
|
||||||
{
|
{
|
||||||
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
|
Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
|
||||||
.OfType<VideoFileInfo>()
|
.OfType<VideoFileInfo>()
|
||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
info.Year = info.Files[0].Year;
|
info.Year = info.Files[0].Year;
|
||||||
|
|
||||||
var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
|
var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
|
||||||
|
|
||||||
|
var extras = GetExtras(remainingFiles, extraBaseNames);
|
||||||
|
|
||||||
if (extras.Count > 0)
|
if (extras.Count > 0)
|
||||||
{
|
{
|
||||||
|
remainingFiles = remainingFiles
|
||||||
|
.Except(extras)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
info.Extras = extras;
|
info.Extras = extras;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,12 +88,15 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
foreach (var media in standaloneMedia)
|
foreach (var media in standaloneMedia)
|
||||||
{
|
{
|
||||||
var info = new VideoInfo(media.Name) { Files = new[] { media } };
|
var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } };
|
||||||
|
|
||||||
info.Year = info.Files[0].Year;
|
info.Year = info.Files[0].Year;
|
||||||
|
|
||||||
remainingFiles.Remove(media);
|
var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
|
||||||
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
|
|
||||||
|
remainingFiles = remainingFiles
|
||||||
|
.Except(extras.Concat(new[] { media }))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
info.Extras = extras;
|
info.Extras = extras;
|
||||||
|
|
||||||
@@ -84,7 +105,8 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
if (supportMultiVersion)
|
if (supportMultiVersion)
|
||||||
{
|
{
|
||||||
list = GetVideosGroupedByVersion(list, namingOptions);
|
list = GetVideosGroupedByVersion(list)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's only one resolved video, use the folder name as well to find extras
|
// If there's only one resolved video, use the folder name as well to find extras
|
||||||
@@ -92,14 +114,19 @@ namespace Emby.Naming.Video
|
|||||||
{
|
{
|
||||||
var info = list[0];
|
var info = list[0];
|
||||||
var videoPath = list[0].Files[0].Path;
|
var videoPath = list[0].Files[0].Path;
|
||||||
var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
|
var parentPath = Path.GetDirectoryName(videoPath);
|
||||||
|
|
||||||
if (!parentPath.IsEmpty)
|
if (!string.IsNullOrEmpty(parentPath))
|
||||||
{
|
{
|
||||||
var folderName = Path.GetFileName(parentPath);
|
var folderName = Path.GetFileName(parentPath);
|
||||||
if (!folderName.IsEmpty)
|
if (!string.IsNullOrEmpty(folderName))
|
||||||
{
|
{
|
||||||
var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
|
var extras = GetExtras(remainingFiles, new List<string> { folderName });
|
||||||
|
|
||||||
|
remainingFiles = remainingFiles
|
||||||
|
.Except(extras)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
extras.AddRange(info.Extras);
|
extras.AddRange(info.Extras);
|
||||||
info.Extras = extras;
|
info.Extras = extras;
|
||||||
}
|
}
|
||||||
@@ -137,168 +164,97 @@ namespace Emby.Naming.Video
|
|||||||
// Whatever files are left, just add them
|
// Whatever files are left, just add them
|
||||||
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
|
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
|
||||||
{
|
{
|
||||||
Files = new[] { i },
|
Files = new List<VideoFileInfo> { i },
|
||||||
Year = i.Year
|
Year = i.Year
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
|
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
|
||||||
{
|
{
|
||||||
if (videos.Count == 0)
|
if (videos.Count == 0)
|
||||||
{
|
{
|
||||||
return videos;
|
return videos;
|
||||||
}
|
}
|
||||||
|
|
||||||
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
|
var list = new List<VideoInfo>();
|
||||||
|
|
||||||
if (folderName.Length <= 1 || !HaveSameYear(videos))
|
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path));
|
||||||
{
|
|
||||||
return videos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
|
if (!string.IsNullOrEmpty(folderName)
|
||||||
for (var i = 0; i < videos.Count; i++)
|
&& folderName.Length > 1
|
||||||
|
&& videos.All(i => i.Files.Count == 1
|
||||||
|
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path))
|
||||||
|
&& HaveSameYear(videos))
|
||||||
{
|
{
|
||||||
var video = videos[i];
|
var ordered = videos.OrderBy(i => i.Name).ToList();
|
||||||
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
|
|
||||||
|
list.Add(ordered[0]);
|
||||||
|
|
||||||
|
var alternateVersionsLen = ordered.Count - 1;
|
||||||
|
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
||||||
|
for (int i = 0; i < alternateVersionsLen; i++)
|
||||||
{
|
{
|
||||||
return videos;
|
alternateVersions[i] = ordered[i + 1].Files[0];
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The list is created and overwritten in the caller, so we are allowed to do in-place sorting
|
|
||||||
videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
|
|
||||||
|
|
||||||
var list = new List<VideoInfo>
|
|
||||||
{
|
|
||||||
videos[0]
|
|
||||||
};
|
|
||||||
|
|
||||||
var alternateVersionsLen = videos.Count - 1;
|
|
||||||
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
|
||||||
var extras = new List<VideoFileInfo>(list[0].Extras);
|
|
||||||
for (int i = 0; i < alternateVersionsLen; i++)
|
|
||||||
{
|
|
||||||
var video = videos[i + 1];
|
|
||||||
alternateVersions[i] = video.Files[0];
|
|
||||||
extras.AddRange(video.Extras);
|
|
||||||
}
|
|
||||||
|
|
||||||
list[0].AlternateVersions = alternateVersions;
|
|
||||||
list[0].Name = folderName.ToString();
|
|
||||||
list[0].Extras = extras;
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
|
|
||||||
{
|
|
||||||
if (videos.Count == 1)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstYear = videos[0].Year ?? -1;
|
|
||||||
for (var i = 1; i < videos.Count; i++)
|
|
||||||
{
|
|
||||||
if ((videos[i].Year ?? -1) != firstYear)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
|
|
||||||
{
|
|
||||||
var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
|
|
||||||
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the folder name before cleaning as we don't care about cleaning that part
|
|
||||||
if (folderName.Length <= testFilename.Length)
|
|
||||||
{
|
|
||||||
testFilename = testFilename[folderName.Length..].Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// There are no span overloads for regex unfortunately
|
|
||||||
var tmpTestFilename = testFilename.ToString();
|
|
||||||
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
|
|
||||||
{
|
|
||||||
tmpTestFilename = cleanName.Trim().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The CleanStringParser should have removed common keywords etc.
|
|
||||||
return string.IsNullOrEmpty(tmpTestFilename)
|
|
||||||
|| testFilename[0] == '-'
|
|
||||||
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
|
|
||||||
{
|
|
||||||
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
|
|
||||||
{
|
|
||||||
if (baseName.IsEmpty)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="remainingFiles">The list of remaining filenames.</param>
|
|
||||||
/// <param name="baseName">The base name to use for the comparison.</param>
|
|
||||||
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
|
||||||
/// <returns>A list of video extras for [baseName].</returns>
|
|
||||||
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
|
|
||||||
{
|
|
||||||
return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="remainingFiles">The list of remaining filenames.</param>
|
|
||||||
/// <param name="firstBaseName">The first base name to use for the comparison.</param>
|
|
||||||
/// <param name="secondBaseName">The second base name to use for the comparison.</param>
|
|
||||||
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
|
||||||
/// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
|
|
||||||
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
|
|
||||||
{
|
|
||||||
var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
|
|
||||||
var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
|
|
||||||
|
|
||||||
var result = new List<VideoFileInfo>();
|
|
||||||
for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
|
|
||||||
{
|
|
||||||
var file = remainingFiles[pos];
|
|
||||||
if (file.ExtraType == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var filename = file.FileNameWithoutExtension;
|
list[0].AlternateVersions = alternateVersions;
|
||||||
if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
|
list[0].Name = folderName;
|
||||||
|| StartsWith(filename, secondBaseName, trimmedSecondBaseName))
|
var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
|
||||||
{
|
extras.AddRange(list[0].Extras);
|
||||||
result.Add(file);
|
list[0].Extras = extras;
|
||||||
remainingFiles.RemoveAt(pos);
|
|
||||||
}
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return videos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HaveSameYear(List<VideoInfo> videos)
|
||||||
|
{
|
||||||
|
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsEligibleForMultiVersion(string folderName, string? testFilename)
|
||||||
|
{
|
||||||
|
testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty;
|
||||||
|
|
||||||
|
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Remove the folder name before cleaning as we don't care about cleaning that part
|
||||||
|
if (folderName.Length <= testFilename.Length)
|
||||||
|
{
|
||||||
|
testFilename = testFilename.Substring(folderName.Length).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
|
||||||
|
{
|
||||||
|
testFilename = cleanName.Trim().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CleanStringParser should have removed common keywords etc.
|
||||||
|
return string.IsNullOrEmpty(testFilename)
|
||||||
|
|| testFilename[0] == '-'
|
||||||
|
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames)
|
||||||
|
{
|
||||||
|
foreach (var name in baseNames.ToList())
|
||||||
|
{
|
||||||
|
var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
|
||||||
|
baseNames.Add(trimmedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return remainingFiles
|
||||||
|
.Where(i => i.ExtraType != null)
|
||||||
|
.Where(i => baseNames.Any(b =>
|
||||||
|
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,45 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using Jellyfin.Extensions;
|
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves <see cref="VideoFileInfo"/> from file path.
|
/// Resolves <see cref="VideoFileInfo"/> from file path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class VideoResolver
|
public class VideoResolver
|
||||||
{
|
{
|
||||||
|
private readonly NamingOptions _options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="VideoResolver"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
|
||||||
|
/// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param>
|
||||||
|
public VideoResolver(NamingOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the directory.
|
/// Resolves the directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
|
public VideoFileInfo? ResolveDirectory(string? path)
|
||||||
{
|
{
|
||||||
return Resolve(path, true, namingOptions);
|
return Resolve(path, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the file.
|
/// Resolves the file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
|
public VideoFileInfo? ResolveFile(string? path)
|
||||||
{
|
{
|
||||||
return Resolve(path, false, namingOptions);
|
return Resolve(path, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -38,11 +47,10 @@ namespace Emby.Naming.Video
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
|
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
|
||||||
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
|
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
|
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
|
||||||
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
|
public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
{
|
{
|
||||||
@@ -50,18 +58,18 @@ namespace Emby.Naming.Video
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isStub = false;
|
bool isStub = false;
|
||||||
ReadOnlySpan<char> container = ReadOnlySpan<char>.Empty;
|
string? container = null;
|
||||||
string? stubType = null;
|
string? stubType = null;
|
||||||
|
|
||||||
if (!isDirectory)
|
if (!isDirectory)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path.AsSpan());
|
var extension = Path.GetExtension(path);
|
||||||
|
|
||||||
// Check supported extensions
|
// Check supported extensions
|
||||||
if (!namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// It's not supported. Check stub extensions
|
// It's not supported. Check stub extensions
|
||||||
if (!StubResolver.TryResolveFile(path, namingOptions, out stubType))
|
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -72,30 +80,33 @@ namespace Emby.Naming.Video
|
|||||||
container = extension.TrimStart('.');
|
container = extension.TrimStart('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
var flags = new FlagParser(_options).GetFlags(path);
|
||||||
|
var format3DResult = new Format3DParser(_options).Parse(flags);
|
||||||
|
|
||||||
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
|
var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(path);
|
var name = isDirectory
|
||||||
|
? Path.GetFileName(path)
|
||||||
|
: Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
int? year = null;
|
int? year = null;
|
||||||
|
|
||||||
if (parseName)
|
if (parseName)
|
||||||
{
|
{
|
||||||
var cleanDateTimeResult = CleanDateTime(name, namingOptions);
|
var cleanDateTimeResult = CleanDateTime(name);
|
||||||
name = cleanDateTimeResult.Name;
|
name = cleanDateTimeResult.Name;
|
||||||
year = cleanDateTimeResult.Year;
|
year = cleanDateTimeResult.Year;
|
||||||
|
|
||||||
if (extraResult.ExtraType == null
|
if (extraResult.ExtraType == null
|
||||||
&& TryCleanString(name, namingOptions, out var newName))
|
&& TryCleanString(name, out ReadOnlySpan<char> newName))
|
||||||
{
|
{
|
||||||
name = newName;
|
name = newName.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new VideoFileInfo(
|
return new VideoFileInfo(
|
||||||
path: path,
|
path: path,
|
||||||
container: container.IsEmpty ? null : container.ToString(),
|
container: container,
|
||||||
isStub: isStub,
|
isStub: isStub,
|
||||||
name: name,
|
name: name,
|
||||||
year: year,
|
year: year,
|
||||||
@@ -111,47 +122,43 @@ namespace Emby.Naming.Video
|
|||||||
/// Determines if path is video file based on extension.
|
/// Determines if path is video file based on extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
|
||||||
/// <returns>True if is video file.</returns>
|
/// <returns>True if is video file.</returns>
|
||||||
public static bool IsVideoFile(string path, NamingOptions namingOptions)
|
public bool IsVideoFile(string path)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path.AsSpan());
|
var extension = Path.GetExtension(path) ?? string.Empty;
|
||||||
return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if path is video file stub based on extension.
|
/// Determines if path is video file stub based on extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
|
||||||
/// <returns>True if is video file stub.</returns>
|
/// <returns>True if is video file stub.</returns>
|
||||||
public static bool IsStubFile(string path, NamingOptions namingOptions)
|
public bool IsStubFile(string path)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path.AsSpan());
|
var extension = Path.GetExtension(path) ?? string.Empty;
|
||||||
return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to clean name of clutter.
|
/// Tries to clean name of clutter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">Raw name.</param>
|
/// <param name="name">Raw name.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
|
||||||
/// <param name="newName">Clean name.</param>
|
/// <param name="newName">Clean name.</param>
|
||||||
/// <returns>True if cleaning of name was successful.</returns>
|
/// <returns>True if cleaning of name was successful.</returns>
|
||||||
public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out string newName)
|
public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
|
||||||
{
|
{
|
||||||
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
|
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to get name and year from raw name.
|
/// Tries to get name and year from raw name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">Raw name.</param>
|
/// <param name="name">Raw name.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
|
||||||
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
|
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
|
||||||
public static CleanDateTimeResult CleanDateTime(string name, NamingOptions namingOptions)
|
public CleanDateTimeResult CleanDateTime(string name)
|
||||||
{
|
{
|
||||||
return CleanDateTimeParser.Clean(name, namingOptions.CleanDateTimeRegexes);
|
return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -23,9 +25,14 @@
|
|||||||
|
|
||||||
<!-- Code analyzers-->
|
<!-- Code analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ namespace Emby.Notifications
|
|||||||
{
|
{
|
||||||
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
|
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
|
||||||
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
|
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
|
||||||
|
_appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
|
||||||
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
|
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -131,6 +132,25 @@ namespace Emby.Notifications
|
|||||||
return _config.GetConfiguration<NotificationOptions>("notifications");
|
return _config.GetConfiguration<NotificationOptions>("notifications");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (!_appHost.HasUpdateAvailable)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = NotificationType.ApplicationUpdateAvailable.ToString();
|
||||||
|
|
||||||
|
var notification = new NotificationRequest
|
||||||
|
{
|
||||||
|
Description = "Please see jellyfin.org for details.",
|
||||||
|
NotificationType = type,
|
||||||
|
Name = _localization.GetLocalizedString("NewVersionIsAvailable")
|
||||||
|
};
|
||||||
|
|
||||||
|
await SendNotification(notification, null).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
|
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (!FilterItem(e.Item))
|
if (!FilterItem(e.Item))
|
||||||
@@ -305,6 +325,7 @@ namespace Emby.Notifications
|
|||||||
|
|
||||||
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
|
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
|
||||||
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
|
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
|
||||||
|
_appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
|
||||||
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
|
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|||||||
@@ -19,16 +19,23 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
CachePath = cacheDirectoryPath;
|
CachePath = cacheDirectoryPath;
|
||||||
WebPath = webDirectoryPath;
|
WebPath = webDirectoryPath;
|
||||||
|
|
||||||
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
DataPath = Path.Combine(ProgramDataPath, "data");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -55,7 +55,11 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
/// Gets the folder path to the data directory.
|
/// Gets the folder path to the data directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The data directory.</value>
|
/// <value>The data directory.</value>
|
||||||
public string DataPath => _dataPath;
|
public string DataPath
|
||||||
|
{
|
||||||
|
get => _dataPath;
|
||||||
|
private set => _dataPath = Directory.CreateDirectory(value).FullName;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string VirtualDataPath => "%AppDataPath%";
|
public string VirtualDataPath => "%AppDataPath%";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -25,11 +23,6 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
|
|
||||||
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _configuration sync lock.
|
|
||||||
/// </summary>
|
|
||||||
private readonly object _configurationSyncLock = new object();
|
|
||||||
|
|
||||||
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
||||||
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
||||||
|
|
||||||
@@ -38,6 +31,11 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private bool _configurationLoaded;
|
private bool _configurationLoaded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The _configuration sync lock.
|
||||||
|
/// </summary>
|
||||||
|
private readonly object _configurationSyncLock = new object();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _configuration.
|
/// The _configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -299,29 +297,25 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public object GetConfiguration(string key)
|
public object GetConfiguration(string key)
|
||||||
{
|
{
|
||||||
return _configurations.GetOrAdd(
|
return _configurations.GetOrAdd(key, k =>
|
||||||
key,
|
{
|
||||||
(k, configurationManager) =>
|
var file = GetConfigurationFile(key);
|
||||||
|
|
||||||
|
var configurationInfo = _configurationStores
|
||||||
|
.FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (configurationInfo == null)
|
||||||
{
|
{
|
||||||
var file = configurationManager.GetConfigurationFile(k);
|
throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
|
||||||
|
}
|
||||||
|
|
||||||
var configurationInfo = Array.Find(
|
var configurationType = configurationInfo.ConfigurationType;
|
||||||
configurationManager._configurationStores,
|
|
||||||
i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (configurationInfo == null)
|
lock (_configurationSyncLock)
|
||||||
{
|
{
|
||||||
throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
|
return LoadConfiguration(file, configurationType);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
var configurationType = configurationInfo.ConfigurationType;
|
|
||||||
|
|
||||||
lock (configurationManager._configurationSyncLock)
|
|
||||||
{
|
|
||||||
return configurationManager.LoadConfiguration(file, configurationType);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private object LoadConfiguration(string path, Type configurationType)
|
private object LoadConfiguration(string path, Type configurationType)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.AppBase
|
namespace Emby.Server.Implementations.AppBase
|
||||||
@@ -33,27 +36,27 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
// Note: CreateInstance returns null for Nullable<T>, e.g. CreateInstance(typeof(int?)) returns null.
|
configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
|
||||||
configuration = Activator.CreateInstance(type)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
||||||
xmlSerializer.SerializeToStream(configuration, stream);
|
xmlSerializer.SerializeToStream(configuration, stream);
|
||||||
|
|
||||||
// Take the object we just got and serialize it back to bytes
|
// Take the object we just got and serialize it back to bytes
|
||||||
Span<byte> newBytes = stream.GetBuffer().AsSpan(0, (int)stream.Length);
|
byte[] newBytes = stream.GetBuffer();
|
||||||
|
int newBytesLen = (int)stream.Length;
|
||||||
|
|
||||||
// If the file didn't exist before, or if something has changed, re-save
|
// If the file didn't exist before, or if something has changed, re-save
|
||||||
if (buffer == null || !newBytes.SequenceEqual(buffer))
|
if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
|
||||||
{
|
{
|
||||||
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
|
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
|
||||||
|
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
|
|
||||||
// Save it after load in case we got new items
|
// Save it after load in case we got new items
|
||||||
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
fs.Write(newBytes);
|
fs.Write(newBytes, 0, newBytesLen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@@ -9,9 +7,11 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna;
|
using Emby.Dlna;
|
||||||
@@ -38,6 +38,7 @@ using Emby.Server.Implementations.Playlists;
|
|||||||
using Emby.Server.Implementations.Plugins;
|
using Emby.Server.Implementations.Plugins;
|
||||||
using Emby.Server.Implementations.QuickConnect;
|
using Emby.Server.Implementations.QuickConnect;
|
||||||
using Emby.Server.Implementations.ScheduledTasks;
|
using Emby.Server.Implementations.ScheduledTasks;
|
||||||
|
using Emby.Server.Implementations.Security;
|
||||||
using Emby.Server.Implementations.Serialization;
|
using Emby.Server.Implementations.Serialization;
|
||||||
using Emby.Server.Implementations.Session;
|
using Emby.Server.Implementations.Session;
|
||||||
using Emby.Server.Implementations.SyncPlay;
|
using Emby.Server.Implementations.SyncPlay;
|
||||||
@@ -58,6 +59,7 @@ using MediaBrowser.Controller.Channels;
|
|||||||
using MediaBrowser.Controller.Chapters;
|
using MediaBrowser.Controller.Chapters;
|
||||||
using MediaBrowser.Controller.Collections;
|
using MediaBrowser.Controller.Collections;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
@@ -73,6 +75,7 @@ using MediaBrowser.Controller.Plugins;
|
|||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Controller.QuickConnect;
|
using MediaBrowser.Controller.QuickConnect;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
|
using MediaBrowser.Controller.Security;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Controller.Sorting;
|
using MediaBrowser.Controller.Sorting;
|
||||||
using MediaBrowser.Controller.Subtitles;
|
using MediaBrowser.Controller.Subtitles;
|
||||||
@@ -100,6 +103,7 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Prometheus.DotNetRuntime;
|
using Prometheus.DotNetRuntime;
|
||||||
|
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
||||||
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
|
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations
|
namespace Emby.Server.Implementations
|
||||||
@@ -114,14 +118,10 @@ namespace Emby.Server.Implementations
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The disposable parts.
|
|
||||||
/// </summary>
|
|
||||||
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystemManager;
|
private readonly IFileSystem _fileSystemManager;
|
||||||
private readonly IConfiguration _startupConfig;
|
private readonly IConfiguration _startupConfig;
|
||||||
private readonly IXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IStartupOptions _startupOptions;
|
private readonly IStartupOptions _startupOptions;
|
||||||
private readonly IPluginManager _pluginManager;
|
private readonly IPluginManager _pluginManager;
|
||||||
|
|
||||||
@@ -130,62 +130,6 @@ namespace Emby.Server.Implementations
|
|||||||
private ISessionManager _sessionManager;
|
private ISessionManager _sessionManager;
|
||||||
private string[] _urlPrefixes;
|
private string[] _urlPrefixes;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets all concrete types.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>All concrete types.</value>
|
|
||||||
private Type[] _allConcreteTypes;
|
|
||||||
|
|
||||||
private DeviceId _deviceId;
|
|
||||||
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
|
||||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
|
||||||
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
|
||||||
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
|
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
|
||||||
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
|
||||||
public ApplicationHost(
|
|
||||||
IServerApplicationPaths applicationPaths,
|
|
||||||
ILoggerFactory loggerFactory,
|
|
||||||
IStartupOptions options,
|
|
||||||
IConfiguration startupConfig,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IServiceCollection serviceCollection)
|
|
||||||
{
|
|
||||||
ApplicationPaths = applicationPaths;
|
|
||||||
LoggerFactory = loggerFactory;
|
|
||||||
_startupOptions = options;
|
|
||||||
_startupConfig = startupConfig;
|
|
||||||
_fileSystemManager = fileSystem;
|
|
||||||
ServiceCollection = serviceCollection;
|
|
||||||
|
|
||||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
|
||||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
|
||||||
|
|
||||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
|
||||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
|
||||||
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
|
||||||
|
|
||||||
_xmlSerializer = new MyXmlSerializer();
|
|
||||||
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
|
|
||||||
_pluginManager = new PluginManager(
|
|
||||||
LoggerFactory.CreateLogger<PluginManager>(),
|
|
||||||
this,
|
|
||||||
ConfigurationManager.Configuration,
|
|
||||||
ApplicationPaths.PluginsPath,
|
|
||||||
ApplicationVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when [has pending restart changed].
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler HasPendingRestartChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether this instance can self restart.
|
/// Gets a value indicating whether this instance can self restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -207,7 +151,13 @@ namespace Emby.Server.Implementations
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
|
if (OperatingSystem.Id == OperatingSystemId.Windows
|
||||||
|
|| OperatingSystem.Id == OperatingSystemId.Darwin)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +166,11 @@ namespace Emby.Server.Implementations
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public INetworkManager NetManager { get; internal set; }
|
public INetworkManager NetManager { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when [has pending restart changed].
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler HasPendingRestartChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -243,11 +198,22 @@ namespace Emby.Server.Implementations
|
|||||||
/// <value>The application paths.</value>
|
/// <value>The application paths.</value>
|
||||||
protected IServerApplicationPaths ApplicationPaths { get; set; }
|
protected IServerApplicationPaths ApplicationPaths { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets all concrete types.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>All concrete types.</value>
|
||||||
|
private Type[] _allConcreteTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The disposable parts.
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the configuration manager.
|
/// Gets or sets the configuration manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The configuration manager.</value>
|
/// <value>The configuration manager.</value>
|
||||||
public ServerConfigurationManager ConfigurationManager { get; set; }
|
protected IConfigurationManager ConfigurationManager { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the service provider.
|
/// Gets or sets the service provider.
|
||||||
@@ -269,6 +235,103 @@ namespace Emby.Server.Implementations
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the server configuration manager.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The server configuration manager.</value>
|
||||||
|
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
||||||
|
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||||
|
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
||||||
|
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
|
||||||
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
|
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||||
|
public ApplicationHost(
|
||||||
|
IServerApplicationPaths applicationPaths,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
IStartupOptions options,
|
||||||
|
IConfiguration startupConfig,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
IServiceCollection serviceCollection)
|
||||||
|
{
|
||||||
|
_xmlSerializer = new MyXmlSerializer();
|
||||||
|
_jsonSerializer = new JsonSerializer();
|
||||||
|
|
||||||
|
ServiceCollection = serviceCollection;
|
||||||
|
|
||||||
|
ApplicationPaths = applicationPaths;
|
||||||
|
LoggerFactory = loggerFactory;
|
||||||
|
_fileSystemManager = fileSystem;
|
||||||
|
|
||||||
|
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
|
||||||
|
// Have to migrate settings here as migration subsystem not yet initialised.
|
||||||
|
MigrateNetworkConfiguration();
|
||||||
|
|
||||||
|
// Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
|
||||||
|
ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
|
||||||
|
NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
||||||
|
|
||||||
|
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||||
|
|
||||||
|
_startupOptions = options;
|
||||||
|
_startupConfig = startupConfig;
|
||||||
|
|
||||||
|
// Initialize runtime stat collection
|
||||||
|
if (ServerConfigurationManager.Configuration.EnableMetrics)
|
||||||
|
{
|
||||||
|
DotNetRuntimeStatsBuilder.Default().StartCollecting();
|
||||||
|
}
|
||||||
|
|
||||||
|
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
||||||
|
|
||||||
|
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||||
|
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||||
|
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
||||||
|
|
||||||
|
_pluginManager = new PluginManager(
|
||||||
|
LoggerFactory.CreateLogger<PluginManager>(),
|
||||||
|
this,
|
||||||
|
ServerConfigurationManager.Configuration,
|
||||||
|
ApplicationPaths.PluginsPath,
|
||||||
|
ApplicationVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Temporary function to migration network settings out of system.xml and into network.xml.
|
||||||
|
/// TODO: remove at the point when a fixed migration path has been decided upon.
|
||||||
|
/// </summary>
|
||||||
|
private void MigrateNetworkConfiguration()
|
||||||
|
{
|
||||||
|
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
var networkSettings = new NetworkConfiguration();
|
||||||
|
ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
|
||||||
|
_xmlSerializer.SerializeToFile(networkSettings, path);
|
||||||
|
Logger?.LogDebug("Successfully migrated network settings.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ExpandVirtualPath(string path)
|
||||||
|
{
|
||||||
|
var appPaths = ApplicationPaths;
|
||||||
|
|
||||||
|
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ReverseVirtualPath(string path)
|
||||||
|
{
|
||||||
|
var appPaths = ApplicationPaths;
|
||||||
|
|
||||||
|
return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Version ApplicationVersion { get; }
|
public Version ApplicationVersion { get; }
|
||||||
|
|
||||||
@@ -293,11 +356,16 @@ namespace Emby.Server.Implementations
|
|||||||
/// <value>The application name.</value>
|
/// <value>The application name.</value>
|
||||||
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
|
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
|
||||||
|
|
||||||
|
private DeviceId _deviceId;
|
||||||
|
|
||||||
public string SystemId
|
public string SystemId
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
|
if (_deviceId == null)
|
||||||
|
{
|
||||||
|
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
|
||||||
|
}
|
||||||
|
|
||||||
return _deviceId.Value;
|
return _deviceId.Value;
|
||||||
}
|
}
|
||||||
@@ -306,50 +374,6 @@ namespace Emby.Server.Implementations
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string Name => ApplicationProductName;
|
public string Name => ApplicationProductName;
|
||||||
|
|
||||||
private string CertificatePath { get; set; }
|
|
||||||
|
|
||||||
public X509Certificate2 Certificate { get; private set; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
|
||||||
|
|
||||||
public string FriendlyName =>
|
|
||||||
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
|
|
||||||
? Environment.MachineName
|
|
||||||
: ConfigurationManager.Configuration.ServerName;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Temporary function to migration network settings out of system.xml and into network.xml.
|
|
||||||
/// TODO: remove at the point when a fixed migration path has been decided upon.
|
|
||||||
/// </summary>
|
|
||||||
private void MigrateNetworkConfiguration()
|
|
||||||
{
|
|
||||||
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
|
|
||||||
if (!File.Exists(path))
|
|
||||||
{
|
|
||||||
var networkSettings = new NetworkConfiguration();
|
|
||||||
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
|
|
||||||
_xmlSerializer.SerializeToFile(networkSettings, path);
|
|
||||||
Logger.LogDebug("Successfully migrated network settings.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ExpandVirtualPath(string path)
|
|
||||||
{
|
|
||||||
var appPaths = ApplicationPaths;
|
|
||||||
|
|
||||||
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ReverseVirtualPath(string path)
|
|
||||||
{
|
|
||||||
var appPaths = ApplicationPaths;
|
|
||||||
|
|
||||||
return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instance of type and resolves all constructor dependencies.
|
/// Creates an instance of type and resolves all constructor dependencies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -361,7 +385,7 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instance of type and resolves all constructor dependencies.
|
/// Creates an instance of type and resolves all constructor dependencies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type.</typeparam>
|
/// /// <typeparam name="T">The type.</typeparam>
|
||||||
/// <returns>T.</returns>
|
/// <returns>T.</returns>
|
||||||
public T CreateInstance<T>()
|
public T CreateInstance<T>()
|
||||||
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
|
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
|
||||||
@@ -373,7 +397,10 @@ namespace Emby.Server.Implementations
|
|||||||
/// <returns>System.Object.</returns>
|
/// <returns>System.Object.</returns>
|
||||||
protected object CreateInstanceSafe(Type type)
|
protected object CreateInstanceSafe(Type type)
|
||||||
{
|
{
|
||||||
_creatingInstances ??= new List<Type>();
|
if (_creatingInstances == null)
|
||||||
|
{
|
||||||
|
_creatingInstances = new List<Type>();
|
||||||
|
}
|
||||||
|
|
||||||
if (_creatingInstances.IndexOf(type) != -1)
|
if (_creatingInstances.IndexOf(type) != -1)
|
||||||
{
|
{
|
||||||
@@ -444,7 +471,7 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
|
public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
|
||||||
{
|
{
|
||||||
// Convert to list so this isn't executed for each iteration
|
// Convert to list so this isn't executed for each iteration
|
||||||
var parts = GetExportTypes<T>()
|
var parts = GetExportTypes<T>()
|
||||||
@@ -467,11 +494,9 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the startup tasks.
|
/// Runs the startup tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns><see cref="Task" />.</returns>
|
/// <returns><see cref="Task" />.</returns>
|
||||||
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
|
public async Task RunStartupTasksAsync()
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
Logger.LogInformation("Running startup tasks");
|
Logger.LogInformation("Running startup tasks");
|
||||||
|
|
||||||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||||
@@ -481,25 +506,18 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
_mediaEncoder.SetFFmpegPath();
|
_mediaEncoder.SetFFmpegPath();
|
||||||
|
|
||||||
Logger.LogInformation("ServerId: {ServerId}", SystemId);
|
Logger.LogInformation("ServerId: {0}", SystemId);
|
||||||
|
|
||||||
var entryPoints = GetExports<IServerEntryPoint>();
|
var entryPoints = GetExports<IServerEntryPoint>();
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var stopWatch = new Stopwatch();
|
var stopWatch = new Stopwatch();
|
||||||
stopWatch.Start();
|
stopWatch.Start();
|
||||||
|
|
||||||
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
|
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
|
||||||
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||||
|
|
||||||
Logger.LogInformation("Core startup complete");
|
Logger.LogInformation("Core startup complete");
|
||||||
CoreStartupHasCompleted = true;
|
CoreStartupHasCompleted = true;
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
stopWatch.Restart();
|
stopWatch.Restart();
|
||||||
|
|
||||||
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
||||||
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
@@ -523,21 +541,7 @@ namespace Emby.Server.Implementations
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Init()
|
public void Init()
|
||||||
{
|
{
|
||||||
DiscoverTypes();
|
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
||||||
|
|
||||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
|
||||||
|
|
||||||
// Have to migrate settings here as migration subsystem not yet initialised.
|
|
||||||
MigrateNetworkConfiguration();
|
|
||||||
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
|
||||||
|
|
||||||
// Initialize runtime stat collection
|
|
||||||
if (ConfigurationManager.Configuration.EnableMetrics)
|
|
||||||
{
|
|
||||||
DotNetRuntimeStatsBuilder.Default().StartCollecting();
|
|
||||||
}
|
|
||||||
|
|
||||||
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
|
||||||
HttpPort = networkConfiguration.HttpServerPortNumber;
|
HttpPort = networkConfiguration.HttpServerPortNumber;
|
||||||
HttpsPort = networkConfiguration.HttpsPortNumber;
|
HttpsPort = networkConfiguration.HttpsPortNumber;
|
||||||
|
|
||||||
@@ -548,8 +552,14 @@ namespace Emby.Server.Implementations
|
|||||||
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
CertificatePath = networkConfiguration.CertificatePath;
|
CertificateInfo = new CertificateInfo
|
||||||
Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
|
{
|
||||||
|
Path = networkConfiguration.CertificatePath,
|
||||||
|
Password = networkConfiguration.CertificatePassword
|
||||||
|
};
|
||||||
|
Certificate = GetCertificate(CertificateInfo);
|
||||||
|
|
||||||
|
DiscoverTypes();
|
||||||
|
|
||||||
RegisterServices();
|
RegisterServices();
|
||||||
|
|
||||||
@@ -565,12 +575,13 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
ServiceCollection.AddMemoryCache();
|
ServiceCollection.AddMemoryCache();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
|
ServiceCollection.AddSingleton(ConfigurationManager);
|
||||||
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
|
|
||||||
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
||||||
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
||||||
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||||
|
|
||||||
|
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton(_fileSystemManager);
|
ServiceCollection.AddSingleton(_fileSystemManager);
|
||||||
ServiceCollection.AddSingleton<TmdbClientManager>();
|
ServiceCollection.AddSingleton<TmdbClientManager>();
|
||||||
|
|
||||||
@@ -593,6 +604,8 @@ namespace Emby.Server.Implementations
|
|||||||
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
||||||
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
||||||
|
|
||||||
|
ServiceCollection.AddSingleton(ServerConfigurationManager);
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||||
@@ -602,8 +615,14 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||||
|
|
||||||
|
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
||||||
|
|
||||||
|
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
||||||
|
ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
|
||||||
|
|
||||||
|
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
||||||
|
ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
|
||||||
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||||
ServiceCollection.AddSingleton<EncodingHelper>();
|
|
||||||
|
|
||||||
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
||||||
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
||||||
@@ -623,6 +642,8 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
|
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
|
||||||
|
|
||||||
|
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
|
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
|
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
|
||||||
@@ -658,21 +679,22 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||||
|
|
||||||
ServiceCollection.AddScoped<ISessionContext, SessionContext>();
|
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
|
||||||
|
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IAuthService, AuthService>();
|
ServiceCollection.AddSingleton<IAuthService, AuthService>();
|
||||||
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
||||||
|
|
||||||
|
ServiceCollection.AddSingleton<EncodingHelper>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<TranscodingJobHelper>();
|
ServiceCollection.AddSingleton<TranscodingJobHelper>();
|
||||||
ServiceCollection.AddScoped<MediaInfoHelper>();
|
ServiceCollection.AddScoped<MediaInfoHelper>();
|
||||||
ServiceCollection.AddScoped<AudioHelper>();
|
ServiceCollection.AddScoped<AudioHelper>();
|
||||||
ServiceCollection.AddScoped<DynamicHlsHelper>();
|
ServiceCollection.AddScoped<DynamicHlsHelper>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -687,6 +709,8 @@ namespace Emby.Server.Implementations
|
|||||||
_mediaEncoder = Resolve<IMediaEncoder>();
|
_mediaEncoder = Resolve<IMediaEncoder>();
|
||||||
_sessionManager = Resolve<ISessionManager>();
|
_sessionManager = Resolve<ISessionManager>();
|
||||||
|
|
||||||
|
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
||||||
|
|
||||||
SetStaticProperties();
|
SetStaticProperties();
|
||||||
|
|
||||||
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
|
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
|
||||||
@@ -715,7 +739,7 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
|
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
|
||||||
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
||||||
logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name);
|
logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
|
||||||
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
||||||
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
||||||
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
||||||
@@ -725,27 +749,30 @@ namespace Emby.Server.Implementations
|
|||||||
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
|
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private X509Certificate2 GetCertificate(string path, string password)
|
private X509Certificate2 GetCertificate(CertificateInfo info)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
var certificateLocation = info?.Path;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(certificateLocation))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(certificateLocation))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't use an empty string password
|
// Don't use an empty string password
|
||||||
password = string.IsNullOrWhiteSpace(password) ? null : password;
|
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
|
||||||
|
|
||||||
var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
|
var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
|
||||||
|
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
|
||||||
if (!localCert.HasPrivateKey)
|
if (!localCert.HasPrivateKey)
|
||||||
{
|
{
|
||||||
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
|
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", certificateLocation);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +780,7 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error loading cert from {CertificateLocation}", path);
|
Logger.LogError(ex, "Error loading cert from {CertificateLocation}", certificateLocation);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -765,7 +792,7 @@ namespace Emby.Server.Implementations
|
|||||||
{
|
{
|
||||||
// For now there's no real way to inject these properly
|
// For now there's no real way to inject these properly
|
||||||
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
||||||
BaseItem.ConfigurationManager = ConfigurationManager;
|
BaseItem.ConfigurationManager = ServerConfigurationManager;
|
||||||
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
||||||
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
||||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||||
@@ -779,6 +806,7 @@ namespace Emby.Server.Implementations
|
|||||||
UserView.CollectionManager = Resolve<ICollectionManager>();
|
UserView.CollectionManager = Resolve<ICollectionManager>();
|
||||||
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
|
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
|
||||||
CollectionFolder.XmlSerializer = _xmlSerializer;
|
CollectionFolder.XmlSerializer = _xmlSerializer;
|
||||||
|
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
|
||||||
CollectionFolder.ApplicationHost = this;
|
CollectionFolder.ApplicationHost = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,12 +815,13 @@ namespace Emby.Server.Implementations
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void FindParts()
|
private void FindParts()
|
||||||
{
|
{
|
||||||
if (!ConfigurationManager.Configuration.IsPortAuthorized)
|
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
|
||||||
{
|
{
|
||||||
ConfigurationManager.Configuration.IsPortAuthorized = true;
|
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
|
||||||
ConfigurationManager.SaveConfiguration();
|
ConfigurationManager.SaveConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||||
_pluginManager.CreatePlugins();
|
_pluginManager.CreatePlugins();
|
||||||
|
|
||||||
_urlPrefixes = GetUrlPrefixes().ToArray();
|
_urlPrefixes = GetUrlPrefixes().ToArray();
|
||||||
@@ -864,6 +893,10 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CertificateInfo CertificateInfo { get; set; }
|
||||||
|
|
||||||
|
public X509Certificate2 Certificate { get; private set; }
|
||||||
|
|
||||||
private IEnumerable<string> GetUrlPrefixes()
|
private IEnumerable<string> GetUrlPrefixes()
|
||||||
{
|
{
|
||||||
var hosts = new[] { "+" };
|
var hosts = new[] { "+" };
|
||||||
@@ -875,7 +908,7 @@ namespace Emby.Server.Implementations
|
|||||||
"http://" + i + ":" + HttpPort + "/"
|
"http://" + i + ":" + HttpPort + "/"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Certificate != null)
|
if (CertificateInfo != null)
|
||||||
{
|
{
|
||||||
prefixes.Add("https://" + i + ":" + HttpsPort + "/");
|
prefixes.Add("https://" + i + ":" + HttpsPort + "/");
|
||||||
}
|
}
|
||||||
@@ -892,7 +925,7 @@ namespace Emby.Server.Implementations
|
|||||||
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var requiresRestart = false;
|
var requiresRestart = false;
|
||||||
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
||||||
|
|
||||||
// Don't do anything if these haven't been set yet
|
// Don't do anything if these haven't been set yet
|
||||||
if (HttpPort != 0 && HttpsPort != 0)
|
if (HttpPort != 0 && HttpsPort != 0)
|
||||||
@@ -901,10 +934,10 @@ namespace Emby.Server.Implementations
|
|||||||
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
||||||
networkConfiguration.HttpsPortNumber != HttpsPort)
|
networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||||
{
|
{
|
||||||
if (ConfigurationManager.Configuration.IsPortAuthorized)
|
if (ServerConfigurationManager.Configuration.IsPortAuthorized)
|
||||||
{
|
{
|
||||||
ConfigurationManager.Configuration.IsPortAuthorized = false;
|
ServerConfigurationManager.Configuration.IsPortAuthorized = false;
|
||||||
ConfigurationManager.SaveConfiguration();
|
ServerConfigurationManager.SaveConfiguration();
|
||||||
|
|
||||||
requiresRestart = true;
|
requiresRestart = true;
|
||||||
}
|
}
|
||||||
@@ -939,13 +972,13 @@ namespace Emby.Server.Implementations
|
|||||||
var newPath = networkConfig.CertificatePath;
|
var newPath = networkConfig.CertificatePath;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(newPath)
|
if (!string.IsNullOrWhiteSpace(newPath)
|
||||||
&& !string.Equals(CertificatePath, newPath, StringComparison.Ordinal))
|
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
if (File.Exists(newPath))
|
if (File.Exists(newPath))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new FileNotFoundException(
|
throw new FileNotFoundException(
|
||||||
string.Format(
|
string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
@@ -1067,9 +1100,9 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the system status.
|
/// Gets the system status.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Where this request originated.</param>
|
/// <param name="source">Where this request originated.</param>
|
||||||
/// <returns>SystemInfo.</returns>
|
/// <returns>SystemInfo.</returns>
|
||||||
public SystemInfo GetSystemInfo(HttpRequest request)
|
public SystemInfo GetSystemInfo(IPAddress source)
|
||||||
{
|
{
|
||||||
return new SystemInfo
|
return new SystemInfo
|
||||||
{
|
{
|
||||||
@@ -1085,14 +1118,16 @@ namespace Emby.Server.Implementations
|
|||||||
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
||||||
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
||||||
CachePath = ApplicationPaths.CachePath,
|
CachePath = ApplicationPaths.CachePath,
|
||||||
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
|
OperatingSystem = OperatingSystem.Id.ToString(),
|
||||||
OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
|
OperatingSystemDisplayName = OperatingSystem.Name,
|
||||||
CanSelfRestart = CanSelfRestart,
|
CanSelfRestart = CanSelfRestart,
|
||||||
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
||||||
|
HasUpdateAvailable = HasUpdateAvailable,
|
||||||
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
||||||
ServerName = FriendlyName,
|
ServerName = FriendlyName,
|
||||||
LocalAddress = GetSmartApiUrl(request),
|
LocalAddress = GetSmartApiUrl(source),
|
||||||
SupportsLibraryMonitor = true,
|
SupportsLibraryMonitor = true,
|
||||||
|
EncoderLocation = _mediaEncoder.EncoderLocation,
|
||||||
SystemArchitecture = RuntimeInformation.OSArchitecture,
|
SystemArchitecture = RuntimeInformation.OSArchitecture,
|
||||||
PackageName = _startupOptions.PackageName
|
PackageName = _startupOptions.PackageName
|
||||||
};
|
};
|
||||||
@@ -1103,22 +1138,25 @@ namespace Emby.Server.Implementations
|
|||||||
.Select(i => new WakeOnLanInfo(i))
|
.Select(i => new WakeOnLanInfo(i))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
|
public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
|
||||||
{
|
{
|
||||||
return new PublicSystemInfo
|
return new PublicSystemInfo
|
||||||
{
|
{
|
||||||
Version = ApplicationVersionString,
|
Version = ApplicationVersionString,
|
||||||
ProductName = ApplicationProductName,
|
ProductName = ApplicationProductName,
|
||||||
Id = SystemId,
|
Id = SystemId,
|
||||||
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
|
OperatingSystem = OperatingSystem.Id.ToString(),
|
||||||
ServerName = FriendlyName,
|
ServerName = FriendlyName,
|
||||||
LocalAddress = GetSmartApiUrl(request),
|
LocalAddress = GetSmartApiUrl(source),
|
||||||
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
|
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
@@ -1127,7 +1165,7 @@ namespace Emby.Server.Implementations
|
|||||||
return PublishedServerUrl.Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(remoteAddr, out port);
|
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
||||||
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
||||||
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -1140,18 +1178,6 @@ namespace Emby.Server.Implementations
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
||||||
{
|
{
|
||||||
// Return the host in the HTTP request as the API url
|
|
||||||
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
|
|
||||||
{
|
|
||||||
int? requestPort = request.Host.Port;
|
|
||||||
if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
requestPort = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
@@ -1202,20 +1228,27 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
|
public string GetLocalApiUrl(string host, string scheme = null, int? port = null)
|
||||||
{
|
{
|
||||||
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
||||||
// not. For consistency, always trim the trailing slash.
|
// not. For consistency, always trim the trailing slash.
|
||||||
return new UriBuilder
|
return new UriBuilder
|
||||||
{
|
{
|
||||||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
||||||
Host = hostname,
|
Host = host,
|
||||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
||||||
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||||
}.ToString().TrimEnd('/');
|
}.ToString().TrimEnd('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
public string FriendlyName =>
|
||||||
|
string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
|
||||||
|
? Environment.MachineName
|
||||||
|
: ServerConfigurationManager.Configuration.ServerName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shuts down.
|
||||||
|
/// </summary>
|
||||||
public async Task Shutdown()
|
public async Task Shutdown()
|
||||||
{
|
{
|
||||||
if (IsShuttingDown)
|
if (IsShuttingDown)
|
||||||
@@ -1239,6 +1272,26 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
protected abstract void ShutdownInternal();
|
protected abstract void ShutdownInternal();
|
||||||
|
|
||||||
|
public event EventHandler HasUpdateAvailableChanged;
|
||||||
|
|
||||||
|
private bool _hasUpdateAvailable;
|
||||||
|
|
||||||
|
public bool HasUpdateAvailable
|
||||||
|
{
|
||||||
|
get => _hasUpdateAvailable;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var fireEvent = value && !_hasUpdateAvailable;
|
||||||
|
|
||||||
|
_hasUpdateAvailable = value;
|
||||||
|
|
||||||
|
if (fireEvent)
|
||||||
|
{
|
||||||
|
HasUpdateAvailableChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
||||||
{
|
{
|
||||||
var assemblies = _allConcreteTypes
|
var assemblies = _allConcreteTypes
|
||||||
@@ -1253,7 +1306,41 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
public virtual void LaunchUrl(string url)
|
||||||
|
{
|
||||||
|
if (!CanLaunchWebBrowser)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = url,
|
||||||
|
UseShellExecute = true,
|
||||||
|
ErrorDialog = false
|
||||||
|
},
|
||||||
|
EnableRaisingEvents = true
|
||||||
|
};
|
||||||
|
process.Exited += (sender, args) => ((Process)sender).Dispose();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error launching url: {url}", url);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||||
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
@@ -1298,4 +1385,11 @@ namespace Emby.Server.Implementations
|
|||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class CertificateInfo
|
||||||
|
{
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ namespace Emby.Server.Implementations.Archiving
|
|||||||
options.Overwrite = true;
|
options.Overwrite = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(targetPath);
|
|
||||||
reader.WriteAllToDirectory(targetPath, options);
|
reader.WriteAllToDirectory(targetPath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +58,6 @@ namespace Emby.Server.Implementations.Archiving
|
|||||||
Overwrite = overwriteExistingFiles
|
Overwrite = overwriteExistingFiles
|
||||||
};
|
};
|
||||||
|
|
||||||
Directory.CreateDirectory(targetPath);
|
|
||||||
reader.WriteAllToDirectory(targetPath, options);
|
reader.WriteAllToDirectory(targetPath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +71,6 @@ namespace Emby.Server.Implementations.Archiving
|
|||||||
Overwrite = overwriteExistingFiles
|
Overwrite = overwriteExistingFiles
|
||||||
};
|
};
|
||||||
|
|
||||||
Directory.CreateDirectory(targetPath);
|
|
||||||
reader.WriteAllToDirectory(targetPath, options);
|
reader.WriteAllToDirectory(targetPath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +120,6 @@ namespace Emby.Server.Implementations.Archiving
|
|||||||
Overwrite = overwriteExistingFiles
|
Overwrite = overwriteExistingFiles
|
||||||
};
|
};
|
||||||
|
|
||||||
Directory.CreateDirectory(targetPath);
|
|
||||||
reader.WriteAllToDirectory(targetPath, options);
|
reader.WriteAllToDirectory(targetPath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +151,6 @@ namespace Emby.Server.Implementations.Archiving
|
|||||||
Overwrite = overwriteExistingFiles
|
Overwrite = overwriteExistingFiles
|
||||||
};
|
};
|
||||||
|
|
||||||
Directory.CreateDirectory(targetPath);
|
|
||||||
reader.WriteAllToDirectory(targetPath, options);
|
reader.WriteAllToDirectory(targetPath, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions.Json;
|
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Progress;
|
using MediaBrowser.Common.Progress;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
@@ -25,6 +21,7 @@ using MediaBrowser.Model.Dto;
|
|||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
|
using MediaBrowser.Model.Serialization;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||||
@@ -47,10 +44,10 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
private readonly ILogger<ChannelManager> _logger;
|
private readonly ILogger<ChannelManager> _logger;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IProviderManager _providerManager;
|
private readonly IProviderManager _providerManager;
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly IMemoryCache _memoryCache;
|
||||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
|
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
|
||||||
@@ -62,6 +59,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
/// <param name="config">The server configuration manager.</param>
|
/// <param name="config">The server configuration manager.</param>
|
||||||
/// <param name="fileSystem">The filesystem.</param>
|
/// <param name="fileSystem">The filesystem.</param>
|
||||||
/// <param name="userDataManager">The user data manager.</param>
|
/// <param name="userDataManager">The user data manager.</param>
|
||||||
|
/// <param name="jsonSerializer">The JSON serializer.</param>
|
||||||
/// <param name="providerManager">The provider manager.</param>
|
/// <param name="providerManager">The provider manager.</param>
|
||||||
/// <param name="memoryCache">The memory cache.</param>
|
/// <param name="memoryCache">The memory cache.</param>
|
||||||
public ChannelManager(
|
public ChannelManager(
|
||||||
@@ -72,6 +70,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IUserDataManager userDataManager,
|
IUserDataManager userDataManager,
|
||||||
|
IJsonSerializer jsonSerializer,
|
||||||
IProviderManager providerManager,
|
IProviderManager providerManager,
|
||||||
IMemoryCache memoryCache)
|
IMemoryCache memoryCache)
|
||||||
{
|
{
|
||||||
@@ -82,6 +81,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
_config = config;
|
_config = config;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_userDataManager = userDataManager;
|
_userDataManager = userDataManager;
|
||||||
|
_jsonSerializer = jsonSerializer;
|
||||||
_providerManager = providerManager;
|
_providerManager = providerManager;
|
||||||
_memoryCache = memoryCache;
|
_memoryCache = memoryCache;
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
|
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
|
||||||
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
|
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
|
||||||
|
|
||||||
return channel is not IDisableMediaSourceDisplay;
|
return !(channel is IDisableMediaSourceDisplay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -337,23 +337,21 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
|
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
|
private List<MediaSourceInfo> GetSavedMediaSources(BaseItem item)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
|
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bytes = File.ReadAllBytes(path);
|
return _jsonSerializer.DeserializeFromFile<List<MediaSourceInfo>>(path) ?? new List<MediaSourceInfo>();
|
||||||
return JsonSerializer.Deserialize<MediaSourceInfo[]>(bytes, _jsonOptions)
|
|
||||||
?? Array.Empty<MediaSourceInfo>();
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return Array.Empty<MediaSourceInfo>();
|
return new List<MediaSourceInfo>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
|
private void SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
|
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
|
||||||
|
|
||||||
@@ -372,8 +370,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||||
|
|
||||||
await using FileStream createStream = File.Create(path);
|
_jsonSerializer.SerializeToFile(mediaSources, path);
|
||||||
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -586,7 +583,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
{
|
{
|
||||||
var supportsLatest = provider is ISupportsLatestMedia;
|
var supportsLatest = provider is ISupportsLatestMedia;
|
||||||
|
|
||||||
return new ChannelFeatures(channel.Name, channel.Id)
|
return new ChannelFeatures
|
||||||
{
|
{
|
||||||
CanFilter = !features.MaxPageSize.HasValue,
|
CanFilter = !features.MaxPageSize.HasValue,
|
||||||
CanSearch = provider is ISearchableChannel,
|
CanSearch = provider is ISearchableChannel,
|
||||||
@@ -596,6 +593,8 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
MediaTypes = features.MediaTypes.ToArray(),
|
MediaTypes = features.MediaTypes.ToArray(),
|
||||||
SupportsSortOrderToggle = features.SupportsSortOrderToggle,
|
SupportsSortOrderToggle = features.SupportsSortOrderToggle,
|
||||||
SupportsLatestMedia = supportsLatest,
|
SupportsLatestMedia = supportsLatest,
|
||||||
|
Name = channel.Name,
|
||||||
|
Id = channel.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||||
SupportsContentDownloading = features.SupportsContentDownloading,
|
SupportsContentDownloading = features.SupportsContentDownloading,
|
||||||
AutoRefreshLevels = features.AutoRefreshLevels
|
AutoRefreshLevels = features.AutoRefreshLevels
|
||||||
};
|
};
|
||||||
@@ -813,8 +812,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
{
|
{
|
||||||
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
|
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
|
var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
|
||||||
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (cachedResult != null)
|
if (cachedResult != null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -836,8 +834,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
{
|
{
|
||||||
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
|
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
|
var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
|
||||||
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (cachedResult != null)
|
if (cachedResult != null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -868,7 +865,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
|
throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
|
||||||
}
|
}
|
||||||
|
|
||||||
await CacheResponse(result, cachePath);
|
CacheResponse(result, cachePath);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -878,14 +875,13 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CacheResponse(ChannelItemResult result, string path)
|
private void CacheResponse(object result, string path)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||||
|
|
||||||
await using FileStream createStream = File.Create(path);
|
_jsonSerializer.SerializeToFile(result, path);
|
||||||
await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -1077,11 +1073,11 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
|
|
||||||
// was used for status
|
// was used for status
|
||||||
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
|
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
|
||||||
// {
|
//{
|
||||||
// item.ExternalEtag = info.Etag;
|
// item.ExternalEtag = info.Etag;
|
||||||
// forceUpdate = true;
|
// forceUpdate = true;
|
||||||
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
|
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
|
||||||
// }
|
//}
|
||||||
|
|
||||||
if (!internalChannelId.Equals(item.ChannelId))
|
if (!internalChannelId.Equals(item.ChannelId))
|
||||||
{
|
{
|
||||||
@@ -1180,11 +1176,11 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
{
|
{
|
||||||
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
|
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
|
||||||
{
|
{
|
||||||
await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
|
SaveMediaSources(item, new List<MediaSourceInfo>());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
|
SaveMediaSources(item, info.MediaSources);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user