mirror of https://github.com/jellyfin/jellyfin.git
Merge branch 'master' into comparisons
This commit is contained in:
commit
634ce40c2f
|
@ -7,7 +7,7 @@ parameters:
|
||||||
default: "ubuntu-latest"
|
default: "ubuntu-latest"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.103
|
default: 6.0.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: CompatibilityCheck
|
- job: CompatibilityCheck
|
||||||
|
|
|
@ -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: 5.0.103
|
DotNetSdkVersion: 6.0.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Build
|
- job: Build
|
||||||
|
@ -91,3 +91,10 @@ 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'
|
||||||
|
|
|
@ -39,6 +39,14 @@ jobs:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||||
|
displayName: Set release version (stable)
|
||||||
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
|
|
||||||
|
- script: './bump-version $(JellyfinVersion)'
|
||||||
|
displayName: Bump internal version (stable)
|
||||||
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
|
|
||||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
|
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
|
||||||
displayName: 'Build Dockerfile'
|
displayName: 'Build Dockerfile'
|
||||||
|
|
||||||
|
@ -80,6 +88,14 @@ jobs:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||||
|
displayName: Set release version (stable)
|
||||||
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
|
|
||||||
|
- script: './bump-version $(JellyfinVersion)'
|
||||||
|
displayName: Bump internal version (stable)
|
||||||
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
|
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: 'Download OpenAPI Spec'
|
displayName: 'Download OpenAPI Spec'
|
||||||
inputs:
|
inputs:
|
||||||
|
@ -127,6 +143,10 @@ jobs:
|
||||||
displayName: Set release version (stable)
|
displayName: Set release version (stable)
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
|
|
||||||
|
- script: './bump-version $(JellyfinVersion)'
|
||||||
|
displayName: Bump internal version (stable)
|
||||||
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
|
|
||||||
- task: Docker@2
|
- task: Docker@2
|
||||||
displayName: 'Push Unstable Image'
|
displayName: 'Push Unstable Image'
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||||
|
@ -181,7 +201,7 @@ jobs:
|
||||||
inputs:
|
inputs:
|
||||||
sshEndpoint: repository
|
sshEndpoint: repository
|
||||||
runOptions: 'commands'
|
runOptions: 'commands'
|
||||||
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
|
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
|
||||||
|
|
||||||
- job: PublishNuget
|
- job: PublishNuget
|
||||||
displayName: 'Publish NuGet packages'
|
displayName: 'Publish NuGet packages'
|
||||||
|
@ -195,10 +215,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Use .NET 5.0 sdk'
|
displayName: 'Use .NET 6.0 sdk'
|
||||||
inputs:
|
inputs:
|
||||||
packageType: 'sdk'
|
packageType: 'sdk'
|
||||||
version: '5.0.x'
|
version: '6.0.x'
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
- task: DotNetCoreCLI@2
|
||||||
displayName: 'Build Stable Nuget packages'
|
displayName: 'Build Stable Nuget packages'
|
||||||
|
@ -211,6 +231,7 @@ 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)
|
||||||
|
|
||||||
|
@ -225,6 +246,7 @@ 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: 5.0.103
|
default: 6.0.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Test
|
- job: Test
|
||||||
|
@ -94,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/net5.0/openapi.json"
|
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json"
|
||||||
artifactName: 'OpenAPI Spec'
|
artifactName: 'OpenAPI Spec'
|
||||||
|
|
|
@ -5,8 +5,6 @@ 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
|
||||||
|
@ -57,6 +55,9 @@ 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')) }}:
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../fedora/Makefile
|
|
|
@ -1,49 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a bug report
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
<!-- A clear and concise description of what the bug is. -->
|
|
||||||
|
|
||||||
**System (please complete the following information):**
|
|
||||||
- OS: [e.g. Debian, Windows]
|
|
||||||
- Virtualization: [e.g. Docker, KVM, LXC]
|
|
||||||
- Clients: [Browser, Android, Fire Stick, etc.]
|
|
||||||
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
|
|
||||||
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
|
|
||||||
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
|
|
||||||
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
|
|
||||||
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
|
|
||||||
- Base URL: [e.g. none, yes: /example]
|
|
||||||
- Networking: [e.g. Host, Bridge/NAT]
|
|
||||||
- Storage: [e.g. local, NFS, cloud]
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
<!-- Steps to reproduce the behavior: -->
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
<!-- A clear and concise description of what you expected to happen. -->
|
|
||||||
|
|
||||||
**Server Logs**
|
|
||||||
<!-- Please paste any log errors. -->
|
|
||||||
|
|
||||||
**FFmpeg Logs**
|
|
||||||
<!-- Please paste any log errors. -->
|
|
||||||
|
|
||||||
**Browser Console Logs**
|
|
||||||
<!-- Please paste any log errors. -->
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
<!-- Add any other context about the problem here. -->
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
name: Issue Report
|
||||||
|
description: File an issue report
|
||||||
|
title: "[Issue]: "
|
||||||
|
labels: [bug, triage]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: Please describe your bug
|
||||||
|
description: Also tell us, what did you expect to happen?
|
||||||
|
placeholder: |
|
||||||
|
The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
|
||||||
|
|
||||||
|
This is my issue.
|
||||||
|
|
||||||
|
Steps to Reproduce
|
||||||
|
1. In this environment...
|
||||||
|
2. With this config...
|
||||||
|
3. Run '...'
|
||||||
|
4. See error...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Jellyfin Version
|
||||||
|
description: What version of Jellyfin are you running?
|
||||||
|
options:
|
||||||
|
- 10.7.7
|
||||||
|
- 10.7.z
|
||||||
|
- 10.6.4
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: version-other
|
||||||
|
attributes:
|
||||||
|
label: "if other:"
|
||||||
|
placeholder: Other
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Environment
|
||||||
|
description: |
|
||||||
|
Examples:
|
||||||
|
- **OS**: [e.g. Debian, Windows]
|
||||||
|
- **Virtualization**: [e.g. Docker, KVM, LXC]
|
||||||
|
- **Clients**: [Browser, Android, Fire Stick, etc.]
|
||||||
|
- **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
|
||||||
|
- **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
|
||||||
|
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
|
||||||
|
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
|
||||||
|
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
|
||||||
|
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
||||||
|
- **Base URL**: [e.g. none, yes: /example]
|
||||||
|
- **Networking**: [e.g. Host, Bridge/NAT]
|
||||||
|
- **Storage**: [e.g. local, NFS, cloud]
|
||||||
|
value: |
|
||||||
|
- OS:
|
||||||
|
- Virtualization:
|
||||||
|
- Clients:
|
||||||
|
- Browser:
|
||||||
|
- FFmpeg Version:
|
||||||
|
- Playback Method:
|
||||||
|
- Hardware Acceleration:
|
||||||
|
- Plugins:
|
||||||
|
- Reverse Proxy:
|
||||||
|
- Base URL:
|
||||||
|
- Networking:
|
||||||
|
- Storage:
|
||||||
|
render: markdown
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Jellyfin logs
|
||||||
|
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||||
|
placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
|
||||||
|
render: shell
|
||||||
|
- type: textarea
|
||||||
|
id: ffmpeg-logs
|
||||||
|
attributes:
|
||||||
|
label: FFmpeg logs
|
||||||
|
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||||
|
placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
|
||||||
|
render: shell
|
||||||
|
- type: textarea
|
||||||
|
id: browserlogs
|
||||||
|
attributes:
|
||||||
|
label: Please attach any browser or client logs here
|
||||||
|
placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Please attach any screenshots here
|
||||||
|
placeholder: Images can be pasted directly into the textbox and will be hosted by github.
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Code of Conduct
|
||||||
|
description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
|
||||||
|
options:
|
||||||
|
- label: I agree to follow this project's Code of Conduct
|
||||||
|
required: true
|
|
@ -23,3 +23,7 @@ markComment: >
|
||||||
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
|
||||||
|
|
|
@ -11,6 +11,7 @@ jobs:
|
||||||
label:
|
label:
|
||||||
name: Labeling
|
name: Labeling
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Apply label
|
- name: Apply label
|
||||||
uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
||||||
|
@ -22,9 +23,10 @@ jobs:
|
||||||
project:
|
project:
|
||||||
name: Project board
|
name: Project board
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Remove from 'Current Release' project
|
- name: Remove from 'Current Release' project
|
||||||
uses: alex-page/github-project-automation-plus@v0.7.1
|
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')
|
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
@ -33,7 +35,7 @@ jobs:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add to 'Release Next' project
|
- name: Add to 'Release Next' project
|
||||||
uses: alex-page/github-project-automation-plus@v0.7.1
|
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||||
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
@ -42,7 +44,7 @@ jobs:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add to 'Current Release' project
|
- name: Add to 'Current Release' project
|
||||||
uses: alex-page/github-project-automation-plus@v0.7.1
|
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')
|
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
@ -56,7 +58,7 @@ jobs:
|
||||||
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
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
|
- name: Move issue to needs triage
|
||||||
uses: alex-page/github-project-automation-plus@v0.7.1
|
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
|
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
@ -65,7 +67,7 @@ jobs:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add issue to triage project
|
- name: Add issue to triage project
|
||||||
uses: alex-page/github-project-automation-plus@v0.7.1
|
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||||
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -24,7 +24,8 @@ jobs:
|
||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '5.0.x'
|
dotnet-version: '6.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -29,7 +29,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Automatic Rebase
|
- name: Automatic Rebase
|
||||||
uses: cirrus-actions/rebase@1.4
|
uses: cirrus-actions/rebase@1.5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
name: OpenAPI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request_target:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
openapi-head:
|
||||||
|
name: OpenAPI - HEAD
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: read-all
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
- name: Setup .NET Core
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: '6.0.x'
|
||||||
|
- name: Generate openapi.json
|
||||||
|
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||||
|
- name: Upload openapi.json
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: openapi-head
|
||||||
|
retention-days: 14
|
||||||
|
if-no-files-found: error
|
||||||
|
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
|
||||||
|
|
||||||
|
openapi-base:
|
||||||
|
name: OpenAPI - BASE
|
||||||
|
if: ${{ github.base_ref != '' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: read-all
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
- name: Setup .NET Core
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: '6.0.x'
|
||||||
|
- name: Generate openapi.json
|
||||||
|
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||||
|
- name: Upload openapi.json
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: openapi-base
|
||||||
|
retention-days: 14
|
||||||
|
if-no-files-found: error
|
||||||
|
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
|
||||||
|
|
||||||
|
openapi-diff:
|
||||||
|
name: OpenAPI - Difference
|
||||||
|
if: ${{ github.event_name == 'pull_request_target' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- openapi-head
|
||||||
|
- openapi-base
|
||||||
|
steps:
|
||||||
|
- name: Download openapi-head
|
||||||
|
uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: openapi-head
|
||||||
|
path: openapi-head
|
||||||
|
- name: Download openapi-base
|
||||||
|
uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: openapi-base
|
||||||
|
path: openapi-base
|
||||||
|
- name: Workaround openapi-diff issue
|
||||||
|
run: |
|
||||||
|
sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
|
||||||
|
sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
|
||||||
|
- name: Calculate OpenAPI difference
|
||||||
|
uses: docker://openapitools/openapi-diff
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
|
||||||
|
- id: read-diff
|
||||||
|
name: Read openapi-diff output
|
||||||
|
run: |
|
||||||
|
body=$(cat openapi-changes.md)
|
||||||
|
body="${body//'%'/'%25'}"
|
||||||
|
body="${body//$'\n'/'%0A'}"
|
||||||
|
body="${body//$'\r'/'%0D'}"
|
||||||
|
echo ::set-output name=body::$body
|
||||||
|
- name: Find difference comment
|
||||||
|
uses: peter-evans/find-comment@v1
|
||||||
|
id: find-comment
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
direction: last
|
||||||
|
body-includes: openapi-diff-workflow-comment
|
||||||
|
- name: Reply or edit difference comment (changed)
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ steps.read-diff.outputs.body != '' }}
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||||
|
edit-mode: replace
|
||||||
|
body: |
|
||||||
|
<!--openapi-diff-workflow-comment-->
|
||||||
|
<details>
|
||||||
|
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
|
||||||
|
|
||||||
|
${{ steps.read-diff.outputs.body }}
|
||||||
|
|
||||||
|
</details>
|
||||||
|
- name: Edit difference comment (unchanged)
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||||
|
edit-mode: replace
|
||||||
|
body: |
|
||||||
|
<!--openapi-diff-workflow-comment-->
|
||||||
|
|
||||||
|
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
|
@ -278,3 +278,6 @@ web/
|
||||||
web-src.*
|
web-src.*
|
||||||
MediaBrowser.WebDashboard/jellyfin-web
|
MediaBrowser.WebDashboard/jellyfin-web
|
||||||
apiclient/generated
|
apiclient/generated
|
||||||
|
|
||||||
|
# Omnisharp crash logs
|
||||||
|
mono_crash.*.json
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.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/net5.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
|
||||||
"args": ["--nowebclient"],
|
"args": ["--nowebclient"],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
- [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)
|
||||||
|
@ -148,6 +149,8 @@
|
||||||
- [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)
|
- [peterspenler](https://github.com/peterspenler)
|
||||||
|
- [MBR-0001](https://github.com/MBR-0001)
|
||||||
|
- [jonas-resch](https://github.com/jonas-resch)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
@ -212,3 +215,5 @@
|
||||||
- [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)
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
<Project>
|
||||||
|
<!-- Sets defaults for all projects in the repo -->
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
50
Dockerfile
50
Dockerfile
|
@ -1,4 +1,8 @@
|
||||||
ARG DOTNET_VERSION=5.0
|
# DESIGNED FOR BUILDING ON AMD64 ONLY
|
||||||
|
#####################################
|
||||||
|
# Requires binfm_misc registration
|
||||||
|
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||||
|
ARG DOTNET_VERSION=6.0
|
||||||
|
|
||||||
FROM node:lts-alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=master
|
||||||
|
@ -8,15 +12,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
|
||||||
&& npm ci --no-audit --unsafe-perm \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
FROM debian:stable-slim as app
|
||||||
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"
|
||||||
|
@ -25,19 +21,17 @@ 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=20.3.2
|
ARG GMMLIB_VERSION=21.2.1
|
||||||
ARG IGC_VERSION=1.0.5435
|
ARG IGC_VERSION=1.0.8517
|
||||||
ARG NEO_VERSION=20.46.18421
|
ARG NEO_VERSION=21.35.20826
|
||||||
ARG LEVEL_ZERO_VERSION=1.0.18421
|
ARG LEVEL_ZERO_VERSION=1.2.20826
|
||||||
|
|
||||||
# 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 \
|
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https curl \
|
||||||
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
|
&& 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 \
|
||||||
|
@ -68,14 +62,32 @@ 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
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# DESIGNED FOR BUILDING ON AMD64 ONLY
|
# DESIGNED FOR BUILDING ON ARM 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=5.0
|
ARG DOTNET_VERSION=6.0
|
||||||
|
|
||||||
|
|
||||||
FROM node:lts-alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
|
@ -13,19 +13,8 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
|
||||||
&& npm ci --no-audit --unsafe-perm \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& 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:buster-slim
|
FROM arm32v7/debian:stable-slim as app
|
||||||
|
|
||||||
# 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"
|
||||||
|
@ -35,6 +24,8 @@ 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 - && \
|
||||||
|
@ -53,7 +44,7 @@ RUN apt-get update \
|
||||||
vainfo \
|
vainfo \
|
||||||
libva2 \
|
libva2 \
|
||||||
locales \
|
locales \
|
||||||
&& apt-get remove curl gnupg -y \
|
&& apt-get remove 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/* \
|
||||||
|
@ -61,17 +52,33 @@ 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
|
||||||
|
|
||||||
COPY --from=builder /jellyfin /jellyfin
|
# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||||
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
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# DESIGNED FOR BUILDING ON AMD64 ONLY
|
# DESIGNED FOR BUILDING ON ARM64 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=5.0
|
ARG DOTNET_VERSION=6.0
|
||||||
|
|
||||||
|
|
||||||
FROM node:lts-alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
|
@ -13,6 +13,40 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
|
||||||
&& npm ci --no-audit --unsafe-perm \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& 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
|
||||||
|
@ -23,44 +57,19 @@ 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 multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
FROM app
|
||||||
FROM arm64v8/debian:buster-slim
|
|
||||||
|
|
||||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
ENV HEALTHCHECK_URL=http://localhost:8096/health
|
||||||
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,10 +10,11 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ namespace DvdLib.Ifo
|
||||||
|
|
||||||
private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
|
private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
|
||||||
{
|
{
|
||||||
var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
|
var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
|
||||||
|
|
||||||
var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
|
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));
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
namespace Emby.Dlna.Configuration
|
namespace Emby.Dlna.Configuration
|
||||||
|
@ -74,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,5 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -140,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))
|
||||||
{
|
{
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,3 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
|
||||||
namespace Emby.Dlna.ContentDirectory
|
namespace Emby.Dlna.ContentDirectory
|
||||||
|
@ -13,24 +11,29 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
/// Initializes a new instance of the <see cref="ServerItem"/> class.
|
/// Initializes a new instance of the <see cref="ServerItem"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="item">The <see cref="BaseItem"/>.</param>
|
/// <param name="item">The <see cref="BaseItem"/>.</param>
|
||||||
public ServerItem(BaseItem item)
|
/// <param name="stubType">The stub type.</param>
|
||||||
|
public ServerItem(BaseItem item, StubType? stubType)
|
||||||
{
|
{
|
||||||
Item = item;
|
Item = item;
|
||||||
|
|
||||||
if (item is IItemByName && !(item is Folder))
|
if (stubType.HasValue)
|
||||||
|
{
|
||||||
|
StubType = stubType;
|
||||||
|
}
|
||||||
|
else if (item is IItemByName and not Folder)
|
||||||
{
|
{
|
||||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the underlying base item.
|
/// Gets the underlying base item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BaseItem Item { get; set; }
|
public BaseItem Item { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the DLNA item type.
|
/// Gets the DLNA item type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public StubType? StubType { get; set; }
|
public StubType? StubType { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -8,9 +6,11 @@ namespace Emby.Dlna
|
||||||
{
|
{
|
||||||
public class ControlResponse
|
public class ControlResponse
|
||||||
{
|
{
|
||||||
public ControlResponse()
|
public ControlResponse(string xml, bool isSuccessful)
|
||||||
{
|
{
|
||||||
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; }
|
||||||
|
|
|
@ -41,8 +41,6 @@ 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;
|
||||||
|
@ -317,7 +315,7 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
if (mediaSource.RunTimeTicks.HasValue)
|
if (mediaSource.RunTimeTicks.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
|
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.Contains("res@size"))
|
if (filter.Contains("res@size"))
|
||||||
|
@ -328,7 +326,7 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
if (size.HasValue)
|
if (size.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
|
writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -342,7 +340,7 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
if (targetChannels.HasValue)
|
if (targetChannels.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
|
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.Contains("res@resolution"))
|
if (filter.Contains("res@resolution"))
|
||||||
|
@ -361,12 +359,12 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
if (targetSampleRate.HasValue)
|
if (targetSampleRate.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
|
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalBitrate.HasValue)
|
if (totalBitrate.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
|
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaProfile = _profile.GetVideoMediaProfile(
|
var mediaProfile = _profile.GetVideoMediaProfile(
|
||||||
|
@ -552,7 +550,7 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
if (mediaSource.RunTimeTicks.HasValue)
|
if (mediaSource.RunTimeTicks.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
|
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.Contains("res@size"))
|
if (filter.Contains("res@size"))
|
||||||
|
@ -563,7 +561,7 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
if (size.HasValue)
|
if (size.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
|
writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -575,17 +573,17 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
if (targetChannels.HasValue)
|
if (targetChannels.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
|
writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetSampleRate.HasValue)
|
if (targetSampleRate.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
|
writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetAudioBitrate.HasValue)
|
if (targetAudioBitrate.HasValue)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
|
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaProfile = _profile.GetAudioMediaProfile(
|
var mediaProfile = _profile.GetAudioMediaProfile(
|
||||||
|
@ -639,7 +637,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(_usCulture));
|
writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
var clientId = GetClientId(folder, stubType);
|
var clientId = GetClientId(folder, stubType);
|
||||||
|
|
||||||
|
@ -731,7 +729,7 @@ namespace Emby.Dlna.Didl
|
||||||
{
|
{
|
||||||
if (item.PremiereDate.HasValue)
|
if (item.PremiereDate.HasValue)
|
||||||
{
|
{
|
||||||
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
|
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -748,7 +746,7 @@ namespace Emby.Dlna.Didl
|
||||||
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
|
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(item is Folder))
|
if (item is not Folder)
|
||||||
{
|
{
|
||||||
if (filter.Contains("dc:description"))
|
if (filter.Contains("dc:description"))
|
||||||
{
|
{
|
||||||
|
@ -931,11 +929,11 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
if (item.IndexNumber.HasValue)
|
if (item.IndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
|
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
|
||||||
|
|
||||||
if (item is Episode)
|
if (item is Episode)
|
||||||
{
|
{
|
||||||
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
|
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,18 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#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;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
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.Common.Json;
|
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
|
@ -96,12 +92,14 @@ namespace Emby.Dlna
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public DeviceProfile GetDefaultProfile()
|
public DeviceProfile GetDefaultProfile()
|
||||||
{
|
{
|
||||||
return new DefaultProfile();
|
return new DefaultProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
|
/// <inheritdoc />
|
||||||
|
public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
|
||||||
{
|
{
|
||||||
if (deviceInfo == null)
|
if (deviceInfo == null)
|
||||||
{
|
{
|
||||||
|
@ -111,35 +109,18 @@ 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)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
_logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LogUnmatchedProfile(deviceInfo);
|
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogUnmatchedProfile(DeviceIdentification profile)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
|
|
||||||
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
|
||||||
builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
|
|
||||||
builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
|
|
||||||
builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
|
|
||||||
builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
|
|
||||||
builder.Append("ModelName: ").AppendLine(profile.ModelName);
|
|
||||||
builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
|
|
||||||
builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
|
|
||||||
builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
|
|
||||||
|
|
||||||
_logger.LogInformation(builder.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to match a device with a profile.
|
/// Attempts to match a device with a profile.
|
||||||
/// Rules:
|
/// Rules:
|
||||||
|
@ -187,7 +168,8 @@ namespace Emby.Dlna
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeviceProfile GetProfile(IHeaderDictionary headers)
|
/// <inheritdoc />
|
||||||
|
public DeviceProfile? GetProfile(IHeaderDictionary headers)
|
||||||
{
|
{
|
||||||
if (headers == null)
|
if (headers == null)
|
||||||
{
|
{
|
||||||
|
@ -195,15 +177,13 @@ 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("Found matching device profile: {0}", profile.Name);
|
_logger.LogDebug("No matching device profile found. {@Headers}", headers);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
|
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||||
_logger.LogDebug("No matching device profile found. {0}", headerString);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
|
@ -253,19 +233,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();
|
.ToList()!; // We just filtered out all the nulls
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return new List<DeviceProfile>();
|
return Array.Empty<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;
|
||||||
}
|
}
|
||||||
|
@ -293,7 +273,8 @@ namespace Emby.Dlna
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeviceProfile GetProfile(string id)
|
/// <inheritdoc />
|
||||||
|
public DeviceProfile? GetProfile(string id)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(id))
|
if (string.IsNullOrEmpty(id))
|
||||||
{
|
{
|
||||||
|
@ -322,6 +303,7 @@ 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,17 +311,14 @@ 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()
|
||||||
|
@ -359,16 +338,20 @@ namespace Emby.Dlna
|
||||||
systemProfilesPath,
|
systemProfilesPath,
|
||||||
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
|
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
|
||||||
|
|
||||||
using (var stream = _assembly.GetManifestResourceStream(name))
|
// The stream should exist as we just got its name from GetManifestResourceNames
|
||||||
|
using (var stream = _assembly.GetManifestResourceStream(name)!)
|
||||||
{
|
{
|
||||||
|
var length = stream.Length;
|
||||||
var fileInfo = _fileSystem.GetFileInfo(path);
|
var fileInfo = _fileSystem.GetFileInfo(path);
|
||||||
|
|
||||||
if (!fileInfo.Exists || fileInfo.Length != stream.Length)
|
if (!fileInfo.Exists || fileInfo.Length != length)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(systemProfilesPath);
|
Directory.CreateDirectory(systemProfilesPath);
|
||||||
|
|
||||||
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
var fileOptions = AsyncFile.WriteOptions;
|
||||||
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
fileOptions.Mode = FileMode.Create;
|
||||||
|
fileOptions.PreallocationSize = length;
|
||||||
|
using (var fileStream = new FileStream(path, fileOptions))
|
||||||
{
|
{
|
||||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -380,6 +363,7 @@ 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));
|
||||||
|
@ -397,6 +381,7 @@ namespace Emby.Dlna
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void CreateProfile(DeviceProfile profile)
|
public void CreateProfile(DeviceProfile profile)
|
||||||
{
|
{
|
||||||
profile = ReserializeProfile(profile);
|
profile = ReserializeProfile(profile);
|
||||||
|
@ -412,7 +397,8 @@ namespace Emby.Dlna
|
||||||
SaveProfile(profile, path, DeviceProfileType.User);
|
SaveProfile(profile, path, DeviceProfileType.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateProfile(DeviceProfile profile)
|
/// <inheritdoc />
|
||||||
|
public void UpdateProfile(string profileId, DeviceProfile profile)
|
||||||
{
|
{
|
||||||
profile = ReserializeProfile(profile);
|
profile = ReserializeProfile(profile);
|
||||||
|
|
||||||
|
@ -426,7 +412,7 @@ namespace Emby.Dlna
|
||||||
throw new ArgumentException("Profile is missing Name");
|
throw new ArgumentException("Profile is missing Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
|
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
|
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
|
||||||
var path = Path.Combine(UserProfilesPath, newFilename);
|
var path = Path.Combine(UserProfilesPath, newFilename);
|
||||||
|
@ -470,9 +456,11 @@ namespace Emby.Dlna
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(profile, _jsonOptions);
|
var json = JsonSerializer.Serialize(profile, _jsonOptions);
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
|
// Output can't be null if the input isn't null
|
||||||
|
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();
|
||||||
|
@ -482,26 +470,37 @@ namespace Emby.Dlna
|
||||||
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageStream GetIcon(string filename)
|
/// <inheritdoc />
|
||||||
|
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);
|
||||||
return new ImageStream
|
if (stream == null)
|
||||||
{
|
{
|
||||||
Format = format,
|
return null;
|
||||||
Stream = _assembly.GetManifestResourceStream(resource)
|
}
|
||||||
|
|
||||||
|
return new ImageStream(stream)
|
||||||
|
{
|
||||||
|
Format = format
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InternalProfileInfo
|
private class InternalProfileInfo
|
||||||
{
|
{
|
||||||
internal DeviceProfileInfo Info { get; set; }
|
internal InternalProfileInfo(DeviceProfileInfo info, string path)
|
||||||
|
{
|
||||||
|
Info = info;
|
||||||
|
Path = path;
|
||||||
|
}
|
||||||
|
|
||||||
internal string Path { get; set; }
|
internal DeviceProfileInfo Info { get; }
|
||||||
|
|
||||||
|
internal string Path { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,9 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.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-->
|
||||||
|
@ -31,10 +29,6 @@
|
||||||
<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" />
|
||||||
|
@ -78,7 +72,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -8,8 +6,10 @@ namespace Emby.Dlna
|
||||||
{
|
{
|
||||||
public class EventSubscriptionResponse
|
public class EventSubscriptionResponse
|
||||||
{
|
{
|
||||||
public EventSubscriptionResponse()
|
public EventSubscriptionResponse(string content, string contentType)
|
||||||
{
|
{
|
||||||
|
Content = content;
|
||||||
|
ContentType = contentType;
|
||||||
Headers = new Dictionary<string, string>();
|
Headers = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ 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;
|
||||||
|
@ -25,8 +26,6 @@ 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;
|
||||||
|
@ -51,11 +50,7 @@ namespace Emby.Dlna.Eventing
|
||||||
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EventSubscriptionResponse
|
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||||
{
|
|
||||||
Content = string.Empty,
|
|
||||||
ContentType = "text/plain"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||||
|
@ -86,9 +81,7 @@ namespace Emby.Dlna.Eventing
|
||||||
if (!string.IsNullOrEmpty(header))
|
if (!string.IsNullOrEmpty(header))
|
||||||
{
|
{
|
||||||
// Starts with SECOND-
|
// Starts with SECOND-
|
||||||
header = header.Split('-')[^1];
|
if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||||
|
|
||||||
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
|
|
||||||
{
|
{
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
@ -103,23 +96,15 @@ namespace Emby.Dlna.Eventing
|
||||||
|
|
||||||
_subscriptions.TryRemove(subscriptionId, out _);
|
_subscriptions.TryRemove(subscriptionId, out _);
|
||||||
|
|
||||||
return new EventSubscriptionResponse
|
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||||
{
|
|
||||||
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
|
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||||
{
|
|
||||||
Content = string.Empty,
|
|
||||||
ContentType = "text/plain"
|
|
||||||
};
|
|
||||||
|
|
||||||
response.Headers["SID"] = subscriptionId;
|
response.Headers["SID"] = subscriptionId;
|
||||||
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
|
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -176,7 +161,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(_usCulture));
|
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
@ -27,11 +27,9 @@ 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
|
||||||
{
|
{
|
||||||
|
@ -54,7 +52,6 @@ namespace Emby.Dlna.Main
|
||||||
private readonly ISocketFactory _socketFactory;
|
private readonly ISocketFactory _socketFactory;
|
||||||
private readonly INetworkManager _networkManager;
|
private readonly INetworkManager _networkManager;
|
||||||
private readonly object _syncLock = new object();
|
private readonly object _syncLock = new object();
|
||||||
private readonly NetworkConfiguration _netConfig;
|
|
||||||
private readonly bool _disabled;
|
private readonly bool _disabled;
|
||||||
|
|
||||||
private PlayToManager _manager;
|
private PlayToManager _manager;
|
||||||
|
@ -127,8 +124,8 @@ namespace Emby.Dlna.Main
|
||||||
config);
|
config);
|
||||||
Current = this;
|
Current = this;
|
||||||
|
|
||||||
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
|
||||||
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
|
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
|
||||||
|
|
||||||
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
||||||
{
|
{
|
||||||
|
@ -204,8 +201,8 @@ namespace Emby.Dlna.Main
|
||||||
{
|
{
|
||||||
if (_communicationsServer == null)
|
if (_communicationsServer == null)
|
||||||
{
|
{
|
||||||
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
|
var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
|
||||||
OperatingSystem.Id == OperatingSystemId.Linux;
|
OperatingSystem.IsLinux();
|
||||||
|
|
||||||
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||||
{
|
{
|
||||||
|
@ -221,11 +218,6 @@ namespace Emby.Dlna.Main
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogMessage(string msg)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
|
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -268,9 +260,13 @@ namespace Emby.Dlna.Main
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
_publisher = new SsdpDevicePublisher(
|
||||||
|
_communicationsServer,
|
||||||
|
MediaBrowser.Common.System.OperatingSystem.Name,
|
||||||
|
Environment.OSVersion.VersionString,
|
||||||
|
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||||
{
|
{
|
||||||
LogFunction = LogMessage,
|
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
|
||||||
SupportPnpRootDevice = false
|
SupportPnpRootDevice = false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -315,15 +311,9 @@ namespace Emby.Dlna.Main
|
||||||
|
|
||||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||||
|
|
||||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
||||||
|
|
||||||
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
|
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri);
|
||||||
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
|
|
||||||
{
|
|
||||||
// DLNA will only work over http, so we must reset to http:// : {port}.
|
|
||||||
uri.Scheme = "http";
|
|
||||||
uri.Port = _netConfig.HttpServerPortNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
var device = new SsdpRootDevice
|
var device = new SsdpRootDevice
|
||||||
{
|
{
|
||||||
|
@ -409,7 +399,6 @@ namespace Emby.Dlna.Main
|
||||||
_imageProcessor,
|
_imageProcessor,
|
||||||
_deviceDiscovery,
|
_deviceDiscovery,
|
||||||
_httpClientFactory,
|
_httpClientFactory,
|
||||||
_config,
|
|
||||||
_userDataManager,
|
_userDataManager,
|
||||||
_localization,
|
_localization,
|
||||||
_mediaSourceManager,
|
_mediaSourceManager,
|
||||||
|
|
|
@ -20,8 +20,6 @@ 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;
|
||||||
|
@ -640,7 +638,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Volume = int.Parse(volumeValue, UsCulture);
|
Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
if (Volume > 0)
|
if (Volume > 0)
|
||||||
{
|
{
|
||||||
|
@ -842,7 +840,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, UsCulture);
|
Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -854,7 +852,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, UsCulture);
|
Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
|
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
|
||||||
|
@ -1194,8 +1192,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, UsCulture);
|
var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
|
var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
return new DeviceIcon
|
return new DeviceIcon
|
||||||
{
|
{
|
||||||
|
@ -1260,10 +1258,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
|
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
|
||||||
{
|
|
||||||
MediaInfo = mediaInfo
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackProgress(UBaseObject mediaInfo)
|
private void OnPlaybackProgress(UBaseObject mediaInfo)
|
||||||
|
@ -1273,27 +1268,17 @@ namespace Emby.Dlna.PlayTo
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
|
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo));
|
||||||
{
|
|
||||||
MediaInfo = mediaInfo
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackStop(UBaseObject mediaInfo)
|
private void OnPlaybackStop(UBaseObject mediaInfo)
|
||||||
{
|
{
|
||||||
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
|
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo));
|
||||||
{
|
|
||||||
MediaInfo = mediaInfo
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
|
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
|
||||||
{
|
{
|
||||||
MediaChanged?.Invoke(this, new MediaChangedEventArgs
|
MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia));
|
||||||
{
|
|
||||||
OldMediaInfo = old,
|
|
||||||
NewMediaInfo = newMedia
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
#nullable disable
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
|
@ -8,6 +6,12 @@ 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; }
|
||||||
|
|
|
@ -30,8 +30,6 @@ 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;
|
||||||
|
@ -716,7 +714,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, _usCulture, out var val))
|
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||||
{
|
{
|
||||||
return SetAudioStreamIndex(val);
|
return SetAudioStreamIndex(val);
|
||||||
}
|
}
|
||||||
|
@ -728,7 +726,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, _usCulture, out var val))
|
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||||
{
|
{
|
||||||
return SetSubtitleStreamIndex(val);
|
return SetSubtitleStreamIndex(val);
|
||||||
}
|
}
|
||||||
|
@ -740,7 +738,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, _usCulture, out var volume))
|
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
|
||||||
{
|
{
|
||||||
return _device.SetVolume(volume, cancellationToken);
|
return _device.SetVolume(volume, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
@ -35,7 +34,6 @@ namespace Emby.Dlna.PlayTo
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
private readonly IUserDataManager _userDataManager;
|
private readonly IUserDataManager _userDataManager;
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
|
|
||||||
|
@ -47,7 +45,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||||
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
|
@ -58,7 +56,6 @@ namespace Emby.Dlna.PlayTo
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
_deviceDiscovery = deviceDiscovery;
|
_deviceDiscovery = deviceDiscovery;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_config = config;
|
|
||||||
_userDataManager = userDataManager;
|
_userDataManager = userDataManager;
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
|
@ -173,7 +170,9 @@ namespace Emby.Dlna.PlayTo
|
||||||
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
|
var sessionInfo = await _sessionManager
|
||||||
|
.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();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -8,6 +6,11 @@ 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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -8,6 +6,11 @@ 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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -8,6 +6,11 @@ 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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo
|
||||||
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
|
private const string 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)
|
||||||
|
@ -45,10 +43,12 @@ 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.PreserveWhitespace,
|
LoadOptions.None,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,14 +78,15 @@ 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(_usCulture));
|
options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
|
||||||
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
|
options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
|
||||||
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
|
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
|
||||||
options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
|
options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
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)
|
||||||
|
@ -94,12 +95,13 @@ 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.PreserveWhitespace,
|
LoadOptions.None,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
|
@ -15,7 +15,6 @@ 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;
|
||||||
|
@ -193,10 +192,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(_usCulture)))
|
.Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
|
||||||
.Append("</width>");
|
.Append("</width>");
|
||||||
builder.Append("<height>")
|
builder.Append("<height>")
|
||||||
.Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
|
.Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
|
||||||
.Append("</height>");
|
.Append("</height>");
|
||||||
builder.Append("<depth>")
|
builder.Append("<depth>")
|
||||||
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
|
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
|
||||||
|
@ -250,8 +249,7 @@ namespace Emby.Dlna.Server
|
||||||
|
|
||||||
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
|
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
|
||||||
|
|
||||||
// TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
|
return SecurityElement.Escape(url);
|
||||||
return SecurityElement.Escape(url) ?? string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<DeviceIcon> GetIcons()
|
private IEnumerable<DeviceIcon> GetIcons()
|
||||||
|
|
|
@ -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
|
||||||
|
@ -64,7 +64,7 @@ namespace Emby.Dlna.Service
|
||||||
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
|
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
|
Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
|
||||||
|
|
||||||
var settings = new XmlWriterSettings
|
var settings = new XmlWriterSettings
|
||||||
{
|
{
|
||||||
|
@ -95,11 +95,7 @@ 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
|
var controlResponse = new ControlResponse(xml, true);
|
||||||
{
|
|
||||||
Xml = xml,
|
|
||||||
IsSuccessful = true
|
|
||||||
};
|
|
||||||
|
|
||||||
controlResponse.Headers.Add("EXT", string.Empty);
|
controlResponse.Headers.Add("EXT", string.Empty);
|
||||||
|
|
||||||
|
|
|
@ -23,14 +23,14 @@ namespace Emby.Dlna.Service
|
||||||
return EventManager.CancelEventSubscription(subscriptionId);
|
return EventManager.CancelEventSubscription(subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string timeoutString, string callbackUrl)
|
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||||
{
|
{
|
||||||
return EventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callbackUrl);
|
return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string timeoutString, string callbackUrl)
|
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||||
{
|
{
|
||||||
return EventManager.CreateEventSubscription(notificationType, timeoutString, callbackUrl);
|
return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,11 +46,7 @@ namespace Emby.Dlna.Service
|
||||||
writer.WriteEndDocument();
|
writer.WriteEndDocument();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ControlResponse
|
return new ControlResponse(builder.ToString(), false);
|
||||||
{
|
|
||||||
Xml = builder.ToString(),
|
|
||||||
IsSuccessful = false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,9 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -30,8 +28,4 @@
|
||||||
<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>
|
||||||
|
|
|
@ -26,7 +26,7 @@ namespace Emby.Drawing
|
||||||
public sealed class ImageProcessor : IImageProcessor, IDisposable
|
public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||||
{
|
{
|
||||||
// Increment this when there's a change requiring caches to be invalidated
|
// Increment this when there's a change requiring caches to be invalidated
|
||||||
private const string Version = "3";
|
private const char Version = '3';
|
||||||
|
|
||||||
private static readonly HashSet<string> _transparentImageTypes
|
private static readonly HashSet<string> _transparentImageTypes
|
||||||
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
|
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
|
||||||
|
@ -102,7 +102,7 @@ namespace Emby.Drawing
|
||||||
{
|
{
|
||||||
var file = await ProcessImage(options).ConfigureAwait(false);
|
var file = await ProcessImage(options).ConfigureAwait(false);
|
||||||
|
|
||||||
using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
|
using (var fileStream = AsyncFile.OpenRead(file.Item1))
|
||||||
{
|
{
|
||||||
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Common.Extensions;
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Audio
|
namespace Emby.Naming.Audio
|
||||||
{
|
{
|
||||||
|
|
|
@ -15,7 +15,7 @@ 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, List<AudioBookFileInfo> files, List<AudioBookFileInfo> extras, List<AudioBookFileInfo> alternateVersions)
|
public AudioBookInfo(string name, int? year, IReadOnlyList<AudioBookFileInfo> files, IReadOnlyList<AudioBookFileInfo> extras, IReadOnlyList<AudioBookFileInfo> alternateVersions)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Year = year;
|
Year = year;
|
||||||
|
@ -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 List<AudioBookFileInfo> Files { get; set; }
|
public IReadOnlyList<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 List<AudioBookFileInfo> Extras { get; set; }
|
public IReadOnlyList<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 List<AudioBookFileInfo> AlternateVersions { get; set; }
|
public IReadOnlyList<AudioBookFileInfo> AlternateVersions { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
|
||||||
public class AudioBookListResolver
|
public class AudioBookListResolver
|
||||||
{
|
{
|
||||||
private readonly NamingOptions _options;
|
private readonly NamingOptions _options;
|
||||||
|
private readonly AudioBookResolver _audioBookResolver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
|
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
|
||||||
|
@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
|
||||||
public AudioBookListResolver(NamingOptions options)
|
public AudioBookListResolver(NamingOptions options)
|
||||||
{
|
{
|
||||||
_options = options;
|
_options = options;
|
||||||
|
_audioBookResolver = new AudioBookResolver(_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -31,21 +33,19 @@ namespace Emby.Naming.AudioBook
|
||||||
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
|
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
|
||||||
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
|
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
|
||||||
{
|
{
|
||||||
var audioBookResolver = new AudioBookResolver(_options);
|
|
||||||
|
|
||||||
// File with empty fullname will be sorted out here.
|
// File with empty fullname will be sorted out here.
|
||||||
var audiobookFileInfos = files
|
var audiobookFileInfos = files
|
||||||
.Select(i => audioBookResolver.Resolve(i.FullName))
|
.Select(i => _audioBookResolver.Resolve(i.FullName))
|
||||||
.OfType<AudioBookFileInfo>()
|
.OfType<AudioBookFileInfo>()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var stackResult = new StackResolver(_options)
|
var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
|
||||||
.ResolveAudioBooks(audiobookFileInfos);
|
|
||||||
|
|
||||||
foreach (var stack in stackResult)
|
foreach (var stack in stackResult)
|
||||||
{
|
{
|
||||||
var stackFiles = stack.Files
|
var stackFiles = stack.Files
|
||||||
.Select(i => audioBookResolver.Resolve(i))
|
.Select(i => _audioBookResolver.Resolve(i))
|
||||||
.OfType<AudioBookFileInfo>()
|
.OfType<AudioBookFileInfo>()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
@ -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") ||
|
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
|
||||||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
|
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
|
||||||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
|
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|
|
@ -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.AudioBook
|
namespace Emby.Naming.AudioBook
|
||||||
{
|
{
|
||||||
|
@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
|
|
||||||
// Check supported extensions
|
// Check supported extensions
|
||||||
if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
#pragma warning disable CA1819
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Emby.Naming.Video;
|
using Emby.Naming.Video;
|
||||||
|
@ -122,11 +125,11 @@ namespace Emby.Naming.Common
|
||||||
token: "DSR")
|
token: "DSR")
|
||||||
};
|
};
|
||||||
|
|
||||||
VideoFileStackingExpressions = new[]
|
VideoFileStackingRules = new[]
|
||||||
{
|
{
|
||||||
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
|
||||||
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
|
||||||
"(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
|
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
|
||||||
};
|
};
|
||||||
|
|
||||||
CleanDateTimes = new[]
|
CleanDateTimes = new[]
|
||||||
|
@ -137,8 +140,11 @@ namespace Emby.Naming.Common
|
||||||
|
|
||||||
CleanStrings = new[]
|
CleanStrings = new[]
|
||||||
{
|
{
|
||||||
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
||||||
@"(\[.*\])"
|
@"^(?<cleaned>.+?)(\[.*\])",
|
||||||
|
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
||||||
|
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
||||||
|
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
|
||||||
};
|
};
|
||||||
|
|
||||||
SubtitleFileExtensions = new[]
|
SubtitleFileExtensions = new[]
|
||||||
|
@ -250,6 +256,8 @@ namespace Emby.Naming.Common
|
||||||
},
|
},
|
||||||
// <!-- foo.ep01, foo.EP_01 -->
|
// <!-- foo.ep01, foo.EP_01 -->
|
||||||
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
|
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
|
||||||
|
// <!-- foo.E01., foo.e01. -->
|
||||||
|
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
|
||||||
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
|
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
|
||||||
{
|
{
|
||||||
DateTimeFormats = new[]
|
DateTimeFormats = new[]
|
||||||
|
@ -277,14 +285,14 @@ 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
|
// Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
|
||||||
// [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
|
// [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]+).*$")
|
new EpisodeExpression(@".*[\\\/]?.*?(\[.*?\])+.*?(?<seriesname>[-\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
@ -305,6 +313,12 @@ namespace Emby.Naming.Common
|
||||||
|
|
||||||
// *** End Kodi Standard Naming
|
// *** End Kodi Standard Naming
|
||||||
|
|
||||||
|
// "Episode 16", "Episode 16 - Title"
|
||||||
|
new EpisodeExpression(@"[Ee]pisode (?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))?[^\\\/]*$")
|
||||||
|
{
|
||||||
|
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
|
||||||
|
@ -362,12 +376,20 @@ 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}))*[^\\\/]*$")
|
// Series and season only expression
|
||||||
|
// "the show/season 1", "the show/s01"
|
||||||
|
new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)\/[Ss](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
|
||||||
{
|
{
|
||||||
IsOptimistic = true,
|
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// Series and season only expression
|
||||||
|
// "the show S01", "the show season 1"
|
||||||
|
new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)[\. _\-]+[sS](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
|
||||||
|
{
|
||||||
|
IsNamed = true
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeWithoutSeasonExpressions = new[]
|
EpisodeWithoutSeasonExpressions = new[]
|
||||||
|
@ -382,6 +404,12 @@ namespace Emby.Naming.Common
|
||||||
|
|
||||||
VideoExtraRules = new[]
|
VideoExtraRules = new[]
|
||||||
{
|
{
|
||||||
|
new ExtraRule(
|
||||||
|
ExtraType.Trailer,
|
||||||
|
ExtraRuleType.DirectoryName,
|
||||||
|
"trailers",
|
||||||
|
MediaType.Video),
|
||||||
|
|
||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.Trailer,
|
ExtraType.Trailer,
|
||||||
ExtraRuleType.Filename,
|
ExtraRuleType.Filename,
|
||||||
|
@ -448,6 +476,12 @@ namespace Emby.Naming.Common
|
||||||
"theme",
|
"theme",
|
||||||
MediaType.Audio),
|
MediaType.Audio),
|
||||||
|
|
||||||
|
new ExtraRule(
|
||||||
|
ExtraType.ThemeSong,
|
||||||
|
ExtraRuleType.DirectoryName,
|
||||||
|
"theme-music",
|
||||||
|
MediaType.Audio),
|
||||||
|
|
||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.Scene,
|
ExtraType.Scene,
|
||||||
ExtraRuleType.Suffix,
|
ExtraRuleType.Suffix,
|
||||||
|
@ -478,6 +512,12 @@ 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,
|
||||||
|
@ -536,7 +576,7 @@ namespace Emby.Naming.Common
|
||||||
ExtraType.Unknown,
|
ExtraType.Unknown,
|
||||||
ExtraRuleType.DirectoryName,
|
ExtraRuleType.DirectoryName,
|
||||||
"extras",
|
"extras",
|
||||||
MediaType.Video),
|
MediaType.Video)
|
||||||
};
|
};
|
||||||
|
|
||||||
Format3DRules = new[]
|
Format3DRules = new[]
|
||||||
|
@ -648,9 +688,29 @@ namespace Emby.Naming.Common
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
|
AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["trailers"] = ExtraType.Trailer,
|
||||||
|
["theme-music"] = ExtraType.ThemeSong,
|
||||||
|
["backdrops"] = ExtraType.ThemeVideo,
|
||||||
|
["extras"] = ExtraType.Unknown,
|
||||||
|
["behind the scenes"] = ExtraType.BehindTheScenes,
|
||||||
|
["deleted scenes"] = ExtraType.DeletedScene,
|
||||||
|
["interviews"] = ExtraType.Interview,
|
||||||
|
["scenes"] = ExtraType.Scene,
|
||||||
|
["samples"] = ExtraType.Sample,
|
||||||
|
["shorts"] = ExtraType.Clip,
|
||||||
|
["featurettes"] = ExtraType.Clip
|
||||||
|
};
|
||||||
|
|
||||||
Compile();
|
Compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the folder name to extra types mapping.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets list of audio file extensions.
|
/// Gets or sets list of audio file extensions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -732,9 +792,9 @@ namespace Emby.Naming.Common
|
||||||
public Format3DRule[] Format3DRules { get; set; }
|
public Format3DRule[] Format3DRules { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets list of raw video file-stacking expressions strings.
|
/// Gets the file stacking rules.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] VideoFileStackingExpressions { get; set; }
|
public FileStackRule[] VideoFileStackingRules { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets list of raw clean DateTimes regular expressions strings.
|
/// Gets or sets list of raw clean DateTimes regular expressions strings.
|
||||||
|
@ -756,11 +816,6 @@ namespace Emby.Naming.Common
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ExtraRule[] VideoExtraRules { get; set; }
|
public ExtraRule[] VideoExtraRules { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets list of video file-stack regular expressions.
|
|
||||||
/// </summary>
|
|
||||||
public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets list of clean datetime regular expressions.
|
/// Gets list of clean datetime regular expressions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -786,7 +841,6 @@ namespace Emby.Naming.Common
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Compile()
|
public void Compile()
|
||||||
{
|
{
|
||||||
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
|
|
||||||
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
|
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
|
||||||
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
|
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
|
||||||
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
|
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -6,15 +6,13 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.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'">
|
||||||
|
@ -40,7 +38,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
|
@ -50,8 +48,4 @@
|
||||||
<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>
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Subtitles
|
namespace Emby.Naming.Subtitles
|
||||||
{
|
{
|
||||||
|
@ -34,7 +35,7 @@ namespace Emby.Naming.Subtitles
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -42,11 +43,11 @@ namespace Emby.Naming.Subtitles
|
||||||
var flags = GetFlags(path);
|
var flags = GetFlags(path);
|
||||||
var info = new SubtitleInfo(
|
var info = new SubtitleInfo(
|
||||||
path,
|
path,
|
||||||
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
|
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
|
||||||
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
|
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
|
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
|
||||||
&& !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
|
&& !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Should have a name, language and file extension
|
// Should have a name, language and file extension
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using Emby.Naming.Video;
|
using Emby.Naming.Video;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.TV
|
namespace Emby.Naming.TV
|
||||||
{
|
{
|
||||||
|
@ -48,7 +48,7 @@ namespace Emby.Naming.TV
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
// Check supported extensions
|
// Check supported extensions
|
||||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// It's not supported. Check stub extensions
|
// It's not supported. Check stub extensions
|
||||||
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
namespace Emby.Naming.TV
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Holder object for Series information.
|
||||||
|
/// </summary>
|
||||||
|
public class SeriesInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SeriesInfo"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path to the file.</param>
|
||||||
|
public SeriesInfo(string path)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The path.</value>
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the series.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The name of the series.</value>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
using Emby.Naming.Common;
|
||||||
|
|
||||||
|
namespace Emby.Naming.TV
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to parse information about series from paths containing more information that only the series name.
|
||||||
|
/// Uses the same regular expressions as the EpisodePathParser but have different success criteria.
|
||||||
|
/// </summary>
|
||||||
|
public static class SeriesPathParser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parses information about series from path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
|
||||||
|
/// <param name="path">Path.</param>
|
||||||
|
/// <returns>Returns <see cref="SeriesPathParserResult"/> object.</returns>
|
||||||
|
public static SeriesPathParserResult Parse(NamingOptions options, string path)
|
||||||
|
{
|
||||||
|
SeriesPathParserResult? result = null;
|
||||||
|
|
||||||
|
foreach (var expression in options.EpisodeExpressions)
|
||||||
|
{
|
||||||
|
var currentResult = Parse(path, expression);
|
||||||
|
if (currentResult.Success)
|
||||||
|
{
|
||||||
|
result = currentResult;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(result.SeriesName))
|
||||||
|
{
|
||||||
|
result.SeriesName = result.SeriesName.Trim(' ', '_', '.', '-');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result ?? new SeriesPathParserResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SeriesPathParserResult Parse(string name, EpisodeExpression expression)
|
||||||
|
{
|
||||||
|
var result = new SeriesPathParserResult();
|
||||||
|
|
||||||
|
var match = expression.Regex.Match(name);
|
||||||
|
|
||||||
|
if (match.Success && match.Groups.Count >= 3)
|
||||||
|
{
|
||||||
|
if (expression.IsNamed)
|
||||||
|
{
|
||||||
|
result.SeriesName = match.Groups["seriesname"].Value;
|
||||||
|
result.Success = !string.IsNullOrEmpty(result.SeriesName) && !string.IsNullOrEmpty(match.Groups["seasonnumber"]?.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
namespace Emby.Naming.TV
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Holder object for <see cref="SeriesPathParser"/> result.
|
||||||
|
/// </summary>
|
||||||
|
public class SeriesPathParserResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the series.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The name of the series.</value>
|
||||||
|
public string? SeriesName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether parsing was successful.
|
||||||
|
/// </summary>
|
||||||
|
public bool Success { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Emby.Naming.Common;
|
||||||
|
|
||||||
|
namespace Emby.Naming.TV
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to resolve information about series from path.
|
||||||
|
/// </summary>
|
||||||
|
public static class SeriesResolver
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Regex that matches strings of at least 2 characters separated by a dot or underscore.
|
||||||
|
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
|
||||||
|
/// preserving namings like "S.H.O.W".
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve information about series from path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"><see cref="NamingOptions"/> object passed to <see cref="SeriesPathParser"/>.</param>
|
||||||
|
/// <param name="path">Path to series.</param>
|
||||||
|
/// <returns>SeriesInfo.</returns>
|
||||||
|
public static SeriesInfo Resolve(NamingOptions options, string path)
|
||||||
|
{
|
||||||
|
string seriesName = Path.GetFileName(path);
|
||||||
|
|
||||||
|
SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(result.SeriesName))
|
||||||
|
{
|
||||||
|
seriesName = result.SeriesName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(seriesName))
|
||||||
|
{
|
||||||
|
seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SeriesInfo(path)
|
||||||
|
{
|
||||||
|
Name = seriesName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
@ -17,38 +16,39 @@ 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 ReadOnlySpan<char> newName)
|
public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out string newName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(name))
|
if (string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
newName = ReadOnlySpan<char>.Empty;
|
newName = string.Empty;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var len = expressions.Count;
|
// Iteratively apply the regexps to clean the string.
|
||||||
for (int i = 0; i < len; i++)
|
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))
|
||||||
{
|
{
|
||||||
return true;
|
cleaned = true;
|
||||||
|
name = newName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newName = ReadOnlySpan<char>.Empty;
|
newName = cleaned ? name : string.Empty;
|
||||||
return false;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
|
private static bool TryClean(string name, Regex expression, out string newName)
|
||||||
{
|
{
|
||||||
var match = expression.Match(name);
|
var match = expression.Match(name);
|
||||||
int index = match.Index;
|
if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
|
||||||
if (match.Success && index != 0)
|
|
||||||
{
|
{
|
||||||
newName = name.AsSpan().Slice(0, match.Index);
|
newName = cleaned.Value;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
newName = ReadOnlySpan<char>.Empty;
|
newName = string.Empty;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
@ -10,45 +11,28 @@ namespace Emby.Naming.Video
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve if file is extra for video.
|
/// Resolve if file is extra for video.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ExtraResolver
|
public static class ExtraResolver
|
||||||
{
|
{
|
||||||
private readonly NamingOptions _options;
|
private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ExtraResolver"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
|
|
||||||
public ExtraResolver(NamingOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to resolve if file is extra.
|
/// Attempts to resolve if file is extra.
|
||||||
/// </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="ExtraResult"/> object.</returns>
|
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||||
public ExtraResult GetExtraInfo(string path)
|
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
var result = new ExtraResult();
|
var result = new ExtraResult();
|
||||||
|
|
||||||
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
|
for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
|
||||||
{
|
{
|
||||||
var rule = _options.VideoExtraRules[i];
|
var rule = namingOptions.VideoExtraRules[i];
|
||||||
if (rule.MediaType == MediaType.Audio)
|
if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|
||||||
{
|
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
|
||||||
if (!AudioFileParser.IsAudioFile(path, _options))
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (rule.MediaType == MediaType.Video)
|
|
||||||
{
|
|
||||||
if (!VideoResolver.IsVideoFile(path, _options))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pathSpan = path.AsSpan();
|
var pathSpan = path.AsSpan();
|
||||||
if (rule.RuleType == ExtraRuleType.Filename)
|
if (rule.RuleType == ExtraRuleType.Filename)
|
||||||
|
@ -63,9 +47,10 @@ namespace Emby.Naming.Video
|
||||||
}
|
}
|
||||||
else if (rule.RuleType == ExtraRuleType.Suffix)
|
else if (rule.RuleType == ExtraRuleType.Suffix)
|
||||||
{
|
{
|
||||||
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
// 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.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
|
if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
result.ExtraType = rule.ExtraType;
|
result.ExtraType = rule.ExtraType;
|
||||||
result.Rule = rule;
|
result.Rule = rule;
|
||||||
|
@ -75,9 +60,9 @@ namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
var filename = Path.GetFileName(path);
|
var filename = Path.GetFileName(path);
|
||||||
|
|
||||||
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
|
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
if (regex.IsMatch(filename))
|
if (isMatch)
|
||||||
{
|
{
|
||||||
result.ExtraType = rule.ExtraType;
|
result.ExtraType = rule.ExtraType;
|
||||||
result.Rule = rule;
|
result.Rule = rule;
|
||||||
|
@ -101,5 +86,66 @@ namespace Emby.Naming.Video
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds extras matching the video info.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="files">The list of file video infos.</param>
|
||||||
|
/// <param name="videoInfo">The video to compare against.</param>
|
||||||
|
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
||||||
|
/// <returns>A list of video extras for [videoInfo].</returns>
|
||||||
|
public static IReadOnlyList<VideoFileInfo> GetExtras(IReadOnlyList<VideoInfo> files, VideoFileInfo videoInfo, ReadOnlySpan<char> videoFlagDelimiters)
|
||||||
|
{
|
||||||
|
var parentDir = videoInfo.IsDirectory ? videoInfo.Path : Path.GetDirectoryName(videoInfo.Path.AsSpan());
|
||||||
|
|
||||||
|
var trimmedFileNameWithoutExtension = TrimFilenameDelimiters(videoInfo.FileNameWithoutExtension, videoFlagDelimiters);
|
||||||
|
var trimmedVideoInfoName = TrimFilenameDelimiters(videoInfo.Name, videoFlagDelimiters);
|
||||||
|
|
||||||
|
var result = new List<VideoFileInfo>();
|
||||||
|
for (var pos = files.Count - 1; pos >= 0; pos--)
|
||||||
|
{
|
||||||
|
var current = files[pos];
|
||||||
|
// ignore non-extras and multi-file (can this happen?)
|
||||||
|
if (current.ExtraType == null || current.Files.Count > 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentFile = current.Files[0];
|
||||||
|
var trimmedCurrentFileName = TrimFilenameDelimiters(currentFile.Name, videoFlagDelimiters);
|
||||||
|
|
||||||
|
// first check filenames
|
||||||
|
bool isValid = StartsWith(trimmedCurrentFileName, trimmedFileNameWithoutExtension)
|
||||||
|
|| (StartsWith(trimmedCurrentFileName, trimmedVideoInfoName) && currentFile.Year == videoInfo.Year);
|
||||||
|
|
||||||
|
// then by directory
|
||||||
|
if (!isValid)
|
||||||
|
{
|
||||||
|
// When the extra rule type is DirectoryName we must go one level higher to get the "real" dir name
|
||||||
|
var currentParentDir = currentFile.ExtraRule?.RuleType == ExtraRuleType.DirectoryName
|
||||||
|
? Path.GetDirectoryName(Path.GetDirectoryName(currentFile.Path.AsSpan()))
|
||||||
|
: Path.GetDirectoryName(currentFile.Path.AsSpan());
|
||||||
|
|
||||||
|
isValid = !currentParentDir.IsEmpty && !parentDir.IsEmpty && currentParentDir.Equals(parentDir, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid)
|
||||||
|
{
|
||||||
|
result.Add(currentFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.OrderBy(r => r.Path).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReadOnlySpan<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)
|
||||||
|
{
|
||||||
|
return !baseName.IsEmpty && fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
|
@ -12,25 +12,30 @@ namespace Emby.Naming.Video
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="FileStack"/> class.
|
/// Initializes a new instance of the <see cref="FileStack"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public FileStack()
|
/// <param name="name">The stack name.</param>
|
||||||
|
/// <param name="isDirectory">Whether the stack files are directories.</param>
|
||||||
|
/// <param name="files">The stack files.</param>
|
||||||
|
public FileStack(string name, bool isDirectory, IReadOnlyList<string> files)
|
||||||
{
|
{
|
||||||
Files = new List<string>();
|
Name = name;
|
||||||
|
IsDirectoryStack = isDirectory;
|
||||||
|
Files = files;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets name of file stack.
|
/// Gets the name of file stack.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets list of paths in stack.
|
/// Gets the list of paths in stack.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string> Files { get; set; }
|
public IReadOnlyList<string> Files { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether stack is directory stack.
|
/// Gets a value indicating whether stack is directory stack.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsDirectoryStack { get; set; }
|
public bool IsDirectoryStack { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper function to determine if path is in the stack.
|
/// Helper function to determine if path is in the stack.
|
||||||
|
@ -40,12 +45,12 @@ namespace Emby.Naming.Video
|
||||||
/// <returns>True if file is in the stack.</returns>
|
/// <returns>True if file is in the stack.</returns>
|
||||||
public bool ContainsFile(string file, bool isDirectory)
|
public bool ContainsFile(string file, bool isDirectory)
|
||||||
{
|
{
|
||||||
if (IsDirectoryStack == isDirectory)
|
if (string.IsNullOrEmpty(file))
|
||||||
{
|
{
|
||||||
return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Emby.Naming.Video;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regex based rule for file stacking (eg. disc1, disc2).
|
||||||
|
/// </summary>
|
||||||
|
public class FileStackRule
|
||||||
|
{
|
||||||
|
private readonly Regex _tokenRegex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FileStackRule"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">Token.</param>
|
||||||
|
/// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
|
||||||
|
public FileStackRule(string token, bool isNumerical)
|
||||||
|
{
|
||||||
|
_tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
|
||||||
|
IsNumerical = isNumerical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsNumerical { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Match the input against the rule regex.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The input.</param>
|
||||||
|
/// <param name="result">The part type and number or <c>null</c>.</param>
|
||||||
|
/// <returns>A value indicating whether the input matched the rule.</returns>
|
||||||
|
public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
var match = _tokenRegex.Match(input);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown";
|
||||||
|
result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Emby.Naming.AudioBook;
|
using Emby.Naming.AudioBook;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
|
@ -12,37 +11,28 @@ namespace Emby.Naming.Video
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve <see cref="FileStack"/> from list of paths.
|
/// Resolve <see cref="FileStack"/> from list of paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class StackResolver
|
public static class StackResolver
|
||||||
{
|
{
|
||||||
private readonly NamingOptions _options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="StackResolver"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
|
|
||||||
public StackResolver(NamingOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves only directories from paths.
|
/// Resolves only directories from paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of paths.</param>
|
/// <param name="files">List of paths.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
|
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
|
||||||
public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
|
public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
|
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves only files from paths.
|
/// Resolves only files from paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of paths.</param>
|
/// <param name="files">List of paths.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
|
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
|
||||||
public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
|
public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
|
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -50,7 +40,7 @@ namespace Emby.Naming.Video
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of paths.</param>
|
/// <param name="files">List of paths.</param>
|
||||||
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
|
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
|
||||||
public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
|
public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
|
||||||
{
|
{
|
||||||
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
|
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
|
||||||
|
|
||||||
|
@ -60,19 +50,13 @@ namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
foreach (var file in directory)
|
foreach (var file in directory)
|
||||||
{
|
{
|
||||||
var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
|
var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
|
||||||
stack.Files.Add(file.Path);
|
|
||||||
yield return stack;
|
yield return stack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
|
var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
|
||||||
foreach (var file in directory)
|
|
||||||
{
|
|
||||||
stack.Files.Add(file.Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return stack;
|
yield return stack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,158 +66,91 @@ namespace Emby.Naming.Video
|
||||||
/// Resolves videos from paths.
|
/// Resolves videos from paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of paths.</param>
|
/// <param name="files">List of paths.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
||||||
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
|
public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
var list = files
|
var potentialFiles = files
|
||||||
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
|
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
|
||||||
.OrderBy(i => i.FullName)
|
.OrderBy(i => i.FullName);
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var expressions = _options.VideoFileStackingRegexes;
|
var potentialStacks = new Dictionary<string, StackMetadata>();
|
||||||
|
foreach (var file in potentialFiles)
|
||||||
for (var i = 0; i < list.Count; i++)
|
|
||||||
{
|
{
|
||||||
var offset = 0;
|
var name = file.Name;
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
var file1 = list[i];
|
|
||||||
|
|
||||||
var expressionIndex = 0;
|
|
||||||
while (expressionIndex < expressions.Length)
|
|
||||||
{
|
{
|
||||||
var exp = expressions[expressionIndex];
|
name = Path.GetFileName(file.FullName);
|
||||||
var stack = new FileStack();
|
}
|
||||||
|
|
||||||
// (Title)(Volume)(Ignore)(Extension)
|
for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
|
||||||
var match1 = FindMatch(file1, exp, offset);
|
|
||||||
|
|
||||||
if (match1.Success)
|
|
||||||
{
|
{
|
||||||
var title1 = match1.Groups["title"].Value;
|
var rule = namingOptions.VideoFileStackingRules[i];
|
||||||
var volume1 = match1.Groups["volume"].Value;
|
if (!rule.Match(name, out var stackParsingResult))
|
||||||
var ignore1 = match1.Groups["ignore"].Value;
|
|
||||||
var extension1 = match1.Groups["extension"].Value;
|
|
||||||
|
|
||||||
var j = i + 1;
|
|
||||||
while (j < list.Count)
|
|
||||||
{
|
{
|
||||||
var file2 = list[j];
|
|
||||||
|
|
||||||
if (file1.IsDirectory != file2.IsDirectory)
|
|
||||||
{
|
|
||||||
j++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// (Title)(Volume)(Ignore)(Extension)
|
var stackName = stackParsingResult.Value.StackName;
|
||||||
var match2 = FindMatch(file2, exp, offset);
|
var partNumber = stackParsingResult.Value.PartNumber;
|
||||||
|
var partType = stackParsingResult.Value.PartType;
|
||||||
|
|
||||||
if (match2.Success)
|
if (!potentialStacks.TryGetValue(stackName, out var stackResult))
|
||||||
{
|
{
|
||||||
var title2 = match2.Groups[1].Value;
|
stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
|
||||||
var volume2 = match2.Groups[2].Value;
|
potentialStacks[stackName] = stackResult;
|
||||||
var ignore2 = match2.Groups[3].Value;
|
|
||||||
var extension2 = match2.Groups[4].Value;
|
|
||||||
|
|
||||||
if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (stack.Files.Count == 0)
|
|
||||||
{
|
|
||||||
stack.Name = title1 + ignore1;
|
|
||||||
stack.IsDirectoryStack = file1.IsDirectory;
|
|
||||||
stack.Files.Add(file1.FullName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.Files.Add(file2.FullName);
|
if (stackResult.Parts.Count > 0)
|
||||||
}
|
{
|
||||||
else
|
if (stackResult.IsDirectory != file.IsDirectory
|
||||||
|
|| !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| stackResult.ContainsPart(partNumber))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.IsNumerical != stackResult.IsNumerical)
|
||||||
{
|
{
|
||||||
// Sequel
|
|
||||||
offset = 0;
|
|
||||||
expressionIndex++;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
stackResult.Parts.Add(partNumber, file);
|
||||||
// False positive, try again with offset
|
|
||||||
offset = match1.Groups[3].Index;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Extension mismatch
|
|
||||||
offset = 0;
|
|
||||||
expressionIndex++;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
foreach (var (fileName, stack) in potentialStacks)
|
||||||
{
|
{
|
||||||
// Title mismatch
|
if (stack.Parts.Count < 2)
|
||||||
offset = 0;
|
|
||||||
expressionIndex++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// No match 2, next expression
|
continue;
|
||||||
offset = 0;
|
|
||||||
expressionIndex++;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
j++;
|
yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
|
||||||
}
|
|
||||||
|
|
||||||
if (j == list.Count)
|
|
||||||
{
|
|
||||||
expressionIndex = expressions.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No match 1
|
|
||||||
offset = 0;
|
|
||||||
expressionIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stack.Files.Count > 1)
|
|
||||||
{
|
|
||||||
yield return stack;
|
|
||||||
i += stack.Files.Count - 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetRegexInput(FileSystemMetadata file)
|
private class StackMetadata
|
||||||
{
|
{
|
||||||
// For directories, dummy up an extension otherwise the expressions will fail
|
public StackMetadata(bool isDirectory, bool isNumerical, string partType)
|
||||||
var input = !file.IsDirectory
|
{
|
||||||
? file.FullName
|
Parts = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
|
||||||
: file.FullName + ".mkv";
|
IsDirectory = isDirectory;
|
||||||
|
IsNumerical = isNumerical;
|
||||||
return Path.GetFileName(input);
|
PartType = partType;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
|
public Dictionary<string, FileSystemMetadata> Parts { get; }
|
||||||
{
|
|
||||||
var regexInput = GetRegexInput(input);
|
|
||||||
|
|
||||||
if (offset < 0 || offset >= regexInput.Length)
|
public bool IsDirectory { get; }
|
||||||
{
|
|
||||||
return Match.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return regex.Match(regexInput, offset);
|
public bool IsNumerical { get; }
|
||||||
|
|
||||||
|
public string PartType { get; }
|
||||||
|
|
||||||
|
public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
|
@ -28,7 +28,7 @@ namespace Emby.Naming.Video
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
|
|
||||||
if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
|
@ -17,7 +18,6 @@ namespace Emby.Naming.Video
|
||||||
Name = name;
|
Name = name;
|
||||||
|
|
||||||
Files = Array.Empty<VideoFileInfo>();
|
Files = Array.Empty<VideoFileInfo>();
|
||||||
Extras = Array.Empty<VideoFileInfo>();
|
|
||||||
AlternateVersions = Array.Empty<VideoFileInfo>();
|
AlternateVersions = Array.Empty<VideoFileInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,16 +39,15 @@ namespace Emby.Naming.Video
|
||||||
/// <value>The files.</value>
|
/// <value>The files.</value>
|
||||||
public IReadOnlyList<VideoFileInfo> Files { get; set; }
|
public IReadOnlyList<VideoFileInfo> Files { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the extras.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The extras.</value>
|
|
||||||
public IReadOnlyList<VideoFileInfo> 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<VideoFileInfo> AlternateVersions { get; set; }
|
public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the extra type.
|
||||||
|
/// </summary>
|
||||||
|
public ExtraType? ExtraType { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
|
@ -17,29 +16,38 @@ 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>
|
||||||
/// <param name="files">List of related video files.</param>
|
/// <param name="videoInfos">List of related video files.</param>
|
||||||
/// <param name="namingOptions">The naming options.</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>
|
||||||
|
/// <param name="parseName">Whether to parse the name or use the filename.</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(List<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
|
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
|
||||||
{
|
{
|
||||||
var videoInfos = files
|
|
||||||
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
|
|
||||||
.OfType<VideoFileInfo>()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// Filter out all extras, otherwise they could cause stacks to not be resolved
|
// Filter out all extras, otherwise they could cause stacks to not be resolved
|
||||||
// See the unit test TestStackedWithTrailer
|
// See the unit test TestStackedWithTrailer
|
||||||
var nonExtras = videoInfos
|
var nonExtras = videoInfos
|
||||||
.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 = StackResolver.Resolve(nonExtras, namingOptions).ToList();
|
||||||
.Resolve(nonExtras).ToList();
|
|
||||||
|
|
||||||
var remainingFiles = videoInfos
|
var remainingFiles = new List<VideoFileInfo>();
|
||||||
.Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
|
var standaloneMedia = new List<VideoFileInfo>();
|
||||||
.ToList();
|
|
||||||
|
for (var i = 0; i < videoInfos.Count; i++)
|
||||||
|
{
|
||||||
|
var current = videoInfos[i];
|
||||||
|
if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingFiles.Add(current);
|
||||||
|
if (current.ExtraType == null)
|
||||||
|
{
|
||||||
|
standaloneMedia.Add(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var list = new List<VideoInfo>();
|
var list = new List<VideoInfo>();
|
||||||
|
|
||||||
|
@ -47,27 +55,15 @@ 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, namingOptions, parseName))
|
||||||
.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);
|
|
||||||
|
|
||||||
if (extras.Count > 0)
|
|
||||||
{
|
|
||||||
info.Extras = extras;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Add(info);
|
list.Add(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
var standaloneMedia = remainingFiles
|
|
||||||
.Where(i => i.ExtraType == null)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
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[] { media } };
|
||||||
|
@ -75,10 +71,6 @@ namespace Emby.Naming.Video
|
||||||
info.Year = info.Files[0].Year;
|
info.Year = info.Files[0].Year;
|
||||||
|
|
||||||
remainingFiles.Remove(media);
|
remainingFiles.Remove(media);
|
||||||
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
|
|
||||||
|
|
||||||
info.Extras = extras;
|
|
||||||
|
|
||||||
list.Add(info);
|
list.Add(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,58 +79,12 @@ namespace Emby.Naming.Video
|
||||||
list = GetVideosGroupedByVersion(list, namingOptions);
|
list = GetVideosGroupedByVersion(list, namingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's only one resolved video, use the folder name as well to find extras
|
|
||||||
if (list.Count == 1)
|
|
||||||
{
|
|
||||||
var info = list[0];
|
|
||||||
var videoPath = list[0].Files[0].Path;
|
|
||||||
var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
|
|
||||||
|
|
||||||
if (!parentPath.IsEmpty)
|
|
||||||
{
|
|
||||||
var folderName = Path.GetFileName(parentPath);
|
|
||||||
if (!folderName.IsEmpty)
|
|
||||||
{
|
|
||||||
var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
|
|
||||||
extras.AddRange(info.Extras);
|
|
||||||
info.Extras = extras;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the extras that are just based on file name as well
|
|
||||||
var extrasByFileName = remainingFiles
|
|
||||||
.Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
remainingFiles = remainingFiles
|
|
||||||
.Except(extrasByFileName)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
extrasByFileName.AddRange(info.Extras);
|
|
||||||
info.Extras = extrasByFileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's only one video, accept all trailers
|
|
||||||
// Be lenient because people use all kinds of mishmash conventions with trailers.
|
|
||||||
if (list.Count == 1)
|
|
||||||
{
|
|
||||||
var trailers = remainingFiles
|
|
||||||
.Where(i => i.ExtraType == ExtraType.Trailer)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
trailers.AddRange(list[0].Extras);
|
|
||||||
list[0].Extras = trailers;
|
|
||||||
|
|
||||||
remainingFiles = remainingFiles
|
|
||||||
.Except(trailers)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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[] { i },
|
||||||
Year = i.Year
|
Year = i.Year,
|
||||||
|
ExtraType = i.ExtraType
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
|
@ -162,6 +108,11 @@ namespace Emby.Naming.Video
|
||||||
for (var i = 0; i < videos.Count; i++)
|
for (var i = 0; i < videos.Count; i++)
|
||||||
{
|
{
|
||||||
var video = videos[i];
|
var video = videos[i];
|
||||||
|
if (video.ExtraType != null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
|
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
|
||||||
{
|
{
|
||||||
return videos;
|
return videos;
|
||||||
|
@ -178,17 +129,14 @@ namespace Emby.Naming.Video
|
||||||
|
|
||||||
var alternateVersionsLen = videos.Count - 1;
|
var alternateVersionsLen = videos.Count - 1;
|
||||||
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
||||||
var extras = new List<VideoFileInfo>(list[0].Extras);
|
|
||||||
for (int i = 0; i < alternateVersionsLen; i++)
|
for (int i = 0; i < alternateVersionsLen; i++)
|
||||||
{
|
{
|
||||||
var video = videos[i + 1];
|
var video = videos[i + 1];
|
||||||
alternateVersions[i] = video.Files[0];
|
alternateVersions[i] = video.Files[0];
|
||||||
extras.AddRange(video.Extras);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
list[0].AlternateVersions = alternateVersions;
|
list[0].AlternateVersions = alternateVersions;
|
||||||
list[0].Name = folderName.ToString();
|
list[0].Name = folderName.ToString();
|
||||||
list[0].Extras = extras;
|
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
@ -230,7 +178,7 @@ namespace Emby.Naming.Video
|
||||||
var tmpTestFilename = testFilename.ToString();
|
var tmpTestFilename = testFilename.ToString();
|
||||||
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
|
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
|
||||||
{
|
{
|
||||||
tmpTestFilename = cleanName.Trim().ToString();
|
tmpTestFilename = cleanName.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The CleanStringParser should have removed common keywords etc.
|
// The CleanStringParser should have removed common keywords etc.
|
||||||
|
@ -238,67 +186,5 @@ namespace Emby.Naming.Video
|
||||||
|| testFilename[0] == '-'
|
|| testFilename[0] == '-'
|
||||||
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
|| 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;
|
|
||||||
if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
|
|
||||||
|| StartsWith(filename, secondBaseName, trimmedSecondBaseName))
|
|
||||||
{
|
|
||||||
result.Add(file);
|
|
||||||
remainingFiles.RemoveAt(pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Common.Extensions;
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
|
@ -16,10 +16,11 @@ namespace Emby.Naming.Video
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
|
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
|
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
|
||||||
{
|
{
|
||||||
return Resolve(path, true, namingOptions);
|
return Resolve(path, true, namingOptions, parseName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -74,7 +75,7 @@ namespace Emby.Naming.Video
|
||||||
|
|
||||||
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
||||||
|
|
||||||
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
|
var extraResult = ExtraResolver.GetExtraInfo(path, namingOptions);
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(path);
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
@ -87,9 +88,9 @@ namespace Emby.Naming.Video
|
||||||
year = cleanDateTimeResult.Year;
|
year = cleanDateTimeResult.Year;
|
||||||
|
|
||||||
if (extraResult.ExtraType == null
|
if (extraResult.ExtraType == null
|
||||||
&& TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
|
&& TryCleanString(name, namingOptions, out var newName))
|
||||||
{
|
{
|
||||||
name = newName.ToString();
|
name = newName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +139,7 @@ namespace Emby.Naming.Video
|
||||||
/// <param name="namingOptions">The naming options.</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 ReadOnlySpan<char> newName)
|
public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out string newName)
|
||||||
{
|
{
|
||||||
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
|
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,9 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
@ -77,7 +78,6 @@ 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;
|
||||||
|
@ -105,7 +105,7 @@ namespace Emby.Notifications
|
||||||
|
|
||||||
var type = entry.Type;
|
var type = entry.Type;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
|
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -132,25 +132,6 @@ 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))
|
||||||
|
@ -325,7 +306,6 @@ 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,13 +19,9 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
@ -60,7 +61,7 @@ namespace Emby.Photos
|
||||||
item.SetImagePath(ImageType.Primary, item.Path);
|
item.SetImagePath(ImageType.Primary, item.Path);
|
||||||
|
|
||||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
|
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
{
|
{
|
||||||
return _configurations.GetOrAdd(
|
return _configurations.GetOrAdd(
|
||||||
key,
|
key,
|
||||||
(k, configurationManager) =>
|
static (k, configurationManager) =>
|
||||||
{
|
{
|
||||||
var file = configurationManager.GetConfigurationFile(k);
|
var file = configurationManager.GetConfigurationFile(k);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.AppBase
|
namespace Emby.Server.Implementations.AppBase
|
||||||
|
@ -41,20 +40,19 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
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
|
||||||
byte[] newBytes = stream.GetBuffer();
|
Span<byte> newBytes = stream.GetBuffer().AsSpan(0, (int)stream.Length);
|
||||||
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.AsSpan(0, newBytesLen).SequenceEqual(buffer))
|
if (buffer == null || !newBytes.SequenceEqual(buffer))
|
||||||
{
|
{
|
||||||
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
|
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, 0, newBytesLen);
|
fs.Write(newBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,11 +1,8 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using SharpCompress.Archives.SevenZip;
|
|
||||||
using SharpCompress.Archives.Tar;
|
|
||||||
using SharpCompress.Common;
|
using SharpCompress.Common;
|
||||||
using SharpCompress.Readers;
|
using SharpCompress.Readers;
|
||||||
using SharpCompress.Readers.GZip;
|
using SharpCompress.Readers.GZip;
|
||||||
using SharpCompress.Readers.Zip;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Archiving
|
namespace Emby.Server.Implementations.Archiving
|
||||||
{
|
{
|
||||||
|
@ -14,53 +11,6 @@ namespace Emby.Server.Implementations.Archiving
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ZipClient : IZipClient
|
public class ZipClient : IZipClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Extracts all.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sourceFile">The source file.</param>
|
|
||||||
/// <param name="targetPath">The target path.</param>
|
|
||||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
|
||||||
public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
|
|
||||||
{
|
|
||||||
using var fileStream = File.OpenRead(sourceFile);
|
|
||||||
ExtractAll(fileStream, targetPath, overwriteExistingFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extracts all.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">The source.</param>
|
|
||||||
/// <param name="targetPath">The target path.</param>
|
|
||||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
|
||||||
public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
|
|
||||||
{
|
|
||||||
using var reader = ReaderFactory.Open(source);
|
|
||||||
var options = new ExtractionOptions
|
|
||||||
{
|
|
||||||
ExtractFullPath = true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (overwriteExistingFiles)
|
|
||||||
{
|
|
||||||
options.Overwrite = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.WriteAllToDirectory(targetPath, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
|
|
||||||
{
|
|
||||||
using var reader = ZipReader.Open(source);
|
|
||||||
var options = new ExtractionOptions
|
|
||||||
{
|
|
||||||
ExtractFullPath = true,
|
|
||||||
Overwrite = overwriteExistingFiles
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.WriteAllToDirectory(targetPath, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
|
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||||
{
|
{
|
||||||
|
@ -71,6 +21,7 @@ namespace Emby.Server.Implementations.Archiving
|
||||||
Overwrite = overwriteExistingFiles
|
Overwrite = overwriteExistingFiles
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Directory.CreateDirectory(targetPath);
|
||||||
reader.WriteAllToDirectory(targetPath, options);
|
reader.WriteAllToDirectory(targetPath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,67 +42,5 @@ namespace Emby.Server.Implementations.Archiving
|
||||||
reader.WriteEntryToFile(Path.Combine(targetPath, filename));
|
reader.WriteEntryToFile(Path.Combine(targetPath, filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extracts all from7z.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sourceFile">The source file.</param>
|
|
||||||
/// <param name="targetPath">The target path.</param>
|
|
||||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
|
||||||
public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
|
|
||||||
{
|
|
||||||
using var fileStream = File.OpenRead(sourceFile);
|
|
||||||
ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extracts all from7z.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">The source.</param>
|
|
||||||
/// <param name="targetPath">The target path.</param>
|
|
||||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
|
||||||
public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
|
|
||||||
{
|
|
||||||
using var archive = SevenZipArchive.Open(source);
|
|
||||||
using var reader = archive.ExtractAllEntries();
|
|
||||||
var options = new ExtractionOptions
|
|
||||||
{
|
|
||||||
ExtractFullPath = true,
|
|
||||||
Overwrite = overwriteExistingFiles
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.WriteAllToDirectory(targetPath, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extracts all from tar.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sourceFile">The source file.</param>
|
|
||||||
/// <param name="targetPath">The target path.</param>
|
|
||||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
|
||||||
public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
|
|
||||||
{
|
|
||||||
using var fileStream = File.OpenRead(sourceFile);
|
|
||||||
ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extracts all from tar.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">The source.</param>
|
|
||||||
/// <param name="targetPath">The target path.</param>
|
|
||||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
|
||||||
public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
|
|
||||||
{
|
|
||||||
using var archive = TarArchive.Open(source);
|
|
||||||
using var reader = archive.ExtractAllEntries();
|
|
||||||
var options = new ExtractionOptions
|
|
||||||
{
|
|
||||||
ExtractFullPath = true,
|
|
||||||
Overwrite = overwriteExistingFiles
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.WriteAllToDirectory(targetPath, options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,9 @@ 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;
|
||||||
|
using Jellyfin.Extensions.Json;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Json;
|
|
||||||
using MediaBrowser.Common.Progress;
|
using MediaBrowser.Common.Progress;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
@ -102,7 +103,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 IDisableMediaSourceDisplay);
|
return channel is not IDisableMediaSourceDisplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -179,7 +180,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
|
return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
|
||||||
&& hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
|
&& hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
@ -541,7 +542,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
return _libraryManager.GetItemIds(
|
return _libraryManager.GetItemIds(
|
||||||
new InternalItemsQuery
|
new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(Channel) },
|
IncludeItemTypes = new[] { BaseItemKind.Channel },
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
|
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
|
||||||
}).Select(i => GetChannelFeatures(i)).ToArray();
|
}).Select(i => GetChannelFeatures(i)).ToArray();
|
||||||
}
|
}
|
||||||
|
@ -586,7 +587,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
{
|
{
|
||||||
var supportsLatest = provider is ISupportsLatestMedia;
|
var supportsLatest = provider is ISupportsLatestMedia;
|
||||||
|
|
||||||
return new ChannelFeatures
|
return new ChannelFeatures(channel.Name, channel.Id)
|
||||||
{
|
{
|
||||||
CanFilter = !features.MaxPageSize.HasValue,
|
CanFilter = !features.MaxPageSize.HasValue,
|
||||||
CanSearch = provider is ISearchableChannel,
|
CanSearch = provider is ISearchableChannel,
|
||||||
|
@ -596,8 +597,6 @@ 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
|
||||||
};
|
};
|
||||||
|
@ -815,7 +814,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 = File.OpenRead(cachePath);
|
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
|
||||||
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||||
if (cachedResult != null)
|
if (cachedResult != null)
|
||||||
{
|
{
|
||||||
|
@ -838,7 +837,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 = File.OpenRead(cachePath);
|
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
|
||||||
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||||
if (cachedResult != null)
|
if (cachedResult != null)
|
||||||
{
|
{
|
||||||
|
@ -880,7 +879,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CacheResponse(object result, string path)
|
private async Task CacheResponse(ChannelItemResult result, string path)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -1077,14 +1076,6 @@ namespace Emby.Server.Implementations.Channels
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// was used for status
|
|
||||||
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
|
|
||||||
//{
|
|
||||||
// item.ExternalEtag = info.Etag;
|
|
||||||
// forceUpdate = true;
|
|
||||||
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
|
|
||||||
//}
|
|
||||||
|
|
||||||
if (!internalChannelId.Equals(item.ChannelId))
|
if (!internalChannelId.Equals(item.ChannelId))
|
||||||
{
|
{
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
|
@ -1145,7 +1136,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
|
|
||||||
if (!info.IsLiveStream)
|
if (!info.IsLiveStream)
|
||||||
{
|
{
|
||||||
if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
|
if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
|
item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
||||||
|
@ -1154,7 +1145,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
|
if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
|
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
|
||||||
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
|
|
||||||
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
|
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(Channel) },
|
IncludeItemTypes = new[] { BaseItemKind.Channel },
|
||||||
ExcludeItemIds = installedChannelIds.ToArray()
|
ExcludeItemIds = installedChannelIds.ToArray()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -63,13 +61,13 @@ namespace Emby.Server.Implementations.Collections
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
|
public event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
|
public event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
|
public event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
|
||||||
|
|
||||||
private IEnumerable<Folder> FindFolders(string path)
|
private IEnumerable<Folder> FindFolders(string path)
|
||||||
{
|
{
|
||||||
|
@ -80,14 +78,12 @@ namespace Emby.Server.Implementations.Collections
|
||||||
.Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
|
.Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded)
|
internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
|
||||||
{
|
{
|
||||||
var existingFolders = FindFolders(path)
|
var existingFolder = FindFolders(path).FirstOrDefault();
|
||||||
.ToList();
|
if (existingFolder != null)
|
||||||
|
|
||||||
if (existingFolders.Count > 0)
|
|
||||||
{
|
{
|
||||||
return existingFolders[0];
|
return existingFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!createIfNeeded)
|
if (!createIfNeeded)
|
||||||
|
@ -99,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
|
|
||||||
var libraryOptions = new LibraryOptions
|
var libraryOptions = new LibraryOptions
|
||||||
{
|
{
|
||||||
PathInfos = new[] { new MediaPathInfo { Path = path } },
|
PathInfos = new[] { new MediaPathInfo(path) },
|
||||||
EnableRealtimeMonitor = false,
|
EnableRealtimeMonitor = false,
|
||||||
SaveLocalMetadata = true
|
SaveLocalMetadata = true
|
||||||
};
|
};
|
||||||
|
@ -116,7 +112,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
return Path.Combine(_appPaths.DataPath, "collections");
|
return Path.Combine(_appPaths.DataPath, "collections");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Folder> GetCollectionsFolder(bool createIfNeeded)
|
private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
|
||||||
{
|
{
|
||||||
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
|
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
|
||||||
}
|
}
|
||||||
|
@ -164,7 +160,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
DateCreated = DateTime.UtcNow
|
DateCreated = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
parentFolder.AddChild(collection, CancellationToken.None);
|
parentFolder.AddChild(collection);
|
||||||
|
|
||||||
if (options.ItemIdList.Count > 0)
|
if (options.ItemIdList.Count > 0)
|
||||||
{
|
{
|
||||||
|
@ -200,13 +196,12 @@ namespace Emby.Server.Implementations.Collections
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
|
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
|
||||||
=> AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
|
=> AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
|
||||||
|
|
||||||
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
|
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
|
||||||
{
|
{
|
||||||
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
|
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
|
||||||
if (collection == null)
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException("No collection exists with the supplied Id");
|
throw new ArgumentException("No collection exists with the supplied Id");
|
||||||
}
|
}
|
||||||
|
@ -258,9 +253,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
|
public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
|
||||||
{
|
{
|
||||||
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
|
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
|
||||||
|
|
||||||
if (collection == null)
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException("No collection exists with the supplied Id");
|
throw new ArgumentException("No collection exists with the supplied Id");
|
||||||
}
|
}
|
||||||
|
@ -314,11 +307,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
if (item is not ISupportsBoxSetGrouping)
|
if (item is ISupportsBoxSetGrouping)
|
||||||
{
|
|
||||||
results[item.Id] = item;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
var itemId = item.Id;
|
var itemId = item.Id;
|
||||||
|
|
||||||
|
@ -342,6 +331,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
}
|
}
|
||||||
|
|
||||||
var alreadyInResults = false;
|
var alreadyInResults = false;
|
||||||
|
|
||||||
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
|
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
|
||||||
if (item is Video video)
|
if (item is Video video)
|
||||||
{
|
{
|
||||||
|
@ -357,11 +347,13 @@ namespace Emby.Server.Implementations.Collections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!alreadyInResults)
|
if (alreadyInResults)
|
||||||
{
|
{
|
||||||
results[itemId] = item;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
results[item.Id] = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.Values;
|
return results.Values;
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Model.Cryptography;
|
using MediaBrowser.Model.Cryptography;
|
||||||
using static MediaBrowser.Common.Cryptography.Constants;
|
using static MediaBrowser.Model.Cryptography.Constants;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Cryptography
|
namespace Emby.Server.Implementations.Cryptography
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class providing abstractions over cryptographic functions.
|
/// Class providing abstractions over cryptographic functions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CryptographyProvider : ICryptoProvider, IDisposable
|
public class CryptographyProvider : ICryptoProvider
|
||||||
{
|
{
|
||||||
|
// TODO: remove when not needed for backwards compat
|
||||||
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
||||||
{
|
{
|
||||||
"MD5",
|
"MD5",
|
||||||
|
@ -30,71 +33,71 @@ namespace Emby.Server.Implementations.Cryptography
|
||||||
"System.Security.Cryptography.SHA512"
|
"System.Security.Cryptography.SHA512"
|
||||||
};
|
};
|
||||||
|
|
||||||
private RandomNumberGenerator _randomNumberGenerator;
|
/// <inheritdoc />
|
||||||
|
public string DefaultHashMethod => "PBKDF2-SHA512";
|
||||||
|
|
||||||
private bool _disposed;
|
/// <inheritdoc />
|
||||||
|
public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="CryptographyProvider"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public CryptographyProvider()
|
|
||||||
{
|
{
|
||||||
// FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
|
byte[] salt = GenerateSalt();
|
||||||
// Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
|
return new PasswordHash(
|
||||||
// there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
|
DefaultHashMethod,
|
||||||
// Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
_randomNumberGenerator = RandomNumberGenerator.Create();
|
password,
|
||||||
|
salt,
|
||||||
|
DefaultIterations,
|
||||||
|
HashAlgorithmName.SHA512,
|
||||||
|
DefaultOutputLength),
|
||||||
|
salt,
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string DefaultHashMethod => "PBKDF2";
|
public bool Verify(PasswordHash hash, ReadOnlySpan<char> password)
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IEnumerable<string> GetSupportedHashMethods()
|
|
||||||
=> _supportedHashMethods;
|
|
||||||
|
|
||||||
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
|
|
||||||
{
|
{
|
||||||
// downgrading for now as we need this library to be dotnetstandard compliant
|
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
|
||||||
// with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
|
|
||||||
if (method != DefaultHashMethod)
|
|
||||||
{
|
{
|
||||||
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
|
return hash.Hash.SequenceEqual(
|
||||||
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
|
password,
|
||||||
|
hash.Salt,
|
||||||
|
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||||
|
HashAlgorithmName.SHA1,
|
||||||
|
32));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
|
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
|
||||||
return r.GetBytes(32);
|
{
|
||||||
|
return hash.Hash.SequenceEqual(
|
||||||
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
|
password,
|
||||||
|
hash.Salt,
|
||||||
|
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||||
|
HashAlgorithmName.SHA512,
|
||||||
|
DefaultOutputLength));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
if (!_supportedHashMethods.Contains(hash.Id))
|
||||||
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
|
|
||||||
{
|
{
|
||||||
if (hashMethod == DefaultHashMethod)
|
throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
|
||||||
{
|
|
||||||
return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_supportedHashMethods.Contains(hashMethod))
|
using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}.");
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(password.ToArray());
|
||||||
|
if (hash.Salt.Length == 0)
|
||||||
{
|
{
|
||||||
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
|
byte[] salted = new byte[bytes.Length + hash.Salt.Length];
|
||||||
if (salt.Length == 0)
|
|
||||||
{
|
|
||||||
return h.ComputeHash(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] salted = new byte[bytes.Length + salt.Length];
|
|
||||||
Array.Copy(bytes, salted, bytes.Length);
|
Array.Copy(bytes, salted, bytes.Length);
|
||||||
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
|
hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
|
||||||
return h.ComputeHash(salted);
|
return hash.Hash.SequenceEqual(h.ComputeHash(salted));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
|
|
||||||
=> PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public byte[] GenerateSalt()
|
public byte[] GenerateSalt()
|
||||||
=> GenerateSalt(DefaultSaltLength);
|
=> GenerateSalt(DefaultSaltLength);
|
||||||
|
@ -102,35 +105,10 @@ namespace Emby.Server.Implementations.Cryptography
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public byte[] GenerateSalt(int length)
|
public byte[] GenerateSalt(int length)
|
||||||
{
|
{
|
||||||
byte[] salt = new byte[length];
|
var salt = new byte[length];
|
||||||
_randomNumberGenerator.GetBytes(salt);
|
using var rng = RandomNumberGenerator.Create();
|
||||||
|
rng.GetNonZeroBytes(salt);
|
||||||
return salt;
|
return salt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases unmanaged and - optionally - managed resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_randomNumberGenerator.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
using SQLitePCL.pretty;
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
protected virtual int? CacheSize => null;
|
protected virtual int? CacheSize => null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />
|
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The journal mode.</value>
|
/// <value>The journal mode.</value>
|
||||||
protected virtual string JournalMode => "TRUNCATE";
|
protected virtual string JournalMode => "TRUNCATE";
|
||||||
|
@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
/// <value>The write connection.</value>
|
/// <value>The write connection.</value>
|
||||||
protected SQLiteDatabaseConnection WriteConnection { get; set; }
|
protected SQLiteDatabaseConnection WriteConnection { get; set; }
|
||||||
|
|
||||||
protected ManagedConnection GetConnection(bool _ = false)
|
protected ManagedConnection GetConnection(bool readOnly = false)
|
||||||
{
|
{
|
||||||
WriteLock.Wait();
|
WriteLock.Wait();
|
||||||
if (WriteConnection != null)
|
if (WriteConnection != null)
|
||||||
|
@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
|
|
||||||
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||||
{
|
{
|
||||||
if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase))
|
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -249,55 +249,4 @@ namespace Emby.Server.Implementations.Data
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The disk synchronization mode, controls how aggressively SQLite will write data
|
|
||||||
/// all the way out to physical storage.
|
|
||||||
/// </summary>
|
|
||||||
public enum SynchronousMode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
|
|
||||||
/// </summary>
|
|
||||||
Off = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SQLite database engine will still sync at the most critical moments.
|
|
||||||
/// </summary>
|
|
||||||
Normal = 1,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SQLite database engine will use the xSync method of the VFS
|
|
||||||
/// to ensure that all content is safely written to the disk surface prior to continuing.
|
|
||||||
/// </summary>
|
|
||||||
Full = 2,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
|
|
||||||
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
|
|
||||||
/// </summary>
|
|
||||||
Extra = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Storage mode used by temporary database files.
|
|
||||||
/// </summary>
|
|
||||||
public enum TempStoreMode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
|
|
||||||
/// is used to determine where temporary tables and indices are stored.
|
|
||||||
/// </summary>
|
|
||||||
Default = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Temporary tables and indices are stored in a file.
|
|
||||||
/// </summary>
|
|
||||||
File = 1,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
|
|
||||||
/// </summary>
|
|
||||||
Memory = 2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,12 @@ using SQLitePCL.pretty;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
public class ManagedConnection : IDisposable
|
public sealed class ManagedConnection : IDisposable
|
||||||
{
|
{
|
||||||
private SQLiteDatabaseConnection? _db;
|
|
||||||
private readonly SemaphoreSlim _writeLock;
|
private readonly SemaphoreSlim _writeLock;
|
||||||
|
|
||||||
|
private SQLiteDatabaseConnection? _db;
|
||||||
|
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
|
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
|
||||||
|
|
|
@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
dateText,
|
dateText,
|
||||||
_datetimeFormats,
|
_datetimeFormats,
|
||||||
DateTimeFormatInfo.InvariantInfo,
|
DateTimeFormatInfo.InvariantInfo,
|
||||||
DateTimeStyles.None).ToUniversalTime();
|
DateTimeStyles.AdjustToUniversal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
|
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
|
||||||
|
@ -108,9 +108,9 @@ namespace Emby.Server.Implementations.Data
|
||||||
|
|
||||||
var dateText = item.ToString();
|
var dateText = item.ToString();
|
||||||
|
|
||||||
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
|
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
|
||||||
{
|
{
|
||||||
result = dateTimeResult.ToUniversalTime();
|
result = dateTimeResult;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -32,6 +32,9 @@ namespace Emby.Server.Implementations.Data
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Opens the connection to the database.
|
/// Opens the connection to the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="userManager">The user manager.</param>
|
||||||
|
/// <param name="dbLock">The lock to use for database IO.</param>
|
||||||
|
/// <param name="dbConnection">The connection to use for database IO.</param>
|
||||||
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
|
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
|
||||||
{
|
{
|
||||||
WriteLock.Dispose();
|
WriteLock.Dispose();
|
||||||
|
@ -49,8 +52,8 @@ namespace Emby.Server.Implementations.Data
|
||||||
connection.RunInTransaction(
|
connection.RunInTransaction(
|
||||||
db =>
|
db =>
|
||||||
{
|
{
|
||||||
db.ExecuteAll(string.Join(';', new[] {
|
db.ExecuteAll(string.Join(';', new[]
|
||||||
|
{
|
||||||
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
||||||
|
|
||||||
"drop index if exists idx_userdata",
|
"drop index if exists idx_userdata",
|
||||||
|
@ -129,19 +132,17 @@ namespace Emby.Server.Implementations.Data
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Saves the user data.
|
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
|
||||||
/// </summary>
|
|
||||||
public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
if (userData == null)
|
if (userData == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(userData));
|
throw new ArgumentNullException(nameof(userData));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (internalUserId <= 0)
|
if (userId <= 0)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(internalUserId));
|
throw new ArgumentNullException(nameof(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
|
@ -149,22 +150,23 @@ namespace Emby.Server.Implementations.Data
|
||||||
throw new ArgumentNullException(nameof(key));
|
throw new ArgumentNullException(nameof(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistUserData(internalUserId, key, userData, cancellationToken);
|
PersistUserData(userId, key, userData, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken)
|
/// <inheritdoc />
|
||||||
|
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (userData == null)
|
if (userData == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(userData));
|
throw new ArgumentNullException(nameof(userData));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (internalUserId <= 0)
|
if (userId <= 0)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(internalUserId));
|
throw new ArgumentNullException(nameof(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistAllUserData(internalUserId, userData, cancellationToken);
|
PersistAllUserData(userId, userData, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -174,7 +176,6 @@ namespace Emby.Server.Implementations.Data
|
||||||
/// <param name="key">The key.</param>
|
/// <param name="key">The key.</param>
|
||||||
/// <param name="userData">The user data.</param>
|
/// <param name="userData">The user data.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
|
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
@ -264,19 +265,19 @@ namespace Emby.Server.Implementations.Data
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the user data.
|
/// Gets the user data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="internalUserId">The user id.</param>
|
/// <param name="userId">The user id.</param>
|
||||||
/// <param name="key">The key.</param>
|
/// <param name="key">The key.</param>
|
||||||
/// <returns>Task{UserItemData}.</returns>
|
/// <returns>Task{UserItemData}.</returns>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException">
|
||||||
/// userId
|
/// userId
|
||||||
/// or
|
/// or
|
||||||
/// key
|
/// key.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
public UserItemData GetUserData(long internalUserId, string key)
|
public UserItemData GetUserData(long userId, string key)
|
||||||
{
|
{
|
||||||
if (internalUserId <= 0)
|
if (userId <= 0)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(internalUserId));
|
throw new ArgumentNullException(nameof(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
|
@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
||||||
{
|
{
|
||||||
statement.TryBind("@UserId", internalUserId);
|
statement.TryBind("@UserId", userId);
|
||||||
statement.TryBind("@Key", key);
|
statement.TryBind("@Key", key);
|
||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
|
@ -301,7 +302,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserItemData GetUserData(long internalUserId, List<string> keys)
|
public UserItemData GetUserData(long userId, List<string> keys)
|
||||||
{
|
{
|
||||||
if (keys == null)
|
if (keys == null)
|
||||||
{
|
{
|
||||||
|
@ -313,19 +314,19 @@ namespace Emby.Server.Implementations.Data
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetUserData(internalUserId, keys[0]);
|
return GetUserData(userId, keys[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return all user-data associated with the given user.
|
/// Return all user-data associated with the given user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="internalUserId"></param>
|
/// <param name="userId">The internal user id.</param>
|
||||||
/// <returns></returns>
|
/// <returns>The list of user item data.</returns>
|
||||||
public List<UserItemData> GetAllUserData(long internalUserId)
|
public List<UserItemData> GetAllUserData(long userId)
|
||||||
{
|
{
|
||||||
if (internalUserId <= 0)
|
if (userId <= 0)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(internalUserId));
|
throw new ArgumentNullException(nameof(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<UserItemData>();
|
var list = new List<UserItemData>();
|
||||||
|
@ -334,7 +335,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
|
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
|
||||||
{
|
{
|
||||||
statement.TryBind("@UserId", internalUserId);
|
statement.TryBind("@UserId", userId);
|
||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
{
|
{
|
||||||
|
@ -349,7 +350,8 @@ namespace Emby.Server.Implementations.Data
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read a row from the specified reader into the provided userData object.
|
/// Read a row from the specified reader into the provided userData object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reader"></param>
|
/// <param name="reader">The list of result set values.</param>
|
||||||
|
/// <returns>The user item data.</returns>
|
||||||
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
|
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
|
||||||
{
|
{
|
||||||
var userData = new UserItemData();
|
var userData = new UserItemData();
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
namespace Emby.Server.Implementations.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The disk synchronization mode, controls how aggressively SQLite will write data
|
||||||
|
/// all the way out to physical storage.
|
||||||
|
/// </summary>
|
||||||
|
public enum SynchronousMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
|
||||||
|
/// </summary>
|
||||||
|
Off = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SQLite database engine will still sync at the most critical moments.
|
||||||
|
/// </summary>
|
||||||
|
Normal = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SQLite database engine will use the xSync method of the VFS
|
||||||
|
/// to ensure that all content is safely written to the disk surface prior to continuing.
|
||||||
|
/// </summary>
|
||||||
|
Full = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
|
||||||
|
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
|
||||||
|
/// </summary>
|
||||||
|
Extra = 3
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
namespace Emby.Server.Implementations.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Storage mode used by temporary database files.
|
||||||
|
/// </summary>
|
||||||
|
public enum TempStoreMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
|
||||||
|
/// is used to determine where temporary tables and indices are stored.
|
||||||
|
/// </summary>
|
||||||
|
Default = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Temporary tables and indices are stored in a file.
|
||||||
|
/// </summary>
|
||||||
|
File = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
|
||||||
|
/// </summary>
|
||||||
|
Memory = 2
|
||||||
|
}
|
|
@ -15,9 +15,18 @@ namespace Emby.Server.Implementations.Devices
|
||||||
{
|
{
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
private readonly ILogger<DeviceId> _logger;
|
private readonly ILogger<DeviceId> _logger;
|
||||||
|
|
||||||
private readonly object _syncLock = new object();
|
private readonly object _syncLock = new object();
|
||||||
|
|
||||||
|
private string _id;
|
||||||
|
|
||||||
|
public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
_appPaths = appPaths;
|
||||||
|
_logger = loggerFactory.CreateLogger<DeviceId>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Value => _id ?? (_id = GetDeviceId());
|
||||||
|
|
||||||
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
|
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
|
||||||
|
|
||||||
private string GetCachedId()
|
private string GetCachedId()
|
||||||
|
@ -86,15 +95,5 @@ namespace Emby.Server.Implementations.Devices
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _id;
|
|
||||||
|
|
||||||
public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
|
|
||||||
{
|
|
||||||
_appPaths = appPaths;
|
|
||||||
_logger = loggerFactory.CreateLogger<DeviceId>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Value => _id ?? (_id = GetDeviceId());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,146 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Jellyfin.Data.Entities;
|
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using Jellyfin.Data.Events;
|
|
||||||
using MediaBrowser.Controller.Devices;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Security;
|
|
||||||
using MediaBrowser.Model.Devices;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using MediaBrowser.Model.Session;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Devices
|
|
||||||
{
|
|
||||||
public class DeviceManager : IDeviceManager
|
|
||||||
{
|
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
private readonly IAuthenticationRepository _authRepo;
|
|
||||||
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
|
|
||||||
|
|
||||||
public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
|
|
||||||
{
|
|
||||||
_userManager = userManager;
|
|
||||||
_authRepo = authRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
|
||||||
|
|
||||||
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
|
|
||||||
{
|
|
||||||
_capabilitiesMap[deviceId] = capabilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
|
|
||||||
{
|
|
||||||
_authRepo.UpdateDeviceOptions(deviceId, options);
|
|
||||||
|
|
||||||
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public DeviceOptions GetDeviceOptions(string deviceId)
|
|
||||||
{
|
|
||||||
return _authRepo.GetDeviceOptions(deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClientCapabilities GetCapabilities(string id)
|
|
||||||
{
|
|
||||||
return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
|
|
||||||
? result
|
|
||||||
: new ClientCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
public DeviceInfo GetDevice(string id)
|
|
||||||
{
|
|
||||||
var session = _authRepo.Get(new AuthenticationInfoQuery
|
|
||||||
{
|
|
||||||
DeviceId = id
|
|
||||||
}).Items.FirstOrDefault();
|
|
||||||
|
|
||||||
var device = session == null ? null : ToDeviceInfo(session);
|
|
||||||
|
|
||||||
return device;
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
|
|
||||||
{
|
|
||||||
IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
|
|
||||||
{
|
|
||||||
// UserId = query.UserId
|
|
||||||
HasUser = true
|
|
||||||
}).Items;
|
|
||||||
|
|
||||||
// TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
|
|
||||||
if (query.SupportsSync.HasValue)
|
|
||||||
{
|
|
||||||
var val = query.SupportsSync.Value;
|
|
||||||
|
|
||||||
sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!query.UserId.Equals(Guid.Empty))
|
|
||||||
{
|
|
||||||
var user = _userManager.GetUserById(query.UserId);
|
|
||||||
|
|
||||||
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
|
|
||||||
}
|
|
||||||
|
|
||||||
var array = sessions.Select(ToDeviceInfo).ToArray();
|
|
||||||
|
|
||||||
return new QueryResult<DeviceInfo>(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
|
|
||||||
{
|
|
||||||
var caps = GetCapabilities(authInfo.DeviceId);
|
|
||||||
|
|
||||||
return new DeviceInfo
|
|
||||||
{
|
|
||||||
AppName = authInfo.AppName,
|
|
||||||
AppVersion = authInfo.AppVersion,
|
|
||||||
Id = authInfo.DeviceId,
|
|
||||||
LastUserId = authInfo.UserId,
|
|
||||||
LastUserName = authInfo.UserName,
|
|
||||||
Name = authInfo.DeviceName,
|
|
||||||
DateLastActivity = authInfo.DateLastActivity,
|
|
||||||
IconUrl = caps?.IconUrl
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanAccessDevice(User user, string deviceId)
|
|
||||||
{
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("user not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(deviceId))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(deviceId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var capabilities = GetCapabilities(deviceId);
|
|
||||||
|
|
||||||
if (capabilities != null && capabilities.SupportsPersistentIdentifier)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,9 +7,9 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
|
@ -51,8 +51,6 @@ namespace Emby.Server.Implementations.Dto
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
|
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
|
||||||
|
|
||||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
|
||||||
|
|
||||||
public DtoService(
|
public DtoService(
|
||||||
ILogger<DtoService> logger,
|
ILogger<DtoService> logger,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
|
@ -75,6 +73,8 @@ namespace Emby.Server.Implementations.Dto
|
||||||
_livetvManagerFactory = livetvManagerFactory;
|
_livetvManagerFactory = livetvManagerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
||||||
{
|
{
|
||||||
|
@ -134,14 +134,11 @@ namespace Emby.Server.Implementations.Dto
|
||||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||||
if (item is LiveTvChannel tvChannel)
|
if (item is LiveTvChannel tvChannel)
|
||||||
{
|
{
|
||||||
var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) };
|
LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
|
||||||
LivetvManager.AddChannelInfo(list, options, user);
|
|
||||||
}
|
}
|
||||||
else if (item is LiveTvProgram)
|
else if (item is LiveTvProgram)
|
||||||
{
|
{
|
||||||
var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) };
|
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
|
||||||
var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user);
|
|
||||||
Task.WaitAll(task);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is IItemByName itemByName
|
if (item is IItemByName itemByName
|
||||||
|
@ -297,7 +294,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
path = path.TrimStart('.');
|
path = path.TrimStart('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase))
|
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
fileExtensionContainer = path;
|
fileExtensionContainer = path;
|
||||||
}
|
}
|
||||||
|
@ -373,6 +370,12 @@ namespace Emby.Server.Implementations.Dto
|
||||||
if (item is MusicAlbum || item is Season || item is Playlist)
|
if (item is MusicAlbum || item is Season || item is Playlist)
|
||||||
{
|
{
|
||||||
dto.ChildCount = dto.RecursiveItemCount;
|
dto.ChildCount = dto.RecursiveItemCount;
|
||||||
|
var folderChildCount = folder.LinkedChildren.Length;
|
||||||
|
// The default is an empty array, so we can't reliably use the count when it's empty
|
||||||
|
if (folderChildCount > 0)
|
||||||
|
{
|
||||||
|
dto.ChildCount ??= folderChildCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.ChildCount))
|
if (options.ContainsField(ItemFields.ChildCount))
|
||||||
|
@ -420,7 +423,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
// Just return something so that apps that are expecting a value won't think the folders are empty
|
// Just return something so that apps that are expecting a value won't think the folders are empty
|
||||||
if (folder is ICollectionFolder || folder is UserView)
|
if (folder is ICollectionFolder || folder is UserView)
|
||||||
{
|
{
|
||||||
return new Random().Next(1, 10);
|
return Random.Shared.Next(1, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
return folder.GetChildCount(user);
|
return folder.GetChildCount(user);
|
||||||
|
@ -467,7 +470,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
{
|
{
|
||||||
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
|
||||||
Name = item.Album,
|
Name = item.Album,
|
||||||
Limit = 1
|
Limit = 1
|
||||||
});
|
});
|
||||||
|
@ -497,7 +500,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error getting {imageType} image info for {path}", image.Type, image.Path);
|
_logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -507,7 +510,6 @@ namespace Emby.Server.Implementations.Dto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto">The dto.</param>
|
/// <param name="dto">The dto.</param>
|
||||||
/// <param name="item">The item.</param>
|
/// <param name="item">The item.</param>
|
||||||
/// <returns>Task.</returns>
|
|
||||||
private void AttachPeople(BaseItemDto dto, BaseItem item)
|
private void AttachPeople(BaseItemDto dto, BaseItem item)
|
||||||
{
|
{
|
||||||
// Ordering by person type to ensure actors and artists are at the front.
|
// Ordering by person type to ensure actors and artists are at the front.
|
||||||
|
@ -616,7 +618,6 @@ namespace Emby.Server.Implementations.Dto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto">The dto.</param>
|
/// <param name="dto">The dto.</param>
|
||||||
/// <param name="item">The item.</param>
|
/// <param name="item">The item.</param>
|
||||||
/// <returns>Task.</returns>
|
|
||||||
private void AttachStudios(BaseItemDto dto, BaseItem item)
|
private void AttachStudios(BaseItemDto dto, BaseItem item)
|
||||||
{
|
{
|
||||||
dto.Studios = item.Studios
|
dto.Studios = item.Studios
|
||||||
|
@ -757,15 +758,6 @@ namespace Emby.Server.Implementations.Dto
|
||||||
dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
|
dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.ScreenshotImageTags))
|
|
||||||
{
|
|
||||||
var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
|
|
||||||
if (screenshotLimit > 0)
|
|
||||||
{
|
|
||||||
dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.Genres))
|
if (options.ContainsField(ItemFields.Genres))
|
||||||
{
|
{
|
||||||
dto.Genres = item.Genres;
|
dto.Genres = item.Genres;
|
||||||
|
@ -807,7 +799,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
|
|
||||||
dto.MediaType = item.MediaType;
|
dto.MediaType = item.MediaType;
|
||||||
|
|
||||||
if (!(item is LiveTvProgram))
|
if (item is not LiveTvProgram)
|
||||||
{
|
{
|
||||||
dto.LocationType = item.LocationType;
|
dto.LocationType = item.LocationType;
|
||||||
}
|
}
|
||||||
|
@ -928,9 +920,9 @@ namespace Emby.Server.Implementations.Dto
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (options.ContainsField(ItemFields.MediaSourceCount))
|
// if (options.ContainsField(ItemFields.MediaSourceCount))
|
||||||
//{
|
// {
|
||||||
// Songs always have one
|
// Songs always have one
|
||||||
//}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is IHasArtist hasArtist)
|
if (item is IHasArtist hasArtist)
|
||||||
|
@ -938,10 +930,10 @@ namespace Emby.Server.Implementations.Dto
|
||||||
dto.Artists = hasArtist.Artists;
|
dto.Artists = hasArtist.Artists;
|
||||||
|
|
||||||
// var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
|
// var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
|
||||||
//{
|
// {
|
||||||
// EnableTotalRecordCount = false,
|
// EnableTotalRecordCount = false,
|
||||||
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
|
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
|
||||||
//});
|
// });
|
||||||
|
|
||||||
// dto.ArtistItems = artistItems.Items
|
// dto.ArtistItems = artistItems.Items
|
||||||
// .Select(i =>
|
// .Select(i =>
|
||||||
|
@ -958,7 +950,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
||||||
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
||||||
dto.ArtistItems = hasArtist.Artists
|
dto.ArtistItems = hasArtist.Artists
|
||||||
//.Except(foundArtists, new DistinctNameComparer())
|
// .Except(foundArtists, new DistinctNameComparer())
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
// This should not be necessary but we're seeing some cases of it
|
// This should not be necessary but we're seeing some cases of it
|
||||||
|
@ -990,10 +982,10 @@ namespace Emby.Server.Implementations.Dto
|
||||||
dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||||
|
|
||||||
// var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
|
// var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
|
||||||
//{
|
// {
|
||||||
// EnableTotalRecordCount = false,
|
// EnableTotalRecordCount = false,
|
||||||
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
|
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
|
||||||
//});
|
// });
|
||||||
|
|
||||||
// dto.AlbumArtists = artistItems.Items
|
// dto.AlbumArtists = artistItems.Items
|
||||||
// .Select(i =>
|
// .Select(i =>
|
||||||
|
@ -1008,7 +1000,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
// .ToList();
|
// .ToList();
|
||||||
|
|
||||||
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
||||||
//.Except(foundArtists, new DistinctNameComparer())
|
// .Except(foundArtists, new DistinctNameComparer())
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
// This should not be necessary but we're seeing some cases of it
|
// This should not be necessary but we're seeing some cases of it
|
||||||
|
@ -1035,8 +1027,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add video info
|
// Add video info
|
||||||
var video = item as Video;
|
if (item is Video video)
|
||||||
if (video != null)
|
|
||||||
{
|
{
|
||||||
dto.VideoType = video.VideoType;
|
dto.VideoType = video.VideoType;
|
||||||
dto.Video3DFormat = video.Video3DFormat;
|
dto.Video3DFormat = video.Video3DFormat;
|
||||||
|
@ -1075,9 +1066,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
if (options.ContainsField(ItemFields.MediaStreams))
|
if (options.ContainsField(ItemFields.MediaStreams))
|
||||||
{
|
{
|
||||||
// Add VideoInfo
|
// Add VideoInfo
|
||||||
var iHasMediaSources = item as IHasMediaSources;
|
if (item is IHasMediaSources)
|
||||||
|
|
||||||
if (iHasMediaSources != null)
|
|
||||||
{
|
{
|
||||||
MediaStream[] mediaStreams;
|
MediaStream[] mediaStreams;
|
||||||
|
|
||||||
|
@ -1146,7 +1135,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
// TODO maybe remove the if statement entirely
|
// TODO maybe remove the if statement entirely
|
||||||
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
|
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
|
||||||
{
|
{
|
||||||
episodeSeries = episodeSeries ?? episode.Series;
|
episodeSeries ??= episode.Series;
|
||||||
if (episodeSeries != null)
|
if (episodeSeries != null)
|
||||||
{
|
{
|
||||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
|
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
|
||||||
|
@ -1159,7 +1148,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.SeriesStudio))
|
if (options.ContainsField(ItemFields.SeriesStudio))
|
||||||
{
|
{
|
||||||
episodeSeries = episodeSeries ?? episode.Series;
|
episodeSeries ??= episode.Series;
|
||||||
if (episodeSeries != null)
|
if (episodeSeries != null)
|
||||||
{
|
{
|
||||||
dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
|
dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
|
||||||
|
@ -1172,7 +1161,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
{
|
{
|
||||||
dto.AirDays = series.AirDays;
|
dto.AirDays = series.AirDays;
|
||||||
dto.AirTime = series.AirTime;
|
dto.AirTime = series.AirTime;
|
||||||
dto.Status = series.Status.HasValue ? series.Status.Value.ToString() : null;
|
dto.Status = series.Status?.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add SeasonInfo
|
// Add SeasonInfo
|
||||||
|
@ -1185,7 +1174,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.SeriesStudio))
|
if (options.ContainsField(ItemFields.SeriesStudio))
|
||||||
{
|
{
|
||||||
series = series ?? season.Series;
|
series ??= season.Series;
|
||||||
if (series != null)
|
if (series != null)
|
||||||
{
|
{
|
||||||
dto.SeriesStudio = series.Studios.FirstOrDefault();
|
dto.SeriesStudio = series.Studios.FirstOrDefault();
|
||||||
|
@ -1196,7 +1185,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
// TODO maybe remove the if statement entirely
|
// TODO maybe remove the if statement entirely
|
||||||
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
|
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
|
||||||
{
|
{
|
||||||
series = series ?? season.Series;
|
series ??= season.Series;
|
||||||
if (series != null)
|
if (series != null)
|
||||||
{
|
{
|
||||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
|
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
|
||||||
|
@ -1283,7 +1272,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
|
|
||||||
var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
|
var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
|
||||||
|
|
||||||
if (parent == null && !(originalItem is UserRootFolder) && !(originalItem is UserView) && !(originalItem is AggregateFolder) && !(originalItem is ICollectionFolder) && !(originalItem is Channel))
|
if (parent == null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel)
|
||||||
{
|
{
|
||||||
parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
|
parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
@ -1316,9 +1305,12 @@ namespace Emby.Server.Implementations.Dto
|
||||||
|
|
||||||
var imageTags = dto.ImageTags;
|
var imageTags = dto.ImageTags;
|
||||||
|
|
||||||
while (((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) &&
|
while ((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
|
||||||
(parent = parent ?? (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null)
|
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
|
||||||
|
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
|
||||||
|
|| parent is Series)
|
||||||
{
|
{
|
||||||
|
parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
|
||||||
if (parent == null)
|
if (parent == null)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
|
@ -1348,7 +1340,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
|
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
|
||||||
{
|
{
|
||||||
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
|
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
|
||||||
|
|
||||||
|
@ -1398,7 +1390,6 @@ namespace Emby.Server.Implementations.Dto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto">The dto.</param>
|
/// <param name="dto">The dto.</param>
|
||||||
/// <param name="item">The item.</param>
|
/// <param name="item">The item.</param>
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
|
public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
|
||||||
{
|
{
|
||||||
dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
|
dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
|
||||||
|
@ -1413,44 +1404,27 @@ namespace Emby.Server.Implementations.Dto
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageDimensions size;
|
|
||||||
|
|
||||||
var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
|
|
||||||
|
|
||||||
if (defaultAspectRatio > 0)
|
|
||||||
{
|
|
||||||
return defaultAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!imageInfo.IsLocalFile)
|
if (!imageInfo.IsLocalFile)
|
||||||
{
|
{
|
||||||
return null;
|
return item.GetDefaultPrimaryImageAspectRatio();
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
size = _imageProcessor.GetImageDimensions(item, imageInfo);
|
var size = _imageProcessor.GetImageDimensions(item, imageInfo);
|
||||||
|
var width = size.Width;
|
||||||
if (size.Width <= 0 || size.Height <= 0)
|
var height = size.Height;
|
||||||
|
if (width > 0 && height > 0)
|
||||||
{
|
{
|
||||||
return null;
|
return (double)width / height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
|
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var width = size.Width;
|
return item.GetDefaultPrimaryImageAspectRatio();
|
||||||
var height = size.Height;
|
|
||||||
|
|
||||||
if (width <= 0 || height <= 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (double)width / height;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,17 +23,18 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
|
||||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" />
|
||||||
<PackageReference Include="Mono.Nat" Version="3.0.1" />
|
<PackageReference Include="Mono.Nat" Version="3.0.2" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
|
||||||
<PackageReference Include="sharpcompress" Version="0.28.3" />
|
<PackageReference Include="sharpcompress" Version="0.30.1" />
|
||||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||||
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
|
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -41,15 +42,11 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
||||||
<NoWarn>AD0001</NoWarn>
|
<NoWarn>AD0001</NoWarn>
|
||||||
<AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
|
|
|
@ -9,12 +9,10 @@ using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Events;
|
|
||||||
using Jellyfin.Networking.Configuration;
|
using Jellyfin.Networking.Configuration;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Plugins;
|
using MediaBrowser.Controller.Plugins;
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Mono.Nat;
|
using Mono.Nat;
|
||||||
|
|
||||||
|
@ -28,7 +26,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private readonly ILogger<ExternalPortForwarding> _logger;
|
private readonly ILogger<ExternalPortForwarding> _logger;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
|
private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
|
||||||
|
|
||||||
|
@ -43,17 +40,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="appHost">The application host.</param>
|
/// <param name="appHost">The application host.</param>
|
||||||
/// <param name="config">The configuration manager.</param>
|
/// <param name="config">The configuration manager.</param>
|
||||||
/// <param name="deviceDiscovery">The device discovery.</param>
|
|
||||||
public ExternalPortForwarding(
|
public ExternalPortForwarding(
|
||||||
ILogger<ExternalPortForwarding> logger,
|
ILogger<ExternalPortForwarding> logger,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config)
|
||||||
IDeviceDiscovery deviceDiscovery)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_config = config;
|
_config = config;
|
||||||
_deviceDiscovery = deviceDiscovery;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetConfigIdentifier()
|
private string GetConfigIdentifier()
|
||||||
|
|
|
@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
|
|
||||||
private static bool EnableRefreshMessage(BaseItem item)
|
private static bool EnableRefreshMessage(BaseItem item)
|
||||||
{
|
{
|
||||||
if (!(item is Folder folder))
|
if (item is not Folder folder)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is IItemByName && !(item is MusicArtist))
|
if (item is IItemByName && item is not MusicArtist)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -436,7 +436,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Translates the physical item to user library.
|
/// Translates the physical item to user library.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
/// <typeparam name="T">The type of item.</typeparam>
|
||||||
/// <param name="item">The item.</param>
|
/// <param name="item">The item.</param>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>
|
/// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>
|
||||||
|
|
|
@ -37,6 +37,9 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
|
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
|
||||||
|
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
|
||||||
|
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
|
||||||
public UdpServerEntryPoint(
|
public UdpServerEntryPoint(
|
||||||
ILogger<UdpServerEntryPoint> logger,
|
ILogger<UdpServerEntryPoint> logger,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Authentication;
|
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
@ -17,13 +17,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
_authorizationContext = authorizationContext;
|
_authorizationContext = authorizationContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthorizationInfo Authenticate(HttpRequest request)
|
public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
|
||||||
{
|
{
|
||||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!auth.HasToken)
|
if (!auth.HasToken)
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Request does not contain a token.");
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!auth.IsAuthenticated)
|
if (!auth.IsAuthenticated)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
@ -23,27 +24,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SessionInfo GetSession(HttpContext requestContext)
|
public async Task<SessionInfo> GetSession(HttpContext requestContext)
|
||||||
{
|
{
|
||||||
var authorization = _authContext.GetAuthorizationInfo(requestContext);
|
var authorization = await _authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
|
||||||
|
|
||||||
var user = authorization.User;
|
var user = authorization.User;
|
||||||
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
|
return await _sessionManager.LogSessionActivity(
|
||||||
|
authorization.Client,
|
||||||
|
authorization.Version,
|
||||||
|
authorization.DeviceId,
|
||||||
|
authorization.Device,
|
||||||
|
requestContext.GetNormalizedRemoteIp().ToString(),
|
||||||
|
user).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SessionInfo GetSession(object requestContext)
|
public Task<SessionInfo> GetSession(object requestContext)
|
||||||
{
|
{
|
||||||
return GetSession((HttpContext)requestContext);
|
return GetSession((HttpContext)requestContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User? GetUser(HttpContext requestContext)
|
public async Task<User?> GetUser(HttpContext requestContext)
|
||||||
{
|
{
|
||||||
var session = GetSession(requestContext);
|
var session = await GetSession(requestContext).ConfigureAwait(false);
|
||||||
|
|
||||||
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
|
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User? GetUser(object requestContext)
|
public Task<User?> GetUser(object requestContext)
|
||||||
{
|
{
|
||||||
return GetUser(((HttpRequest)requestContext).HttpContext);
|
return GetUser(((HttpRequest)requestContext).HttpContext);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
|
@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
public event EventHandler<EventArgs>? Closed;
|
public event EventHandler<EventArgs>? Closed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the remote end point.
|
/// Gets the remote end point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IPAddress? RemoteEndPoint { get; }
|
public IPAddress? RemoteEndPoint { get; }
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
public DateTime LastKeepAliveDate { get; set; }
|
public DateTime LastKeepAliveDate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the query string.
|
/// Gets the query string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The query string.</value>
|
/// <value>The query string.</value>
|
||||||
public IQueryCollection QueryString { get; }
|
public IQueryCollection QueryString { get; }
|
||||||
|
@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a message asynchronously.
|
/// Sends a message asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
/// <typeparam name="T">The type of the message.</typeparam>
|
||||||
/// <param name="message">The message.</param>
|
/// <param name="message">The message.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
|
@ -150,8 +150,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
{
|
{
|
||||||
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
|
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
} while (
|
}
|
||||||
(_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
|
while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
|
||||||
&& receiveresult.MessageType != WebSocketMessageType.Close);
|
&& receiveresult.MessageType != WebSocketMessageType.Close);
|
||||||
|
|
||||||
Closed?.Invoke(this, EventArgs.Empty);
|
Closed?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
|
@ -35,7 +35,12 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task WebSocketRequestHandler(HttpContext context)
|
public async Task WebSocketRequestHandler(HttpContext context)
|
||||||
{
|
{
|
||||||
_ = _authService.Authenticate(context.Request);
|
var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false);
|
||||||
|
if (!authorizationInfo.IsAuthenticated)
|
||||||
|
{
|
||||||
|
throw new SecurityException("Token is required");
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
|
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.IO
|
namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
public class FileRefresher : IDisposable
|
public sealed class FileRefresher : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
|
|
||||||
private readonly List<string> _affectedPaths = new List<string>();
|
private readonly List<string> _affectedPaths = new List<string>();
|
||||||
private readonly object _timerLock = new object();
|
private readonly object _timerLock = new object();
|
||||||
private Timer _timer;
|
private Timer? _timer;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
||||||
|
@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
AddPath(path);
|
AddPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public event EventHandler<EventArgs> Completed;
|
public event EventHandler<EventArgs>? Completed;
|
||||||
|
|
||||||
public string Path { get; private set; }
|
public string Path { get; private set; }
|
||||||
|
|
||||||
|
@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
RestartTimer();
|
RestartTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTimerCallback(object state)
|
private void OnTimerCallback(object? state)
|
||||||
{
|
{
|
||||||
List<string> paths;
|
List<string> paths;
|
||||||
|
|
||||||
|
@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ProcessPathChanges(paths.ToList());
|
ProcessPathChanges(paths);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO
|
||||||
|
|
||||||
private void ProcessPathChanges(List<string> paths)
|
private void ProcessPathChanges(List<string> paths)
|
||||||
{
|
{
|
||||||
var itemsToRefresh = paths
|
IEnumerable<BaseItem> itemsToRefresh = paths
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(GetAffectedBaseItem)
|
.Select(GetAffectedBaseItem)
|
||||||
.Where(item => item != null)
|
.Where(item => item != null)
|
||||||
.GroupBy(x => x.Id)
|
.GroupBy(x => x!.Id) // Removed null values in the previous .Where()
|
||||||
.Select(x => x.First());
|
.Select(x => x.First())!;
|
||||||
|
|
||||||
foreach (var item in itemsToRefresh)
|
foreach (var item in itemsToRefresh)
|
||||||
{
|
{
|
||||||
|
@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <returns>BaseItem.</returns>
|
/// <returns>BaseItem.</returns>
|
||||||
private BaseItem GetAffectedBaseItem(string path)
|
private BaseItem? GetAffectedBaseItem(string path)
|
||||||
{
|
{
|
||||||
BaseItem item = null;
|
BaseItem? item = null;
|
||||||
|
|
||||||
while (item == null && !string.IsNullOrEmpty(path))
|
while (item == null && !string.IsNullOrEmpty(path))
|
||||||
{
|
{
|
||||||
item = _libraryManager.FindByPath(path, null);
|
item = _libraryManager.FindByPath(path, null);
|
||||||
|
|
||||||
path = System.IO.Path.GetDirectoryName(path);
|
path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item != null)
|
if (item != null)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue