Merge branch 'master' into TVFix
This commit is contained in:
commit
ffe5ae8056
|
@ -7,7 +7,7 @@ parameters:
|
||||||
default: "ubuntu-latest"
|
default: "ubuntu-latest"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.103
|
default: 5.0.302
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: CompatibilityCheck
|
- job: CompatibilityCheck
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
parameters:
|
|
||||||
- name: LinuxImage
|
|
||||||
type: string
|
|
||||||
default: "ubuntu-latest"
|
|
||||||
- name: GeneratorVersion
|
|
||||||
type: string
|
|
||||||
default: "5.0.1"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- job: GenerateApiClients
|
|
||||||
displayName: 'Generate Api Clients'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
|
||||||
dependsOn: Test
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: "${{ parameters.LinuxImage }}"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: DownloadPipelineArtifact@2
|
|
||||||
displayName: 'Download OpenAPI Spec Artifact'
|
|
||||||
inputs:
|
|
||||||
source: 'current'
|
|
||||||
artifact: "OpenAPI Spec"
|
|
||||||
path: "$(System.ArtifactsDirectory)/openapispec"
|
|
||||||
runVersion: "latest"
|
|
||||||
|
|
||||||
- task: CmdLine@2
|
|
||||||
displayName: 'Download OpenApi Generator'
|
|
||||||
inputs:
|
|
||||||
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
|
|
||||||
|
|
||||||
## Authenticate with npm registry
|
|
||||||
- task: npmAuthenticate@0
|
|
||||||
inputs:
|
|
||||||
workingFile: ./.npmrc
|
|
||||||
customEndpoint: 'jellyfin-bot for NPM'
|
|
||||||
|
|
||||||
## Generate npm api client
|
|
||||||
- task: CmdLine@2
|
|
||||||
displayName: 'Build stable typescript axios client'
|
|
||||||
inputs:
|
|
||||||
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
|
|
||||||
|
|
||||||
## Run npm install
|
|
||||||
- task: Npm@1
|
|
||||||
displayName: 'Install npm dependencies'
|
|
||||||
inputs:
|
|
||||||
command: install
|
|
||||||
workingDir: ./apiclient/generated/typescript/axios
|
|
||||||
|
|
||||||
## Publish npm packages
|
|
||||||
- task: Npm@1
|
|
||||||
displayName: 'Publish stable typescript axios client'
|
|
||||||
inputs:
|
|
||||||
command: custom
|
|
||||||
customCommand: publish --access public
|
|
||||||
publishRegistry: useExternalRegistry
|
|
||||||
publishEndpoint: 'jellyfin-bot for NPM'
|
|
||||||
workingDir: ./apiclient/generated/typescript/axios
|
|
|
@ -1,7 +1,7 @@
|
||||||
parameters:
|
parameters:
|
||||||
LinuxImage: 'ubuntu-latest'
|
LinuxImage: 'ubuntu-latest'
|
||||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
DotNetSdkVersion: 5.0.103
|
DotNetSdkVersion: 5.0.302
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Build
|
- job: Build
|
||||||
|
|
|
@ -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: 5.0.302
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Test
|
- job: Test
|
||||||
|
|
|
@ -6,7 +6,7 @@ variables:
|
||||||
- name: RestoreBuildProjects
|
- name: RestoreBuildProjects
|
||||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
value: 5.0.103
|
value: 5.0.302
|
||||||
|
|
||||||
pr:
|
pr:
|
||||||
autoCancel: true
|
autoCancel: true
|
||||||
|
@ -61,6 +61,3 @@ jobs:
|
||||||
|
|
||||||
- ${{ 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')) }}:
|
||||||
- template: azure-pipelines-package.yml
|
- template: azure-pipelines-package.yml
|
||||||
|
|
||||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
|
||||||
- template: azure-pipelines-api-client.yml
|
|
||||||
|
|
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -14,9 +14,11 @@ assignees: ''
|
||||||
- OS: [e.g. Debian, Windows]
|
- OS: [e.g. Debian, Windows]
|
||||||
- Virtualization: [e.g. Docker, KVM, LXC]
|
- Virtualization: [e.g. Docker, KVM, LXC]
|
||||||
- Clients: [Browser, Android, Fire Stick, etc.]
|
- Clients: [Browser, Android, Fire Stick, etc.]
|
||||||
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
|
- Browser: [e.g. Firefox 91, Chrome 93, Safari 13]
|
||||||
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
|
- Jellyfin Version: [e.g. 10.7.6, unstable 20191231]
|
||||||
|
- FFmpeg Version: [e.g. 4.3.2-Jellyfin]
|
||||||
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
|
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
|
||||||
|
- Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
|
||||||
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
|
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
|
||||||
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
|
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
|
||||||
- Base URL: [e.g. none, yes: /example]
|
- Base URL: [e.g. none, yes: /example]
|
||||||
|
@ -33,7 +35,13 @@ assignees: ''
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
<!-- A clear and concise description of what you expected to happen. -->
|
<!-- A clear and concise description of what you expected to happen. -->
|
||||||
|
|
||||||
**Logs**
|
**Server Logs**
|
||||||
|
<!-- Please paste any log errors. -->
|
||||||
|
|
||||||
|
**FFmpeg Logs**
|
||||||
|
<!-- Please paste any log errors. -->
|
||||||
|
|
||||||
|
**Browser Console Logs**
|
||||||
<!-- Please paste any log errors. -->
|
<!-- Please paste any log errors. -->
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
|
|
4
.github/stale.yml
vendored
4
.github/stale.yml
vendored
|
@ -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
|
||||||
|
|
48
.github/workflows/automation.yml
vendored
48
.github/workflows/automation.yml
vendored
|
@ -1,46 +1,56 @@
|
||||||
name: Automation
|
name: Automation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
push:
|
||||||
issues:
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request_target:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
label:
|
||||||
|
name: Labeling
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Does PR has the stable backport label?
|
- name: Apply label
|
||||||
uses: Dreamcodeio/does-pr-has-label@v1.2
|
uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
||||||
id: checkLabel
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||||
with:
|
with:
|
||||||
label: stable backport
|
dirtyLabel: 'merge conflict'
|
||||||
|
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
project:
|
||||||
|
name: Project board
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
|
steps:
|
||||||
- name: Remove from 'Current Release' project
|
- name: Remove from 'Current Release' project
|
||||||
uses: alex-page/github-project-automation-plus@v0.5.1
|
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||||
if: (github.event.pull_request || github.event.issue.pull_request) && !steps.checkLabel.outputs.hasLabel
|
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:
|
||||||
project: Current Release
|
project: Current Release
|
||||||
action: delete
|
action: delete
|
||||||
repo-token: ${{ secrets.GH_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.5.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:
|
||||||
project: Release Next
|
project: Release Next
|
||||||
column: In progress
|
column: In progress
|
||||||
repo-token: ${{ secrets.GH_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.5.1
|
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||||
if: (github.event.pull_request || github.event.issue.pull_request) && steps.checkLabel.outputs.hasLabel
|
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:
|
||||||
project: Current Release
|
project: Current Release
|
||||||
column: In progress
|
column: In progress
|
||||||
repo-token: ${{ secrets.GH_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Check number of comments from the team member
|
- name: Check number of comments from the team member
|
||||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
|
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
|
||||||
|
@ -48,19 +58,19 @@ 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.5.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:
|
||||||
project: Issue Triage for Main Repo
|
project: Issue Triage for Main Repo
|
||||||
column: Needs triage
|
column: Needs triage
|
||||||
repo-token: ${{ secrets.GH_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.5.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:
|
||||||
project: Issue Triage for Main Repo
|
project: Issue Triage for Main Repo
|
||||||
column: Pending response
|
column: Pending response
|
||||||
repo-token: ${{ secrets.GH_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
119
.github/workflows/commands.yml
vendored
Normal file
119
.github/workflows/commands.yml
vendored
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
name: Commands
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types:
|
||||||
|
- created
|
||||||
|
- edited
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rebase:
|
||||||
|
name: Rebase
|
||||||
|
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Notify as seen
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ github.event.comment.id }}
|
||||||
|
reactions: '+1'
|
||||||
|
|
||||||
|
- name: Checkout the latest code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Automatic Rebase
|
||||||
|
uses: cirrus-actions/rebase@1.4
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
check-backport:
|
||||||
|
name: Check Backport
|
||||||
|
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Notify as seen
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ github.event.comment.id }}
|
||||||
|
reactions: eyes
|
||||||
|
|
||||||
|
- name: Checkout the latest code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Notify as running
|
||||||
|
id: comment_running
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
Running backport tests...
|
||||||
|
|
||||||
|
- name: Perform test backport
|
||||||
|
id: run_tests
|
||||||
|
run: |
|
||||||
|
set +o errexit
|
||||||
|
git config --global user.name "Jellyfin Bot"
|
||||||
|
git config --global user.email "team@jellyfin.org"
|
||||||
|
CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
|
||||||
|
git checkout master
|
||||||
|
git merge --no-ff ${CURRENT_BRANCH}
|
||||||
|
MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
|
||||||
|
git fetch --all
|
||||||
|
CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
|
||||||
|
stable_branch="Current stable release branch: ${CURRENT_STABLE}"
|
||||||
|
echo ${stable_branch}
|
||||||
|
echo ::set-output name=branch::${stable_branch}
|
||||||
|
git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
|
||||||
|
git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
|
||||||
|
retcode=$?
|
||||||
|
cat output.txt | grep -v 'hint:'
|
||||||
|
output="$( grep -v 'hint:' output.txt )"
|
||||||
|
output="${output//'%'/'%25'}"
|
||||||
|
output="${output//$'\n'/'%0A'}"
|
||||||
|
output="${output//$'\r'/'%0D'}"
|
||||||
|
echo ::set-output name=output::$output
|
||||||
|
exit ${retcode}
|
||||||
|
|
||||||
|
- name: Notify with result success
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null && success() }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
||||||
|
body: |
|
||||||
|
${{ steps.run_tests.outputs.branch }}
|
||||||
|
Output from `git cherry-pick`:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
${{ steps.run_tests.outputs.output }}
|
||||||
|
reactions: hooray
|
||||||
|
|
||||||
|
- name: Notify with result failure
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null && failure() }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
||||||
|
body: |
|
||||||
|
${{ steps.run_tests.outputs.branch }}
|
||||||
|
Output from `git cherry-pick`:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
${{ steps.run_tests.outputs.output }}
|
||||||
|
reactions: confused
|
17
.github/workflows/merge-conflicts.yml
vendored
17
.github/workflows/merge-conflicts.yml
vendored
|
@ -1,17 +0,0 @@
|
||||||
name: 'Merge Conflicts'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- synchronize
|
|
||||||
jobs:
|
|
||||||
triage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
|
||||||
with:
|
|
||||||
dirtyLabel: 'merge conflict'
|
|
||||||
repoToken: ${{ secrets.GH_TOKEN }}
|
|
27
.github/workflows/rebase.yml
vendored
27
.github/workflows/rebase.yml
vendored
|
@ -1,27 +0,0 @@
|
||||||
name: Automatic Rebase
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
rebase:
|
|
||||||
name: Rebase
|
|
||||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Notify as seen
|
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
|
||||||
comment-id: ${{ github.event.comment.id }}
|
|
||||||
reactions: '+1'
|
|
||||||
|
|
||||||
- name: Checkout the latest code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Automatic Rebase
|
|
||||||
uses: cirrus-actions/rebase@1.4
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -268,6 +268,7 @@ doc/
|
||||||
# Deployment artifacts
|
# Deployment artifacts
|
||||||
dist
|
dist
|
||||||
*.exe
|
*.exe
|
||||||
|
*.dll
|
||||||
|
|
||||||
# BenchmarkDotNet artifacts
|
# BenchmarkDotNet artifacts
|
||||||
BenchmarkDotNet.Artifacts
|
BenchmarkDotNet.Artifacts
|
||||||
|
@ -277,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
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -70,6 +71,7 @@
|
||||||
- [marius-luca-87](https://github.com/marius-luca-87)
|
- [marius-luca-87](https://github.com/marius-luca-87)
|
||||||
- [mark-monteiro](https://github.com/mark-monteiro)
|
- [mark-monteiro](https://github.com/mark-monteiro)
|
||||||
- [Matt07211](https://github.com/Matt07211)
|
- [Matt07211](https://github.com/Matt07211)
|
||||||
|
- [Maxr1998](https://github.com/Maxr1998)
|
||||||
- [mcarlton00](https://github.com/mcarlton00)
|
- [mcarlton00](https://github.com/mcarlton00)
|
||||||
- [mitchfizz05](https://github.com/mitchfizz05)
|
- [mitchfizz05](https://github.com/mitchfizz05)
|
||||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||||
|
@ -110,7 +112,7 @@
|
||||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||||
- [sparky8251](https://github.com/sparky8251)
|
- [sparky8251](https://github.com/sparky8251)
|
||||||
- [spookbits](https://github.com/spookbits)
|
- [spookbits](https://github.com/spookbits)
|
||||||
- [ssenart] (https://github.com/ssenart)
|
- [ssenart](https://github.com/ssenart)
|
||||||
- [stanionascu](https://github.com/stanionascu)
|
- [stanionascu](https://github.com/stanionascu)
|
||||||
- [stevehayles](https://github.com/stevehayles)
|
- [stevehayles](https://github.com/stevehayles)
|
||||||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||||
|
@ -146,6 +148,7 @@
|
||||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||||
- [skyfrk](https://github.com/skyfrk)
|
- [skyfrk](https://github.com/skyfrk)
|
||||||
- [ianjazz246](https://github.com/ianjazz246)
|
- [ianjazz246](https://github.com/ianjazz246)
|
||||||
|
- [peterspenler](https://github.com/peterspenler)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
@ -210,3 +213,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)
|
||||||
|
|
14
Directory.Build.props
Normal file
14
Directory.Build.props
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<Project>
|
||||||
|
<!-- Sets defaults for all projects in the repo -->
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
32
Dockerfile
32
Dockerfile
|
@ -1,22 +1,14 @@
|
||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=5.0
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=master
|
||||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& npm ci --no-audit \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
FROM debian:bullseye-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,9 +17,6 @@ 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=20.3.2
|
||||||
ARG IGC_VERSION=1.0.5435
|
ARG IGC_VERSION=1.0.5435
|
||||||
|
@ -73,6 +62,19 @@ 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
|
||||||
|
|
||||||
|
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", \
|
||||||
|
|
|
@ -5,27 +5,16 @@
|
||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=5.0
|
||||||
|
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=master
|
||||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& npm ci --no-audit \
|
&& 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:bullseye-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"
|
||||||
|
@ -61,14 +50,25 @@ 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
|
|
||||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
|
||||||
|
|
||||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||||
ENV LC_ALL en_US.UTF-8
|
ENV 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
|
||||||
|
|
||||||
|
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", \
|
||||||
|
|
|
@ -5,26 +5,16 @@
|
||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=5.0
|
||||||
|
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=master
|
||||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& npm ci --no-audit \
|
&& 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-arm64 "-p:DebugSymbols=false;DebugType=none"
|
|
||||||
|
|
||||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||||
FROM arm64v8/debian:buster-slim
|
FROM arm64v8/debian:bullseye-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"
|
||||||
|
@ -50,14 +40,25 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
|
||||||
&& 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
|
|
||||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
|
||||||
|
|
||||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||||
ENV LC_ALL en_US.UTF-8
|
ENV 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-arm64 "-p:DebugSymbols=false;DebugType=none"
|
||||||
|
|
||||||
|
FROM app
|
||||||
|
|
||||||
|
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", \
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.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>
|
||||||
|
|
|
@ -72,7 +72,7 @@ namespace Emby.Dlna.Configuration
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the default user account that the dlna server uses.
|
/// Gets or sets the default user account that the dlna server uses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DefaultUserId { get; set; }
|
public string? DefaultUserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether playTo device profiles should be created.
|
/// Gets or sets a value indicating whether playTo device profiles should be created.
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#nullable enable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using Emby.Dlna.Configuration;
|
using Emby.Dlna.Configuration;
|
||||||
|
|
|
@ -138,7 +138,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
||||||
/// <returns>The <see cref="User"/>.</returns>
|
/// <returns>The <see cref="User"/>.</returns>
|
||||||
private User GetUser(DeviceProfile profile)
|
private User? GetUser(DeviceProfile profile)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(profile.UserId))
|
if (!string.IsNullOrEmpty(profile.UserId))
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
@ -286,21 +288,14 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
/// <returns>The xml feature list.</returns>
|
/// <returns>The xml feature list.</returns>
|
||||||
private static string WriteFeatureListXml()
|
private static string WriteFeatureListXml()
|
||||||
{
|
{
|
||||||
// TODO: clean this up
|
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
||||||
var builder = new StringBuilder();
|
+ "<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">"
|
||||||
|
+ "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"
|
||||||
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
+ "<container id=\"I\" type=\"object.item.imageItem\"/>"
|
||||||
builder.Append("<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">");
|
+ "<container id=\"A\" type=\"object.item.audioItem\"/>"
|
||||||
|
+ "<container id=\"V\" type=\"object.item.videoItem\"/>"
|
||||||
builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">");
|
+ "</Feature>"
|
||||||
builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>");
|
+ "</Features>";
|
||||||
builder.Append("<container id=\"A\" type=\"object.item.audioItem\"/>");
|
|
||||||
builder.Append("<container id=\"V\" type=\"object.item.videoItem\"/>");
|
|
||||||
builder.Append("</Feature>");
|
|
||||||
|
|
||||||
builder.Append("</Features>");
|
|
||||||
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -17,7 +17,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
{
|
{
|
||||||
Item = item;
|
Item = item;
|
||||||
|
|
||||||
if (item is IItemByName && !(item is Folder))
|
if (item is IItemByName && item is not Folder)
|
||||||
{
|
{
|
||||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
|
@ -6,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; }
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -746,7 +748,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"))
|
||||||
{
|
{
|
||||||
|
@ -976,15 +978,28 @@ namespace Emby.Dlna.Didl
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
|
// TODO: Remove these default values
|
||||||
|
var albumArtUrlInfo = GetImageUrl(
|
||||||
|
imageInfo,
|
||||||
|
_profile.MaxAlbumArtWidth ?? 10000,
|
||||||
|
_profile.MaxAlbumArtHeight ?? 10000,
|
||||||
|
"jpg");
|
||||||
|
|
||||||
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
|
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
|
||||||
|
if (!string.IsNullOrEmpty(_profile.AlbumArtPn))
|
||||||
|
{
|
||||||
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
|
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
|
||||||
writer.WriteString(albumartUrlInfo.url);
|
}
|
||||||
|
|
||||||
|
writer.WriteString(albumArtUrlInfo.url);
|
||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
|
|
||||||
// TOOD: Remove these default values
|
// TODO: Remove these default values
|
||||||
var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
|
var iconUrlInfo = GetImageUrl(
|
||||||
|
imageInfo,
|
||||||
|
_profile.MaxIconWidth ?? 48,
|
||||||
|
_profile.MaxIconHeight ?? 48,
|
||||||
|
"jpg");
|
||||||
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
|
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
|
||||||
|
|
||||||
if (!_profile.EnableAlbumArtInDidl)
|
if (!_profile.EnableAlbumArtInDidl)
|
||||||
|
@ -1207,8 +1222,7 @@ namespace Emby.Dlna.Didl
|
||||||
|
|
||||||
if (width.HasValue && height.HasValue)
|
if (width.HasValue && height.HasValue)
|
||||||
{
|
{
|
||||||
var newSize = DrawingUtils.Resize(
|
var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
|
||||||
new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
|
|
||||||
|
|
||||||
width = newSize.Width;
|
width = newSize.Width;
|
||||||
height = newSize.Height;
|
height = newSize.Height;
|
||||||
|
|
|
@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
|
||||||
{
|
{
|
||||||
public class StringWriterWithEncoding : StringWriter
|
public class StringWriterWithEncoding : StringWriter
|
||||||
{
|
{
|
||||||
private readonly Encoding _encoding;
|
private readonly Encoding? _encoding;
|
||||||
|
|
||||||
public StringWriterWithEncoding()
|
public StringWriterWithEncoding()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#nullable enable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#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;
|
||||||
|
@ -12,9 +11,9 @@ 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;
|
||||||
|
@ -94,12 +93,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)
|
||||||
{
|
{
|
||||||
|
@ -109,13 +110,13 @@ namespace Emby.Dlna
|
||||||
var profile = GetProfiles()
|
var profile = GetProfiles()
|
||||||
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
|
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
|
||||||
|
|
||||||
if (profile != null)
|
if (profile == null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
LogUnmatchedProfile(deviceInfo);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LogUnmatchedProfile(deviceInfo);
|
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
|
@ -138,80 +139,45 @@ namespace Emby.Dlna
|
||||||
_logger.LogInformation(builder.ToString());
|
_logger.LogInformation(builder.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
/// <summary>
|
||||||
|
/// Attempts to match a device with a profile.
|
||||||
|
/// Rules:
|
||||||
|
/// - If the profile field has no value, the field matches irregardless of its contents.
|
||||||
|
/// - the profile field can be an exact match, or a reg exp.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
|
||||||
|
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
|
||||||
|
/// <returns><b>True</b> if they match.</returns>
|
||||||
|
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
|
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
|
||||||
{
|
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
|
||||||
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
|
||||||
{
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
|
||||||
return false;
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
|
||||||
}
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
|
||||||
}
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
|
||||||
|
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
|
||||||
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
|
|
||||||
{
|
|
||||||
if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelName))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
|
|
||||||
{
|
|
||||||
if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(pattern))
|
||||||
|
{
|
||||||
|
// In profile identification: An empty pattern matches anything.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
// The profile contains a value, and the device doesn't.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
|
@ -220,7 +186,8 @@ namespace Emby.Dlna
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeviceProfile GetProfile(IHeaderDictionary headers)
|
/// <inheritdoc />
|
||||||
|
public DeviceProfile? GetProfile(IHeaderDictionary headers)
|
||||||
{
|
{
|
||||||
if (headers == null)
|
if (headers == null)
|
||||||
{
|
{
|
||||||
|
@ -228,15 +195,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;
|
||||||
|
@ -286,19 +251,19 @@ namespace Emby.Dlna
|
||||||
return xmlFies
|
return xmlFies
|
||||||
.Select(i => ParseProfileFile(i, type))
|
.Select(i => ParseProfileFile(i, type))
|
||||||
.Where(i => i != null)
|
.Where(i => i != null)
|
||||||
.ToList();
|
.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;
|
||||||
}
|
}
|
||||||
|
@ -326,7 +291,8 @@ namespace Emby.Dlna
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeviceProfile GetProfile(string id)
|
/// <inheritdoc />
|
||||||
|
public DeviceProfile? GetProfile(string id)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(id))
|
if (string.IsNullOrEmpty(id))
|
||||||
{
|
{
|
||||||
|
@ -355,6 +321,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);
|
||||||
|
@ -362,17 +329,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()
|
||||||
|
@ -392,7 +356,8 @@ 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 fileInfo = _fileSystem.GetFileInfo(path);
|
var fileInfo = _fileSystem.GetFileInfo(path);
|
||||||
|
|
||||||
|
@ -413,6 +378,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));
|
||||||
|
@ -430,6 +396,7 @@ namespace Emby.Dlna
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void CreateProfile(DeviceProfile profile)
|
public void CreateProfile(DeviceProfile profile)
|
||||||
{
|
{
|
||||||
profile = ReserializeProfile(profile);
|
profile = ReserializeProfile(profile);
|
||||||
|
@ -445,6 +412,7 @@ namespace Emby.Dlna
|
||||||
SaveProfile(profile, path, DeviceProfileType.User);
|
SaveProfile(profile, path, DeviceProfileType.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void UpdateProfile(DeviceProfile profile)
|
public void UpdateProfile(DeviceProfile profile)
|
||||||
{
|
{
|
||||||
profile = ReserializeProfile(profile);
|
profile = ReserializeProfile(profile);
|
||||||
|
@ -503,9 +471,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();
|
||||||
|
@ -515,6 +485,7 @@ namespace Emby.Dlna
|
||||||
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ImageStream GetIcon(string filename)
|
public ImageStream GetIcon(string filename)
|
||||||
{
|
{
|
||||||
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
||||||
|
@ -532,9 +503,15 @@ namespace Emby.Dlna
|
||||||
|
|
||||||
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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
|
@ -30,10 +30,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" />
|
||||||
|
|
|
@ -6,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>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -49,11 +51,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)
|
||||||
|
@ -101,20 +99,12 @@ 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(_usCulture)) : requestedTimeoutString;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -25,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
|
||||||
{
|
{
|
||||||
|
@ -202,8 +202,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)
|
||||||
{
|
{
|
||||||
|
@ -266,7 +266,12 @@ namespace Emby.Dlna.Main
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
_publisher = new SsdpDevicePublisher(
|
||||||
|
_communicationsServer,
|
||||||
|
_networkManager,
|
||||||
|
MediaBrowser.Common.System.OperatingSystem.Name,
|
||||||
|
Environment.OSVersion.VersionString,
|
||||||
|
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||||
{
|
{
|
||||||
LogFunction = LogMessage,
|
LogFunction = LogMessage,
|
||||||
SupportPnpRootDevice = false
|
SupportPnpRootDevice = false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -368,6 +370,42 @@ namespace Emby.Dlna.PlayTo
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
|
||||||
|
* Without that information, the next track command on the device does not work.
|
||||||
|
*/
|
||||||
|
public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
url = url.Replace("&", "&", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
|
||||||
|
|
||||||
|
var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (command == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dictionary = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "NextURI", url },
|
||||||
|
{ "NextURIMetaData", CreateDidlMeta(metaData) }
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
|
if (service == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unable to find service");
|
||||||
|
}
|
||||||
|
|
||||||
|
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
||||||
|
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
private static string CreateDidlMeta(string value)
|
private static string CreateDidlMeta(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
|
@ -1222,10 +1260,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)
|
||||||
|
@ -1235,27 +1270,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,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
|
@ -6,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; }
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -102,6 +104,22 @@ namespace Emby.Dlna.PlayTo
|
||||||
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||||
|
*/
|
||||||
|
private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
|
||||||
|
{
|
||||||
|
// The current playing item is indeed in the play list and we are not yet at the end of the playlist.
|
||||||
|
var nextItemIndex = currentPlayListItemIndex + 1;
|
||||||
|
var nextItem = _playlist[nextItemIndex];
|
||||||
|
|
||||||
|
// Send the SetNextAvTransport message.
|
||||||
|
await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDeviceUnavailable()
|
private void OnDeviceUnavailable()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -156,6 +174,15 @@ namespace Emby.Dlna.PlayTo
|
||||||
var newItemProgress = GetProgressInfo(streamInfo);
|
var newItemProgress = GetProgressInfo(streamInfo);
|
||||||
|
|
||||||
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||||
|
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
|
||||||
|
if (currentItemIndex >= 0)
|
||||||
|
{
|
||||||
|
_currentPlaylistIndex = currentItemIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -425,6 +452,11 @@ namespace Emby.Dlna.PlayTo
|
||||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||||
|
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -623,6 +655,9 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
await SendNextTrackMessage(index, cancellationToken);
|
||||||
|
|
||||||
var streamInfo = currentitem.StreamInfo;
|
var streamInfo = currentitem.StreamInfo;
|
||||||
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
||||||
{
|
{
|
||||||
|
@ -736,6 +771,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||||
|
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||||
|
|
||||||
if (EnableClientSideSeek(newItem.StreamInfo))
|
if (EnableClientSideSeek(newItem.StreamInfo))
|
||||||
{
|
{
|
||||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
@ -761,6 +800,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||||
|
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||||
|
|
||||||
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
||||||
{
|
{
|
||||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -171,7 +173,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();
|
||||||
|
|
||||||
|
@ -188,7 +192,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||||
|
|
||||||
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
|
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
|
||||||
|
|
||||||
controller = new PlayToController(
|
controller = new PlayToController(
|
||||||
sessionInfo,
|
sessionInfo,
|
||||||
|
|
|
@ -6,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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -46,7 +46,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var serviceAction = new ServiceAction
|
var serviceAction = new ServiceAction
|
||||||
{
|
{
|
||||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
var argumentList = serviceAction.ArgumentList;
|
var argumentList = serviceAction.ArgumentList;
|
||||||
|
@ -68,9 +68,9 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
return new Argument
|
return new Argument
|
||||||
{
|
{
|
||||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||||
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
|
Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
|
||||||
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
|
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,8 +89,8 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
return new StateVariable
|
return new StateVariable
|
||||||
{
|
{
|
||||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||||
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
|
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
|
||||||
AllowedValues = allowedValues
|
AllowedValues = allowedValues
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -166,7 +166,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
|
private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
|
||||||
{
|
{
|
||||||
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
|
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -250,7 +250,8 @@ namespace Emby.Dlna.Server
|
||||||
|
|
||||||
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
|
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
|
||||||
|
|
||||||
return SecurityElement.Escape(url);
|
// TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
|
||||||
|
return SecurityElement.Escape(url) ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<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
|
||||||
|
@ -47,7 +47,7 @@ namespace Emby.Dlna.Service
|
||||||
|
|
||||||
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
|
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
|
||||||
{
|
{
|
||||||
ControlRequestInfo requestInfo = null;
|
ControlRequestInfo? requestInfo = null;
|
||||||
|
|
||||||
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
|
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -151,7 +147,7 @@ namespace Emby.Dlna.Service
|
||||||
|
|
||||||
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
||||||
{
|
{
|
||||||
string namespaceURI = null, localName = null;
|
string? namespaceURI = null, localName = null;
|
||||||
|
|
||||||
await reader.MoveToContentAsync().ConfigureAwait(false);
|
await reader.MoveToContentAsync().ConfigureAwait(false);
|
||||||
await reader.ReadAsync().ConfigureAwait(false);
|
await reader.ReadAsync().ConfigureAwait(false);
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp
|
||||||
{
|
{
|
||||||
Location = e.DiscoveredDevice.DescriptionLocation,
|
Location = e.DiscoveredDevice.DescriptionLocation,
|
||||||
Headers = headers,
|
Headers = headers,
|
||||||
LocalIpAddress = e.LocalIpAddress
|
RemoteIpAddress = e.RemoteIpAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
DeviceDiscoveredInternal?.Invoke(this, args);
|
DeviceDiscoveredInternal?.Invoke(this, args);
|
||||||
|
|
|
@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp
|
||||||
{
|
{
|
||||||
public static class SsdpExtensions
|
public static class SsdpExtensions
|
||||||
{
|
{
|
||||||
public static string GetValue(this XElement container, XName name)
|
public static string? GetValue(this XElement container, XName name)
|
||||||
{
|
{
|
||||||
var node = container.Element(name);
|
var node = container.Element(name);
|
||||||
|
|
||||||
return node?.Value;
|
return node?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetAttributeValue(this XElement container, XName name)
|
public static string? GetAttributeValue(this XElement container, XName name)
|
||||||
{
|
{
|
||||||
var node = container.Attribute(name);
|
var node = container.Attribute(name);
|
||||||
|
|
||||||
return node?.Value;
|
return node?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetDescendantValue(this XElement container, XName name)
|
public static string? GetDescendantValue(this XElement container, XName name)
|
||||||
=> container.Descendants(name).FirstOrDefault()?.Value;
|
=> container.Descendants(name).FirstOrDefault()?.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,7 @@
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -30,8 +29,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>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Audio
|
namespace Emby.Naming.Audio
|
||||||
{
|
{
|
||||||
|
@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
|
||||||
/// <returns>True if file at path is audio file.</returns>
|
/// <returns>True if file at path is audio file.</returns>
|
||||||
public static bool IsAudioFile(string path, NamingOptions options)
|
public static bool IsAudioFile(string path, NamingOptions options)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
{
|
{
|
||||||
|
|
|
@ -137,7 +137,7 @@ 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|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
@"[ _\,\.\(\)\[\]\-](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|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
||||||
@"(\[.*\])"
|
@"(\[.*\])"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -277,14 +277,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 +305,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 +368,6 @@ namespace Emby.Naming.Common
|
||||||
IsOptimistic = true,
|
IsOptimistic = true,
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
// "Episode 16", "Episode 16 - Title"
|
|
||||||
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
|
|
||||||
{
|
|
||||||
IsOptimistic = true,
|
|
||||||
IsNamed = true
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeWithoutSeasonExpressions = new[]
|
EpisodeWithoutSeasonExpressions = new[]
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -9,12 +9,11 @@
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
<Nullable>enable</Nullable>
|
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||||
|
@ -23,11 +22,12 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\SharedVersion.cs" />
|
<Compile Include="../SharedVersion.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
|
||||||
|
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -49,8 +49,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>
|
||||||
|
|
|
@ -16,7 +16,7 @@ namespace Emby.Naming.TV
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
|
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
|
||||||
public EpisodeResolver(NamingOptions options)
|
public EpisodeResolver(NamingOptions options)
|
||||||
{
|
{
|
||||||
_options = options;
|
_options = options;
|
||||||
|
@ -62,12 +62,16 @@ namespace Emby.Naming.TV
|
||||||
container = extension.TrimStart('.');
|
container = extension.TrimStart('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
var flags = new FlagParser(_options).GetFlags(path);
|
var format3DResult = Format3DParser.Parse(path, _options);
|
||||||
var format3DResult = new Format3DParser(_options).Parse(flags);
|
|
||||||
|
|
||||||
var parsingResult = new EpisodePathParser(_options)
|
var parsingResult = new EpisodePathParser(_options)
|
||||||
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
||||||
|
|
||||||
|
if (!parsingResult.Success && !isStub)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return new EpisodeInfo(path)
|
return new EpisodeInfo(path)
|
||||||
{
|
{
|
||||||
Container = container,
|
Container = container,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Emby.Naming.Audio;
|
using Emby.Naming.Audio;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
@ -29,36 +28,33 @@ namespace Emby.Naming.Video
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||||
public ExtraResult GetExtraInfo(string path)
|
public ExtraResult GetExtraInfo(string path)
|
||||||
{
|
|
||||||
return _options.VideoExtraRules
|
|
||||||
.Select(i => GetExtraInfo(path, i))
|
|
||||||
.FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ExtraResult GetExtraInfo(string path, ExtraRule rule)
|
|
||||||
{
|
{
|
||||||
var result = new ExtraResult();
|
var result = new ExtraResult();
|
||||||
|
|
||||||
|
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
|
||||||
|
{
|
||||||
|
var rule = _options.VideoExtraRules[i];
|
||||||
if (rule.MediaType == MediaType.Audio)
|
if (rule.MediaType == MediaType.Audio)
|
||||||
{
|
{
|
||||||
if (!AudioFileParser.IsAudioFile(path, _options))
|
if (!AudioFileParser.IsAudioFile(path, _options))
|
||||||
{
|
{
|
||||||
return result;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (rule.MediaType == MediaType.Video)
|
else if (rule.MediaType == MediaType.Video)
|
||||||
{
|
{
|
||||||
if (!new VideoResolver(_options).IsVideoFile(path))
|
if (!VideoResolver.IsVideoFile(path, _options))
|
||||||
{
|
{
|
||||||
return result;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pathSpan = path.AsSpan();
|
||||||
if (rule.RuleType == ExtraRuleType.Filename)
|
if (rule.RuleType == ExtraRuleType.Filename)
|
||||||
{
|
{
|
||||||
var filename = Path.GetFileNameWithoutExtension(path);
|
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
||||||
|
|
||||||
if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
|
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
result.ExtraType = rule.ExtraType;
|
result.ExtraType = rule.ExtraType;
|
||||||
result.Rule = rule;
|
result.Rule = rule;
|
||||||
|
@ -66,9 +62,9 @@ namespace Emby.Naming.Video
|
||||||
}
|
}
|
||||||
else if (rule.RuleType == ExtraRuleType.Suffix)
|
else if (rule.RuleType == ExtraRuleType.Suffix)
|
||||||
{
|
{
|
||||||
var filename = Path.GetFileNameWithoutExtension(path);
|
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
||||||
|
|
||||||
if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
|
if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
result.ExtraType = rule.ExtraType;
|
result.ExtraType = rule.ExtraType;
|
||||||
result.Rule = rule;
|
result.Rule = rule;
|
||||||
|
@ -88,14 +84,20 @@ namespace Emby.Naming.Video
|
||||||
}
|
}
|
||||||
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
||||||
{
|
{
|
||||||
var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
|
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
|
||||||
if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
|
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
result.ExtraType = rule.ExtraType;
|
result.ExtraType = rule.ExtraType;
|
||||||
result.Rule = rule;
|
result.Rule = rule;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.ExtraType != null)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using Emby.Naming.Common;
|
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Parses list of flags from filename based on delimiters.
|
|
||||||
/// </summary>
|
|
||||||
public class FlagParser
|
|
||||||
{
|
|
||||||
private readonly NamingOptions _options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="FlagParser"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param>
|
|
||||||
public FlagParser(NamingOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Path to file.</param>
|
|
||||||
/// <returns>List of found flags.</returns>
|
|
||||||
public string[] GetFlags(string path)
|
|
||||||
{
|
|
||||||
return GetFlags(path, _options.VideoFlagDelimiters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses flags from filename based on delimiters.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Path to file.</param>
|
|
||||||
/// <param name="delimiters">Delimiters used to extract flags.</param>
|
|
||||||
/// <returns>List of found flags.</returns>
|
|
||||||
public string[] GetFlags(string path, char[] delimiters)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
{
|
|
||||||
return Array.Empty<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
|
|
||||||
|
|
||||||
var file = Path.GetFileName(path);
|
|
||||||
|
|
||||||
return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +1,37 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parste 3D format related flags.
|
/// Parse 3D format related flags.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Format3DParser
|
public static class Format3DParser
|
||||||
{
|
{
|
||||||
private readonly NamingOptions _options;
|
// Static default result to save on allocation costs.
|
||||||
|
private static readonly Format3DResult _defaultResult = new (false, null);
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="Format3DParser"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param>
|
|
||||||
public Format3DParser(NamingOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse 3D format related flags.
|
/// Parse 3D format related flags.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
|
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
|
||||||
public Format3DResult Parse(string path)
|
public static Format3DResult Parse(ReadOnlySpan<char> path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
int oldLen = _options.VideoFlagDelimiters.Length;
|
int oldLen = namingOptions.VideoFlagDelimiters.Length;
|
||||||
var delimiters = new char[oldLen + 1];
|
Span<char> delimiters = stackalloc char[oldLen + 1];
|
||||||
_options.VideoFlagDelimiters.CopyTo(delimiters, 0);
|
namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
|
||||||
delimiters[oldLen] = ' ';
|
delimiters[oldLen] = ' ';
|
||||||
|
|
||||||
return Parse(new FlagParser(_options).GetFlags(path, delimiters));
|
return Parse(path, delimiters, namingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Format3DResult Parse(string[] videoFlags)
|
private static Format3DResult Parse(ReadOnlySpan<char> path, ReadOnlySpan<char> delimiters, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
foreach (var rule in _options.Format3DRules)
|
foreach (var rule in namingOptions.Format3DRules)
|
||||||
{
|
{
|
||||||
var result = Parse(videoFlags, rule);
|
var result = Parse(path, rule, delimiters);
|
||||||
|
|
||||||
if (result.Is3D)
|
if (result.Is3D)
|
||||||
{
|
{
|
||||||
|
@ -47,51 +39,43 @@ namespace Emby.Naming.Video
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Format3DResult();
|
return _defaultResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Format3DResult Parse(string[] videoFlags, Format3DRule rule)
|
private static Format3DResult Parse(ReadOnlySpan<char> path, Format3DRule rule, ReadOnlySpan<char> delimiters)
|
||||||
{
|
{
|
||||||
var result = new Format3DResult();
|
bool is3D = false;
|
||||||
|
string? format3D = null;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(rule.PrecedingToken))
|
// If there's no preceding token we just consider it found
|
||||||
|
var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken);
|
||||||
|
while (path.Length > 0)
|
||||||
{
|
{
|
||||||
result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
|
var index = path.IndexOfAny(delimiters);
|
||||||
result.Is3D = !string.IsNullOrEmpty(result.Format3D);
|
if (index == -1)
|
||||||
|
|
||||||
if (result.Is3D)
|
|
||||||
{
|
{
|
||||||
result.Tokens.Add(rule.Token);
|
index = path.Length - 1;
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var foundPrefix = false;
|
|
||||||
string? format = null;
|
|
||||||
|
|
||||||
foreach (var flag in videoFlags)
|
|
||||||
{
|
|
||||||
if (foundPrefix)
|
|
||||||
{
|
|
||||||
result.Tokens.Add(rule.PrecedingToken);
|
|
||||||
|
|
||||||
if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
format = flag;
|
|
||||||
result.Tokens.Add(rule.Token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentSlice = path[..index];
|
||||||
|
path = path[(index + 1)..];
|
||||||
|
|
||||||
|
if (!foundPrefix)
|
||||||
|
{
|
||||||
|
foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (is3D)
|
||||||
|
{
|
||||||
|
format3D = rule.Token;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
|
return is3D ? new Format3DResult(true, format3D) : _defaultResult;
|
||||||
result.Format3D = format;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -10,27 +8,24 @@ namespace Emby.Naming.Video
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
|
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Format3DResult()
|
/// <param name="is3D">A value indicating whether the parsed string contains 3D tokens.</param>
|
||||||
|
/// <param name="format3D">The 3D format. Value might be null if [is3D] is <c>false</c>.</param>
|
||||||
|
public Format3DResult(bool is3D, string? format3D)
|
||||||
{
|
{
|
||||||
Tokens = new List<string>();
|
Is3D = is3D;
|
||||||
|
Format3D = format3D;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether [is3 d].
|
/// Gets a value indicating whether [is3 d].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
|
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
|
||||||
public bool Is3D { get; set; }
|
public bool Is3D { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the format3 d.
|
/// Gets the format3 d.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The format3 d.</value>
|
/// <value>The format3 d.</value>
|
||||||
public string? Format3D { get; set; }
|
public string? Format3D { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the tokens.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The tokens.</value>
|
|
||||||
public List<string> Tokens { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,10 +85,8 @@ namespace Emby.Naming.Video
|
||||||
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
||||||
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
|
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
|
||||||
{
|
{
|
||||||
var resolver = new VideoResolver(_options);
|
|
||||||
|
|
||||||
var list = files
|
var list = files
|
||||||
.Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))
|
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
|
||||||
.OrderBy(i => i.FullName)
|
.OrderBy(i => i.FullName)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
|
@ -106,9 +107,9 @@ namespace Emby.Naming.Video
|
||||||
/// Gets the file name without extension.
|
/// Gets the file name without extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The file name without extension.</value>
|
/// <value>The file name without extension.</value>
|
||||||
public string FileNameWithoutExtension => !IsDirectory
|
public ReadOnlySpan<char> FileNameWithoutExtension => !IsDirectory
|
||||||
? System.IO.Path.GetFileNameWithoutExtension(Path)
|
? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan())
|
||||||
: System.IO.Path.GetFileName(Path);
|
: System.IO.Path.GetFileName(Path.AsSpan());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|
|
@ -12,31 +12,19 @@ namespace Emby.Naming.Video
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves alternative versions and extras from list of video files.
|
/// Resolves alternative versions and extras from list of video files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class VideoListResolver
|
public static class VideoListResolver
|
||||||
{
|
{
|
||||||
private readonly NamingOptions _options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="VideoListResolver"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
|
|
||||||
public VideoListResolver(NamingOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves alternative versions and extras from list of video files.
|
/// Resolves alternative versions and extras from list of video files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of related video files.</param>
|
/// <param name="files">List of related video files.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
||||||
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
||||||
public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
|
public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
|
||||||
{
|
{
|
||||||
var videoResolver = new VideoResolver(_options);
|
|
||||||
|
|
||||||
var videoInfos = files
|
var videoInfos = files
|
||||||
.Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
|
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
|
||||||
.OfType<VideoFileInfo>()
|
.OfType<VideoFileInfo>()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
@ -46,7 +34,7 @@ namespace Emby.Naming.Video
|
||||||
.Where(i => i.ExtraType == null)
|
.Where(i => i.ExtraType == null)
|
||||||
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
||||||
|
|
||||||
var stackResult = new StackResolver(_options)
|
var stackResult = new StackResolver(namingOptions)
|
||||||
.Resolve(nonExtras).ToList();
|
.Resolve(nonExtras).ToList();
|
||||||
|
|
||||||
var remainingFiles = videoInfos
|
var remainingFiles = videoInfos
|
||||||
|
@ -59,23 +47,17 @@ 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))
|
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
|
||||||
.OfType<VideoFileInfo>()
|
.OfType<VideoFileInfo>()
|
||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
info.Year = info.Files[0].Year;
|
info.Year = info.Files[0].Year;
|
||||||
|
|
||||||
var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
|
var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
|
||||||
|
|
||||||
var extras = GetExtras(remainingFiles, extraBaseNames);
|
|
||||||
|
|
||||||
if (extras.Count > 0)
|
if (extras.Count > 0)
|
||||||
{
|
{
|
||||||
remainingFiles = remainingFiles
|
|
||||||
.Except(extras)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
info.Extras = extras;
|
info.Extras = extras;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,15 +70,12 @@ namespace Emby.Naming.Video
|
||||||
|
|
||||||
foreach (var media in standaloneMedia)
|
foreach (var media in standaloneMedia)
|
||||||
{
|
{
|
||||||
var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } };
|
var info = new VideoInfo(media.Name) { Files = new[] { media } };
|
||||||
|
|
||||||
info.Year = info.Files[0].Year;
|
info.Year = info.Files[0].Year;
|
||||||
|
|
||||||
var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
|
remainingFiles.Remove(media);
|
||||||
|
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
|
||||||
remainingFiles = remainingFiles
|
|
||||||
.Except(extras.Concat(new[] { media }))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
info.Extras = extras;
|
info.Extras = extras;
|
||||||
|
|
||||||
|
@ -105,8 +84,7 @@ namespace Emby.Naming.Video
|
||||||
|
|
||||||
if (supportMultiVersion)
|
if (supportMultiVersion)
|
||||||
{
|
{
|
||||||
list = GetVideosGroupedByVersion(list)
|
list = GetVideosGroupedByVersion(list, namingOptions);
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's only one resolved video, use the folder name as well to find extras
|
// If there's only one resolved video, use the folder name as well to find extras
|
||||||
|
@ -114,19 +92,14 @@ namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
var info = list[0];
|
var info = list[0];
|
||||||
var videoPath = list[0].Files[0].Path;
|
var videoPath = list[0].Files[0].Path;
|
||||||
var parentPath = Path.GetDirectoryName(videoPath);
|
var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(parentPath))
|
if (!parentPath.IsEmpty)
|
||||||
{
|
{
|
||||||
var folderName = Path.GetFileName(parentPath);
|
var folderName = Path.GetFileName(parentPath);
|
||||||
if (!string.IsNullOrEmpty(folderName))
|
if (!folderName.IsEmpty)
|
||||||
{
|
{
|
||||||
var extras = GetExtras(remainingFiles, new List<string> { folderName });
|
var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
|
||||||
|
|
||||||
remainingFiles = remainingFiles
|
|
||||||
.Except(extras)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
extras.AddRange(info.Extras);
|
extras.AddRange(info.Extras);
|
||||||
info.Extras = extras;
|
info.Extras = extras;
|
||||||
}
|
}
|
||||||
|
@ -164,96 +137,168 @@ namespace Emby.Naming.Video
|
||||||
// Whatever files are left, just add them
|
// Whatever files are left, just add them
|
||||||
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
|
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
|
||||||
{
|
{
|
||||||
Files = new List<VideoFileInfo> { i },
|
Files = new[] { i },
|
||||||
Year = i.Year
|
Year = i.Year
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
|
private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
if (videos.Count == 0)
|
if (videos.Count == 0)
|
||||||
{
|
{
|
||||||
return videos;
|
return videos;
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<VideoInfo>();
|
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
|
||||||
|
|
||||||
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path));
|
if (folderName.Length <= 1 || !HaveSameYear(videos))
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(folderName)
|
|
||||||
&& folderName.Length > 1
|
|
||||||
&& videos.All(i => i.Files.Count == 1
|
|
||||||
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path))
|
|
||||||
&& HaveSameYear(videos))
|
|
||||||
{
|
{
|
||||||
var ordered = videos.OrderBy(i => i.Name).ToList();
|
return videos;
|
||||||
|
}
|
||||||
|
|
||||||
list.Add(ordered[0]);
|
// Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
|
||||||
|
for (var i = 0; i < videos.Count; i++)
|
||||||
|
{
|
||||||
|
var video = videos[i];
|
||||||
|
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
|
||||||
|
{
|
||||||
|
return videos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var alternateVersionsLen = ordered.Count - 1;
|
// The list is created and overwritten in the caller, so we are allowed to do in-place sorting
|
||||||
|
videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
var list = new List<VideoInfo>
|
||||||
|
{
|
||||||
|
videos[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
var alternateVersionsLen = videos.Count - 1;
|
||||||
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
var 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++)
|
||||||
{
|
{
|
||||||
alternateVersions[i] = ordered[i + 1].Files[0];
|
var video = videos[i + 1];
|
||||||
|
alternateVersions[i] = video.Files[0];
|
||||||
|
extras.AddRange(video.Extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
list[0].AlternateVersions = alternateVersions;
|
list[0].AlternateVersions = alternateVersions;
|
||||||
list[0].Name = folderName;
|
list[0].Name = folderName.ToString();
|
||||||
var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
|
|
||||||
extras.AddRange(list[0].Extras);
|
|
||||||
list[0].Extras = extras;
|
list[0].Extras = extras;
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
return videos;
|
private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
|
||||||
|
{
|
||||||
|
if (videos.Count == 1)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HaveSameYear(List<VideoInfo> videos)
|
var firstYear = videos[0].Year ?? -1;
|
||||||
|
for (var i = 1; i < videos.Count; i++)
|
||||||
{
|
{
|
||||||
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
|
if ((videos[i].Year ?? -1) != firstYear)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
|
return true;
|
||||||
{
|
|
||||||
string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
|
|
||||||
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// Remove the folder name before cleaning as we don't care about cleaning that part
|
|
||||||
if (folderName.Length <= testFilename.Length)
|
|
||||||
{
|
|
||||||
testFilename = testFilename.Substring(folderName.Length).Trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
|
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
|
||||||
|
{
|
||||||
|
var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
|
||||||
|
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
testFilename = cleanName.Trim().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The CleanStringParser should have removed common keywords etc.
|
|
||||||
return string.IsNullOrEmpty(testFilename)
|
|
||||||
|| testFilename[0] == '-'
|
|
||||||
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames)
|
// Remove the folder name before cleaning as we don't care about cleaning that part
|
||||||
|
if (folderName.Length <= testFilename.Length)
|
||||||
{
|
{
|
||||||
foreach (var name in baseNames.ToList())
|
testFilename = testFilename[folderName.Length..].Trim();
|
||||||
{
|
|
||||||
var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
|
|
||||||
baseNames.Add(trimmedName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return remainingFiles
|
// There are no span overloads for regex unfortunately
|
||||||
.Where(i => i.ExtraType != null)
|
var tmpTestFilename = testFilename.ToString();
|
||||||
.Where(i => baseNames.Any(b =>
|
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
|
||||||
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
|
{
|
||||||
.ToList();
|
tmpTestFilename = cleanName.Trim().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CleanStringParser should have removed common keywords etc.
|
||||||
|
return string.IsNullOrEmpty(tmpTestFilename)
|
||||||
|
|| testFilename[0] == '-'
|
||||||
|
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
|
||||||
|
{
|
||||||
|
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
|
||||||
|
{
|
||||||
|
if (baseName.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="remainingFiles">The list of remaining filenames.</param>
|
||||||
|
/// <param name="baseName">The base name to use for the comparison.</param>
|
||||||
|
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
||||||
|
/// <returns>A list of video extras for [baseName].</returns>
|
||||||
|
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
|
||||||
|
{
|
||||||
|
return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="remainingFiles">The list of remaining filenames.</param>
|
||||||
|
/// <param name="firstBaseName">The first base name to use for the comparison.</param>
|
||||||
|
/// <param name="secondBaseName">The second base name to use for the comparison.</param>
|
||||||
|
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
||||||
|
/// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
|
||||||
|
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
|
||||||
|
{
|
||||||
|
var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
|
||||||
|
var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
|
||||||
|
|
||||||
|
var result = new List<VideoFileInfo>();
|
||||||
|
for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
|
||||||
|
{
|
||||||
|
var file = remainingFiles[pos];
|
||||||
|
if (file.ExtraType == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filename = file.FileNameWithoutExtension;
|
||||||
|
if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
|
||||||
|
|| StartsWith(filename, secondBaseName, trimmedSecondBaseName))
|
||||||
|
{
|
||||||
|
result.Add(file);
|
||||||
|
remainingFiles.RemoveAt(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +1,36 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves <see cref="VideoFileInfo"/> from file path.
|
/// Resolves <see cref="VideoFileInfo"/> from file path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class VideoResolver
|
public static class VideoResolver
|
||||||
{
|
{
|
||||||
private readonly NamingOptions _options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="VideoResolver"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
|
|
||||||
/// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param>
|
|
||||||
public VideoResolver(NamingOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the directory.
|
/// Resolves the directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
public VideoFileInfo? ResolveDirectory(string? path)
|
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
return Resolve(path, true);
|
return Resolve(path, true, namingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the file.
|
/// Resolves the file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
public VideoFileInfo? ResolveFile(string? path)
|
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
return Resolve(path, false);
|
return Resolve(path, false, namingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -48,10 +38,11 @@ namespace Emby.Naming.Video
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
|
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
|
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
|
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
|
||||||
public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true)
|
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
{
|
{
|
||||||
|
@ -59,18 +50,18 @@ namespace Emby.Naming.Video
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isStub = false;
|
bool isStub = false;
|
||||||
string? container = null;
|
ReadOnlySpan<char> container = ReadOnlySpan<char>.Empty;
|
||||||
string? stubType = null;
|
string? stubType = null;
|
||||||
|
|
||||||
if (!isDirectory)
|
if (!isDirectory)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
|
|
||||||
// Check supported extensions
|
// Check supported extensions
|
||||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!namingOptions.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, namingOptions, out stubType))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -81,25 +72,22 @@ namespace Emby.Naming.Video
|
||||||
container = extension.TrimStart('.');
|
container = extension.TrimStart('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
var flags = new FlagParser(_options).GetFlags(path);
|
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
||||||
var format3DResult = new Format3DParser(_options).Parse(flags);
|
|
||||||
|
|
||||||
var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
|
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
|
||||||
|
|
||||||
var name = isDirectory
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
? Path.GetFileName(path)
|
|
||||||
: Path.GetFileNameWithoutExtension(path);
|
|
||||||
|
|
||||||
int? year = null;
|
int? year = null;
|
||||||
|
|
||||||
if (parseName)
|
if (parseName)
|
||||||
{
|
{
|
||||||
var cleanDateTimeResult = CleanDateTime(name);
|
var cleanDateTimeResult = CleanDateTime(name, namingOptions);
|
||||||
name = cleanDateTimeResult.Name;
|
name = cleanDateTimeResult.Name;
|
||||||
year = cleanDateTimeResult.Year;
|
year = cleanDateTimeResult.Year;
|
||||||
|
|
||||||
if (extraResult.ExtraType == null
|
if (extraResult.ExtraType == null
|
||||||
&& TryCleanString(name, out ReadOnlySpan<char> newName))
|
&& TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
|
||||||
{
|
{
|
||||||
name = newName.ToString();
|
name = newName.ToString();
|
||||||
}
|
}
|
||||||
|
@ -107,7 +95,7 @@ namespace Emby.Naming.Video
|
||||||
|
|
||||||
return new VideoFileInfo(
|
return new VideoFileInfo(
|
||||||
path: path,
|
path: path,
|
||||||
container: container,
|
container: container.IsEmpty ? null : container.ToString(),
|
||||||
isStub: isStub,
|
isStub: isStub,
|
||||||
name: name,
|
name: name,
|
||||||
year: year,
|
year: year,
|
||||||
|
@ -123,43 +111,47 @@ namespace Emby.Naming.Video
|
||||||
/// Determines if path is video file based on extension.
|
/// Determines if path is video file based on extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>True if is video file.</returns>
|
/// <returns>True if is video file.</returns>
|
||||||
public bool IsVideoFile(string path)
|
public static bool IsVideoFile(string path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if path is video file stub based on extension.
|
/// Determines if path is video file stub based on extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>True if is video file stub.</returns>
|
/// <returns>True if is video file stub.</returns>
|
||||||
public bool IsStubFile(string path)
|
public static bool IsStubFile(string path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to clean name of clutter.
|
/// Tries to clean name of clutter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">Raw name.</param>
|
/// <param name="name">Raw name.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="newName">Clean name.</param>
|
/// <param name="newName">Clean name.</param>
|
||||||
/// <returns>True if cleaning of name was successful.</returns>
|
/// <returns>True if cleaning of name was successful.</returns>
|
||||||
public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
|
public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
|
||||||
{
|
{
|
||||||
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
|
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to get name and year from raw name.
|
/// Tries to get name and year from raw name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">Raw name.</param>
|
/// <param name="name">Raw name.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
|
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
|
||||||
public CleanDateTimeResult CleanDateTime(string name)
|
public static CleanDateTimeResult CleanDateTime(string name, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
|
return CleanDateTimeParser.Clean(name, namingOptions.CleanDateTimeRegexes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,6 @@
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.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>
|
||||||
|
|
|
@ -77,7 +77,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;
|
||||||
|
@ -132,25 +131,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 +305,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;
|
||||||
|
|
|
@ -22,10 +22,6 @@
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.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-->
|
||||||
|
|
|
@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
CachePath = cacheDirectoryPath;
|
CachePath = cacheDirectoryPath;
|
||||||
WebPath = webDirectoryPath;
|
WebPath = webDirectoryPath;
|
||||||
|
|
||||||
DataPath = Path.Combine(ProgramDataPath, "data");
|
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
/// Gets the folder path to the data directory.
|
/// Gets the folder path to the data directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The data directory.</value>
|
/// <value>The data directory.</value>
|
||||||
public string DataPath
|
public string DataPath => _dataPath;
|
||||||
{
|
|
||||||
get => _dataPath;
|
|
||||||
private set => _dataPath = Directory.CreateDirectory(value).FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string VirtualDataPath => "%AppDataPath%";
|
public string VirtualDataPath => "%AppDataPath%";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The _configuration sync lock.
|
||||||
|
/// </summary>
|
||||||
|
private readonly object _configurationSyncLock = new object();
|
||||||
|
|
||||||
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
||||||
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
||||||
|
|
||||||
|
@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool _configurationLoaded;
|
private bool _configurationLoaded;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _configuration sync lock.
|
|
||||||
/// </summary>
|
|
||||||
private readonly object _configurationSyncLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _configuration.
|
/// The _configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -297,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public object GetConfiguration(string key)
|
public object GetConfiguration(string key)
|
||||||
{
|
{
|
||||||
return _configurations.GetOrAdd(key, k =>
|
return _configurations.GetOrAdd(
|
||||||
|
key,
|
||||||
|
(k, configurationManager) =>
|
||||||
{
|
{
|
||||||
var file = GetConfigurationFile(key);
|
var file = configurationManager.GetConfigurationFile(k);
|
||||||
|
|
||||||
var configurationInfo = _configurationStores
|
var configurationInfo = Array.Find(
|
||||||
.FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
|
configurationManager._configurationStores,
|
||||||
|
i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (configurationInfo == null)
|
if (configurationInfo == null)
|
||||||
{
|
{
|
||||||
throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
|
throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var configurationType = configurationInfo.ConfigurationType;
|
var configurationType = configurationInfo.ConfigurationType;
|
||||||
|
|
||||||
lock (_configurationSyncLock)
|
lock (configurationManager._configurationSyncLock)
|
||||||
{
|
{
|
||||||
return LoadConfiguration(file, configurationType);
|
return configurationManager.LoadConfiguration(file, configurationType);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private object LoadConfiguration(string path, Type configurationType)
|
private object LoadConfiguration(string path, Type configurationType)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -35,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
|
// Note: CreateInstance returns null for Nullable<T>, e.g. CreateInstance(typeof(int?)) returns null.
|
||||||
|
configuration = Activator.CreateInstance(type)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -36,7 +38,6 @@ using Emby.Server.Implementations.Playlists;
|
||||||
using Emby.Server.Implementations.Plugins;
|
using Emby.Server.Implementations.Plugins;
|
||||||
using Emby.Server.Implementations.QuickConnect;
|
using Emby.Server.Implementations.QuickConnect;
|
||||||
using Emby.Server.Implementations.ScheduledTasks;
|
using Emby.Server.Implementations.ScheduledTasks;
|
||||||
using Emby.Server.Implementations.Security;
|
|
||||||
using Emby.Server.Implementations.Serialization;
|
using Emby.Server.Implementations.Serialization;
|
||||||
using Emby.Server.Implementations.Session;
|
using Emby.Server.Implementations.Session;
|
||||||
using Emby.Server.Implementations.SyncPlay;
|
using Emby.Server.Implementations.SyncPlay;
|
||||||
|
@ -57,7 +58,6 @@ using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Chapters;
|
using MediaBrowser.Controller.Chapters;
|
||||||
using MediaBrowser.Controller.Collections;
|
using MediaBrowser.Controller.Collections;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Devices;
|
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
|
@ -73,7 +73,6 @@ using MediaBrowser.Controller.Plugins;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Controller.QuickConnect;
|
using MediaBrowser.Controller.QuickConnect;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
using MediaBrowser.Controller.Security;
|
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Controller.Sorting;
|
using MediaBrowser.Controller.Sorting;
|
||||||
using MediaBrowser.Controller.Subtitles;
|
using MediaBrowser.Controller.Subtitles;
|
||||||
|
@ -101,7 +100,6 @@ using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Prometheus.DotNetRuntime;
|
using Prometheus.DotNetRuntime;
|
||||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
|
||||||
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
|
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations
|
namespace Emby.Server.Implementations
|
||||||
|
@ -116,6 +114,11 @@ namespace Emby.Server.Implementations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The disposable parts.
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystemManager;
|
private readonly IFileSystem _fileSystemManager;
|
||||||
private readonly IConfiguration _startupConfig;
|
private readonly IConfiguration _startupConfig;
|
||||||
private readonly IXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
|
@ -127,110 +130,15 @@ namespace Emby.Server.Implementations
|
||||||
private ISessionManager _sessionManager;
|
private ISessionManager _sessionManager;
|
||||||
private string[] _urlPrefixes;
|
private string[] _urlPrefixes;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether this instance can self restart.
|
|
||||||
/// </summary>
|
|
||||||
public bool CanSelfRestart => _startupOptions.RestartPath != null;
|
|
||||||
|
|
||||||
public bool CoreStartupHasCompleted { get; private set; }
|
|
||||||
|
|
||||||
public virtual bool CanLaunchWebBrowser
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!Environment.UserInteractive)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_startupOptions.IsService)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OperatingSystem.Id == OperatingSystemId.Windows
|
|
||||||
|| OperatingSystem.Id == OperatingSystemId.Darwin)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
|
||||||
/// </summary>
|
|
||||||
public INetworkManager NetManager { get; internal set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when [has pending restart changed].
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler HasPendingRestartChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
|
||||||
/// </summary>
|
|
||||||
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
|
|
||||||
public bool HasPendingRestart { get; private set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsShuttingDown { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the logger.
|
|
||||||
/// </summary>
|
|
||||||
protected ILogger<ApplicationHost> Logger { get; }
|
|
||||||
|
|
||||||
protected IServiceCollection ServiceCollection { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the logger factory.
|
|
||||||
/// </summary>
|
|
||||||
protected ILoggerFactory LoggerFactory { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the application paths.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The application paths.</value>
|
|
||||||
protected IServerApplicationPaths ApplicationPaths { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets all concrete types.
|
/// Gets or sets all concrete types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>All concrete types.</value>
|
/// <value>All concrete types.</value>
|
||||||
private Type[] _allConcreteTypes;
|
private Type[] _allConcreteTypes;
|
||||||
|
|
||||||
/// <summary>
|
private DeviceId _deviceId;
|
||||||
/// The disposable parts.
|
|
||||||
/// </summary>
|
|
||||||
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
|
||||||
|
|
||||||
/// <summary>
|
private bool _disposed = false;
|
||||||
/// Gets or sets the configuration manager.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The configuration manager.</value>
|
|
||||||
public ServerConfigurationManager ConfigurationManager { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the service provider.
|
|
||||||
/// </summary>
|
|
||||||
public IServiceProvider ServiceProvider { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the http port for the webhost.
|
|
||||||
/// </summary>
|
|
||||||
public int HttpPort { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the https port for the webhost.
|
|
||||||
/// </summary>
|
|
||||||
public int HttpsPort { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the value of the PublishedServerUrl setting.
|
|
||||||
/// </summary>
|
|
||||||
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
||||||
|
@ -273,6 +181,143 @@ namespace Emby.Server.Implementations
|
||||||
ApplicationVersion);
|
ApplicationVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when [has pending restart changed].
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler HasPendingRestartChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this instance can self restart.
|
||||||
|
/// </summary>
|
||||||
|
public bool CanSelfRestart => _startupOptions.RestartPath != null;
|
||||||
|
|
||||||
|
public bool CoreStartupHasCompleted { get; private set; }
|
||||||
|
|
||||||
|
public virtual bool CanLaunchWebBrowser
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!Environment.UserInteractive)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_startupOptions.IsService)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
||||||
|
/// </summary>
|
||||||
|
public INetworkManager NetManager { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
||||||
|
/// </summary>
|
||||||
|
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
|
||||||
|
public bool HasPendingRestart { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsShuttingDown { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the logger.
|
||||||
|
/// </summary>
|
||||||
|
protected ILogger<ApplicationHost> Logger { get; }
|
||||||
|
|
||||||
|
protected IServiceCollection ServiceCollection { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the logger factory.
|
||||||
|
/// </summary>
|
||||||
|
protected ILoggerFactory LoggerFactory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the application paths.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The application paths.</value>
|
||||||
|
protected IServerApplicationPaths ApplicationPaths { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the configuration manager.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The configuration manager.</value>
|
||||||
|
public ServerConfigurationManager ConfigurationManager { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the service provider.
|
||||||
|
/// </summary>
|
||||||
|
public IServiceProvider ServiceProvider { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the http port for the webhost.
|
||||||
|
/// </summary>
|
||||||
|
public int HttpPort { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the https port for the webhost.
|
||||||
|
/// </summary>
|
||||||
|
public int HttpsPort { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of the PublishedServerUrl setting.
|
||||||
|
/// </summary>
|
||||||
|
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Version ApplicationVersion { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string ApplicationVersionString { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current application user agent.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The application user agent.</value>
|
||||||
|
public string ApplicationUserAgent { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the email address for use within a comment section of a user agent field.
|
||||||
|
/// Presently used to provide contact information to MusicBrainz service.
|
||||||
|
/// </summary>
|
||||||
|
public string ApplicationUserAgentAddress => "team@jellyfin.org";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current application name.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The application name.</value>
|
||||||
|
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
|
||||||
|
|
||||||
|
public string SystemId
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
|
||||||
|
|
||||||
|
return _deviceId.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Name => ApplicationProductName;
|
||||||
|
|
||||||
|
private CertificateInfo CertificateInfo { get; set; }
|
||||||
|
|
||||||
|
public X509Certificate2 Certificate { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
||||||
|
|
||||||
|
public string FriendlyName =>
|
||||||
|
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
|
||||||
|
? Environment.MachineName
|
||||||
|
: ConfigurationManager.Configuration.ServerName;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Temporary function to migration network settings out of system.xml and into network.xml.
|
/// Temporary function to migration network settings out of system.xml and into network.xml.
|
||||||
/// TODO: remove at the point when a fixed migration path has been decided upon.
|
/// TODO: remove at the point when a fixed migration path has been decided upon.
|
||||||
|
@ -305,48 +350,6 @@ namespace Emby.Server.Implementations
|
||||||
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Version ApplicationVersion { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string ApplicationVersionString { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current application user agent.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The application user agent.</value>
|
|
||||||
public string ApplicationUserAgent { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the email address for use within a comment section of a user agent field.
|
|
||||||
/// Presently used to provide contact information to MusicBrainz service.
|
|
||||||
/// </summary>
|
|
||||||
public string ApplicationUserAgentAddress => "team@jellyfin.org";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current application name.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The application name.</value>
|
|
||||||
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
|
|
||||||
|
|
||||||
private DeviceId _deviceId;
|
|
||||||
|
|
||||||
public string SystemId
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_deviceId == null)
|
|
||||||
{
|
|
||||||
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _deviceId.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public string Name => ApplicationProductName;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instance of type and resolves all constructor dependencies.
|
/// Creates an instance of type and resolves all constructor dependencies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -370,10 +373,7 @@ namespace Emby.Server.Implementations
|
||||||
/// <returns>System.Object.</returns>
|
/// <returns>System.Object.</returns>
|
||||||
protected object CreateInstanceSafe(Type type)
|
protected object CreateInstanceSafe(Type type)
|
||||||
{
|
{
|
||||||
if (_creatingInstances == null)
|
_creatingInstances ??= new List<Type>();
|
||||||
{
|
|
||||||
_creatingInstances = new List<Type>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_creatingInstances.IndexOf(type) != -1)
|
if (_creatingInstances.IndexOf(type) != -1)
|
||||||
{
|
{
|
||||||
|
@ -467,6 +467,7 @@ namespace Emby.Server.Implementations
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the startup tasks.
|
/// Runs the startup tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns><see cref="Task" />.</returns>
|
/// <returns><see cref="Task" />.</returns>
|
||||||
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
|
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
@ -480,7 +481,7 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
_mediaEncoder.SetFFmpegPath();
|
_mediaEncoder.SetFFmpegPath();
|
||||||
|
|
||||||
Logger.LogInformation("ServerId: {0}", SystemId);
|
Logger.LogInformation("ServerId: {ServerId}", SystemId);
|
||||||
|
|
||||||
var entryPoints = GetExports<IServerEntryPoint>();
|
var entryPoints = GetExports<IServerEntryPoint>();
|
||||||
|
|
||||||
|
@ -605,8 +606,6 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||||
ServiceCollection.AddSingleton<EncodingHelper>();
|
ServiceCollection.AddSingleton<EncodingHelper>();
|
||||||
|
|
||||||
|
@ -628,8 +627,6 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
|
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
|
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
|
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
|
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
|
||||||
|
@ -665,8 +662,7 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
|
ServiceCollection.AddScoped<ISessionContext, SessionContext>();
|
||||||
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
|
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IAuthService, AuthService>();
|
ServiceCollection.AddSingleton<IAuthService, AuthService>();
|
||||||
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
||||||
|
@ -695,8 +691,6 @@ namespace Emby.Server.Implementations
|
||||||
_mediaEncoder = Resolve<IMediaEncoder>();
|
_mediaEncoder = Resolve<IMediaEncoder>();
|
||||||
_sessionManager = Resolve<ISessionManager>();
|
_sessionManager = Resolve<ISessionManager>();
|
||||||
|
|
||||||
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
|
||||||
|
|
||||||
SetStaticProperties();
|
SetStaticProperties();
|
||||||
|
|
||||||
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
|
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
|
||||||
|
@ -725,7 +719,7 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
|
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
|
||||||
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
||||||
logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
|
logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name);
|
||||||
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
||||||
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
||||||
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
||||||
|
@ -877,10 +871,6 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private CertificateInfo CertificateInfo { get; set; }
|
|
||||||
|
|
||||||
public X509Certificate2 Certificate { get; private set; }
|
|
||||||
|
|
||||||
private IEnumerable<string> GetUrlPrefixes()
|
private IEnumerable<string> GetUrlPrefixes()
|
||||||
{
|
{
|
||||||
var hosts = new[] { "+" };
|
var hosts = new[] { "+" };
|
||||||
|
@ -1102,16 +1092,14 @@ namespace Emby.Server.Implementations
|
||||||
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
||||||
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
||||||
CachePath = ApplicationPaths.CachePath,
|
CachePath = ApplicationPaths.CachePath,
|
||||||
OperatingSystem = OperatingSystem.Id.ToString(),
|
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
|
||||||
OperatingSystemDisplayName = OperatingSystem.Name,
|
OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
|
||||||
CanSelfRestart = CanSelfRestart,
|
CanSelfRestart = CanSelfRestart,
|
||||||
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
||||||
HasUpdateAvailable = HasUpdateAvailable,
|
|
||||||
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
||||||
ServerName = FriendlyName,
|
ServerName = FriendlyName,
|
||||||
LocalAddress = GetSmartApiUrl(source),
|
LocalAddress = GetSmartApiUrl(source),
|
||||||
SupportsLibraryMonitor = true,
|
SupportsLibraryMonitor = true,
|
||||||
EncoderLocation = _mediaEncoder.EncoderLocation,
|
|
||||||
SystemArchitecture = RuntimeInformation.OSArchitecture,
|
SystemArchitecture = RuntimeInformation.OSArchitecture,
|
||||||
PackageName = _startupOptions.PackageName
|
PackageName = _startupOptions.PackageName
|
||||||
};
|
};
|
||||||
|
@ -1122,25 +1110,22 @@ namespace Emby.Server.Implementations
|
||||||
.Select(i => new WakeOnLanInfo(i))
|
.Select(i => new WakeOnLanInfo(i))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
|
public PublicSystemInfo GetPublicSystemInfo(IPAddress address)
|
||||||
{
|
{
|
||||||
return new PublicSystemInfo
|
return new PublicSystemInfo
|
||||||
{
|
{
|
||||||
Version = ApplicationVersionString,
|
Version = ApplicationVersionString,
|
||||||
ProductName = ApplicationProductName,
|
ProductName = ApplicationProductName,
|
||||||
Id = SystemId,
|
Id = SystemId,
|
||||||
OperatingSystem = OperatingSystem.Id.ToString(),
|
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
|
||||||
ServerName = FriendlyName,
|
ServerName = FriendlyName,
|
||||||
LocalAddress = GetSmartApiUrl(source),
|
LocalAddress = GetSmartApiUrl(address),
|
||||||
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
|
@ -1149,7 +1134,7 @@ namespace Emby.Server.Implementations
|
||||||
return PublishedServerUrl.Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
string smart = NetManager.GetBindInterface(remoteAddr, out port);
|
||||||
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
||||||
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
@ -1211,27 +1196,20 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetLocalApiUrl(string host, string scheme = null, int? port = null)
|
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
|
||||||
{
|
{
|
||||||
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
||||||
// not. For consistency, always trim the trailing slash.
|
// not. For consistency, always trim the trailing slash.
|
||||||
return new UriBuilder
|
return new UriBuilder
|
||||||
{
|
{
|
||||||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
||||||
Host = host,
|
Host = hostname,
|
||||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
||||||
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||||
}.ToString().TrimEnd('/');
|
}.ToString().TrimEnd('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FriendlyName =>
|
/// <inheritdoc />
|
||||||
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
|
|
||||||
? Environment.MachineName
|
|
||||||
: ConfigurationManager.Configuration.ServerName;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shuts down.
|
|
||||||
/// </summary>
|
|
||||||
public async Task Shutdown()
|
public async Task Shutdown()
|
||||||
{
|
{
|
||||||
if (IsShuttingDown)
|
if (IsShuttingDown)
|
||||||
|
@ -1255,26 +1233,6 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
protected abstract void ShutdownInternal();
|
protected abstract void ShutdownInternal();
|
||||||
|
|
||||||
public event EventHandler HasUpdateAvailableChanged;
|
|
||||||
|
|
||||||
private bool _hasUpdateAvailable;
|
|
||||||
|
|
||||||
public bool HasUpdateAvailable
|
|
||||||
{
|
|
||||||
get => _hasUpdateAvailable;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
var fireEvent = value && !_hasUpdateAvailable;
|
|
||||||
|
|
||||||
_hasUpdateAvailable = value;
|
|
||||||
|
|
||||||
if (fireEvent)
|
|
||||||
{
|
|
||||||
HasUpdateAvailableChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
||||||
{
|
{
|
||||||
var assemblies = _allConcreteTypes
|
var assemblies = _allConcreteTypes
|
||||||
|
@ -1289,41 +1247,7 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void LaunchUrl(string url)
|
/// <inheritdoc />
|
||||||
{
|
|
||||||
if (!CanLaunchWebBrowser)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var process = new Process
|
|
||||||
{
|
|
||||||
StartInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = url,
|
|
||||||
UseShellExecute = true,
|
|
||||||
ErrorDialog = false
|
|
||||||
},
|
|
||||||
EnableRaisingEvents = true
|
|
||||||
};
|
|
||||||
process.Exited += (sender, args) => ((Process)sender).Dispose();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
process.Start();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error launching url: {url}", url);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
@ -9,7 +11,7 @@ using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Json;
|
using Jellyfin.Extensions.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;
|
||||||
|
@ -100,7 +102,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
|
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
|
||||||
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
|
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
|
||||||
|
|
||||||
return !(channel is IDisableMediaSourceDisplay);
|
return channel is not IDisableMediaSourceDisplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -878,7 +880,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,11 +1079,11 @@ namespace Emby.Server.Implementations.Channels
|
||||||
|
|
||||||
// was used for status
|
// was used for status
|
||||||
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
|
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
|
||||||
//{
|
// {
|
||||||
// item.ExternalEtag = info.Etag;
|
// item.ExternalEtag = info.Etag;
|
||||||
// forceUpdate = true;
|
// forceUpdate = true;
|
||||||
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
|
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
|
||||||
//}
|
// }
|
||||||
|
|
||||||
if (!internalChannelId.Equals(item.ChannelId))
|
if (!internalChannelId.Equals(item.ChannelId))
|
||||||
{
|
{
|
||||||
|
|
|
@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.Where(i => i != null)
|
.Where(i => i != null)
|
||||||
.GroupBy(x => x.Id)
|
.GroupBy(x => x!.Id) // We removed the null values
|
||||||
.Select(x => x.First())
|
.Select(x => x.First())
|
||||||
.ToList();
|
.ToList()!; // Again... the list doesn't contain any null values
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
|
@ -61,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)
|
||||||
{
|
{
|
||||||
|
@ -78,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)
|
||||||
|
@ -97,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
|
||||||
};
|
};
|
||||||
|
@ -114,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);
|
||||||
}
|
}
|
||||||
|
@ -162,9 +160,9 @@ namespace Emby.Server.Implementations.Collections
|
||||||
DateCreated = DateTime.UtcNow
|
DateCreated = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
parentFolder.AddChild(collection, CancellationToken.None);
|
parentFolder.AddChild(collection);
|
||||||
|
|
||||||
if (options.ItemIdList.Length > 0)
|
if (options.ItemIdList.Count > 0)
|
||||||
{
|
{
|
||||||
await AddToCollectionAsync(
|
await AddToCollectionAsync(
|
||||||
collection.Id,
|
collection.Id,
|
||||||
|
@ -198,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");
|
||||||
}
|
}
|
||||||
|
@ -248,11 +245,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
|
|
||||||
if (fireEvent)
|
if (fireEvent)
|
||||||
{
|
{
|
||||||
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs
|
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
|
||||||
{
|
|
||||||
Collection = collection,
|
|
||||||
ItemsChanged = itemList
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -260,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");
|
||||||
}
|
}
|
||||||
|
@ -304,11 +295,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
},
|
},
|
||||||
RefreshPriority.High);
|
RefreshPriority.High);
|
||||||
|
|
||||||
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
|
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
|
||||||
{
|
|
||||||
Collection = collection,
|
|
||||||
ItemsChanged = itemList
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -320,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;
|
||||||
|
|
||||||
|
@ -348,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)
|
||||||
{
|
{
|
||||||
|
@ -363,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,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -59,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";
|
||||||
|
@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data
|
||||||
|
|
||||||
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
|
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
|
||||||
{
|
{
|
||||||
if (row[1].SQLiteType != SQLiteType.Null)
|
if (row.TryGetString(1, out var columnName))
|
||||||
{
|
{
|
||||||
var name = row[1].ToString();
|
columnNames.Add(columnName);
|
||||||
|
|
||||||
columnNames.Add(name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
public class ManagedConnection : IDisposable
|
public class ManagedConnection : IDisposable
|
||||||
{
|
{
|
||||||
private SQLiteDatabaseConnection _db;
|
private SQLiteDatabaseConnection? _db;
|
||||||
private readonly SemaphoreSlim _writeLock;
|
private readonly SemaphoreSlim _writeLock;
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
@ -54,12 +54,12 @@ namespace Emby.Server.Implementations.Data
|
||||||
return _db.RunInTransaction(action, mode);
|
return _db.RunInTransaction(action, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql)
|
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
|
||||||
{
|
{
|
||||||
return _db.Query(sql);
|
return _db.Query(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql, params object[] values)
|
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
|
||||||
{
|
{
|
||||||
return _db.Query(sql, values);
|
return _db.Query(sql, values);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
#nullable disable
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Guid ReadGuidFromBlob(this IResultSetValue result)
|
public static Guid ReadGuidFromBlob(this ResultSetValue result)
|
||||||
{
|
{
|
||||||
return new Guid(result.ToBlob());
|
return new Guid(result.ToBlob());
|
||||||
}
|
}
|
||||||
|
@ -85,7 +86,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
private static string GetDateTimeKindFormat(DateTimeKind kind)
|
private static string GetDateTimeKindFormat(DateTimeKind kind)
|
||||||
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
|
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
|
||||||
|
|
||||||
public static DateTime ReadDateTime(this IResultSetValue result)
|
public static DateTime ReadDateTime(this ResultSetValue result)
|
||||||
{
|
{
|
||||||
var dateText = result.ToString();
|
var dateText = result.ToString();
|
||||||
|
|
||||||
|
@ -96,49 +97,139 @@ namespace Emby.Server.Implementations.Data
|
||||||
DateTimeStyles.None).ToUniversalTime();
|
DateTimeStyles.None).ToUniversalTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DateTime? TryReadDateTime(this IResultSetValue result)
|
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
|
||||||
{
|
{
|
||||||
var dateText = result.ToString();
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dateText = item.ToString();
|
||||||
|
|
||||||
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
|
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
|
||||||
{
|
{
|
||||||
return dateTimeResult.ToUniversalTime();
|
result = dateTimeResult.ToUniversalTime();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
result = default;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsDBNull(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
|
||||||
{
|
{
|
||||||
return result[index].SQLiteType == SQLiteType.Null;
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetString(this IReadOnlyList<IResultSetValue> result, int index)
|
result = item.ReadGuidFromBlob();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsDbNull(this ResultSetValue result)
|
||||||
|
{
|
||||||
|
return result.SQLiteType == SQLiteType.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
|
||||||
{
|
{
|
||||||
return result[index].ToString();
|
return result[index].ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool GetBoolean(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
|
||||||
{
|
{
|
||||||
return result[index].ToBool();
|
return result[index].ToBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int GetInt32(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
|
||||||
{
|
{
|
||||||
return result[index].ToInt();
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index)
|
result = item.ToBool();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
|
||||||
|
{
|
||||||
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ToInt();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
|
||||||
{
|
{
|
||||||
return result[index].ToInt64();
|
return result[index].ToInt64();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float GetFloat(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
|
||||||
{
|
{
|
||||||
return result[index].ToFloat();
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index)
|
result = item.ToInt64();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
|
||||||
|
{
|
||||||
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ToFloat();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
|
||||||
|
{
|
||||||
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ToDouble();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index)
|
||||||
{
|
{
|
||||||
return result[index].ReadGuidFromBlob();
|
return result[index].ReadGuidFromBlob();
|
||||||
}
|
}
|
||||||
|
@ -350,7 +441,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement)
|
public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
|
||||||
{
|
{
|
||||||
while (statement.MoveNext())
|
while (statement.MoveNext())
|
||||||
{
|
{
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -127,19 +129,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))
|
||||||
|
@ -147,22 +147,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>
|
||||||
|
@ -172,7 +173,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();
|
||||||
|
@ -262,19 +262,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))
|
||||||
|
@ -286,7 +286,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())
|
||||||
|
@ -299,7 +299,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)
|
||||||
{
|
{
|
||||||
|
@ -311,19 +311,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>();
|
||||||
|
@ -332,7 +332,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())
|
||||||
{
|
{
|
||||||
|
@ -347,17 +347,18 @@ 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>
|
||||||
private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
|
/// <returns>The user item data.</returns>
|
||||||
|
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
|
||||||
{
|
{
|
||||||
var userData = new UserItemData();
|
var userData = new UserItemData();
|
||||||
|
|
||||||
userData.Key = reader[0].ToString();
|
userData.Key = reader[0].ToString();
|
||||||
// userData.UserId = reader[1].ReadGuidFromBlob();
|
// userData.UserId = reader[1].ReadGuidFromBlob();
|
||||||
|
|
||||||
if (reader[2].SQLiteType != SQLiteType.Null)
|
if (reader.TryGetDouble(2, out var rating))
|
||||||
{
|
{
|
||||||
userData.Rating = reader[2].ToDouble();
|
userData.Rating = rating;
|
||||||
}
|
}
|
||||||
|
|
||||||
userData.Played = reader[3].ToBool();
|
userData.Played = reader[3].ToBool();
|
||||||
|
@ -365,19 +366,19 @@ namespace Emby.Server.Implementations.Data
|
||||||
userData.IsFavorite = reader[5].ToBool();
|
userData.IsFavorite = reader[5].ToBool();
|
||||||
userData.PlaybackPositionTicks = reader[6].ToInt64();
|
userData.PlaybackPositionTicks = reader[6].ToInt64();
|
||||||
|
|
||||||
if (reader[7].SQLiteType != SQLiteType.Null)
|
if (reader.TryReadDateTime(7, out var lastPlayedDate))
|
||||||
{
|
{
|
||||||
userData.LastPlayedDate = reader[7].TryReadDateTime();
|
userData.LastPlayedDate = lastPlayedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reader[8].SQLiteType != SQLiteType.Null)
|
if (reader.TryGetInt32(8, out var audioStreamIndex))
|
||||||
{
|
{
|
||||||
userData.AudioStreamIndex = reader[8].ToInt();
|
userData.AudioStreamIndex = audioStreamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reader[9].SQLiteType != SQLiteType.Null)
|
if (reader.TryGetInt32(9, out var subtitleStreamIndex))
|
||||||
{
|
{
|
||||||
userData.SubtitleStreamIndex = reader[9].ToInt();
|
userData.SubtitleStreamIndex = subtitleStreamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return userData;
|
return userData;
|
||||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
/// This holds all the types in the running assemblies
|
/// This holds all the types in the running assemblies
|
||||||
/// so that we can de-serialize properly when we don't have strong types.
|
/// so that we can de-serialize properly when we don't have strong types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>();
|
private readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the type.
|
/// Gets the type.
|
||||||
|
@ -21,26 +21,16 @@ namespace Emby.Server.Implementations.Data
|
||||||
/// <param name="typeName">Name of the type.</param>
|
/// <param name="typeName">Name of the type.</param>
|
||||||
/// <returns>Type.</returns>
|
/// <returns>Type.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
|
/// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
|
||||||
public Type GetType(string typeName)
|
public Type? GetType(string typeName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(typeName))
|
if (string.IsNullOrEmpty(typeName))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(typeName));
|
throw new ArgumentNullException(nameof(typeName));
|
||||||
}
|
}
|
||||||
|
|
||||||
return _typeMap.GetOrAdd(typeName, LookupType);
|
return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
|
||||||
}
|
.Select(a => a.GetType(k))
|
||||||
|
.FirstOrDefault(t => t != null));
|
||||||
/// <summary>
|
|
||||||
/// Lookups the type.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="typeName">Name of the type.</param>
|
|
||||||
/// <returns>Type.</returns>
|
|
||||||
private Type LookupType(string typeName)
|
|
||||||
{
|
|
||||||
return AppDomain.CurrentDomain.GetAssemblies()
|
|
||||||
.Select(a => a.GetType(typeName))
|
|
||||||
.FirstOrDefault(t => t != null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -49,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,
|
||||||
|
@ -73,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)
|
||||||
{
|
{
|
||||||
|
@ -505,7 +507,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.
|
||||||
|
@ -614,7 +615,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
|
||||||
|
@ -665,10 +665,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
var tag = GetImageCacheTag(item, image);
|
var tag = GetImageCacheTag(item, image);
|
||||||
if (!string.IsNullOrEmpty(image.BlurHash))
|
if (!string.IsNullOrEmpty(image.BlurHash))
|
||||||
{
|
{
|
||||||
if (dto.ImageBlurHashes == null)
|
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
|
||||||
{
|
|
||||||
dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dto.ImageBlurHashes.ContainsKey(image.Type))
|
if (!dto.ImageBlurHashes.ContainsKey(image.Type))
|
||||||
{
|
{
|
||||||
|
@ -702,10 +699,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
|
|
||||||
if (hashes.Count > 0)
|
if (hashes.Count > 0)
|
||||||
{
|
{
|
||||||
if (dto.ImageBlurHashes == null)
|
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
|
||||||
{
|
|
||||||
dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
dto.ImageBlurHashes[imageType] = hashes;
|
dto.ImageBlurHashes[imageType] = hashes;
|
||||||
}
|
}
|
||||||
|
@ -811,7 +805,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;
|
||||||
}
|
}
|
||||||
|
@ -898,10 +892,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
dto.Taglines = new string[] { item.Tagline };
|
dto.Taglines = new string[] { item.Tagline };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.Taglines == null)
|
dto.Taglines ??= Array.Empty<string>();
|
||||||
{
|
|
||||||
dto.Taglines = Array.Empty<string>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.Type = item.GetBaseItemKind();
|
dto.Type = item.GetBaseItemKind();
|
||||||
|
@ -935,9 +926,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)
|
||||||
|
@ -945,10 +936,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 =>
|
||||||
|
@ -965,7 +956,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
|
||||||
|
@ -997,10 +988,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 =>
|
||||||
|
@ -1015,7 +1006,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
|
||||||
|
@ -1042,8 +1033,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;
|
||||||
|
@ -1082,9 +1072,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;
|
||||||
|
|
||||||
|
@ -1153,7 +1141,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);
|
||||||
|
@ -1166,7 +1154,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();
|
||||||
|
@ -1179,7 +1167,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
|
||||||
|
@ -1192,7 +1180,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();
|
||||||
|
@ -1203,7 +1191,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);
|
||||||
|
@ -1290,7 +1278,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();
|
||||||
}
|
}
|
||||||
|
@ -1323,9 +1311,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;
|
||||||
|
@ -1355,7 +1346,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);
|
||||||
|
|
||||||
|
@ -1405,7 +1396,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);
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
|
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
|
||||||
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
|
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
|
||||||
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
|
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
|
||||||
|
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||||
|
@ -22,16 +23,17 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DiscUtils.Udf" Version="0.16.4" />
|
||||||
<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="5.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
|
||||||
<PackageReference Include="Mono.Nat" Version="3.0.1" />
|
<PackageReference Include="Mono.Nat" Version="3.0.1" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.0" />
|
||||||
<PackageReference Include="sharpcompress" Version="0.28.1" />
|
<PackageReference Include="sharpcompress" Version="0.28.3" />
|
||||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.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.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -43,11 +45,13 @@
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
|
||||||
<!-- 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>
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release'">
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -106,8 +108,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
NatUtility.StartDiscovery();
|
NatUtility.StartDiscovery();
|
||||||
|
|
||||||
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
|
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
|
||||||
|
|
||||||
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Stop()
|
private void Stop()
|
||||||
|
@ -118,13 +118,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
|
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
|
||||||
|
|
||||||
_timer?.Dispose();
|
_timer?.Dispose();
|
||||||
|
|
||||||
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
|
||||||
{
|
|
||||||
NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
|
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -147,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;
|
||||||
}
|
}
|
||||||
|
@ -401,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -39,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,
|
||||||
|
@ -56,8 +57,8 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_udpServer = new UdpServer(_logger, _appHost, _config);
|
_udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
|
||||||
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
|
_udpServer.Start(_cancellationTokenSource.Token);
|
||||||
}
|
}
|
||||||
catch (SocketException ex)
|
catch (SocketException ex)
|
||||||
{
|
{
|
||||||
|
|
|
@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
|
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
|
||||||
|
|
||||||
private readonly object _syncLock = new object();
|
private readonly object _syncLock = new object();
|
||||||
private Timer _updateTimer;
|
private Timer? _updateTimer;
|
||||||
|
|
||||||
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
|
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
|
||||||
{
|
{
|
||||||
|
@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
|
private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
|
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
|
||||||
{
|
{
|
||||||
|
@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
|
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
|
if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem>? keys))
|
||||||
{
|
{
|
||||||
keys = new List<BaseItem>();
|
keys = new List<BaseItem>();
|
||||||
_changedItems[e.UserId] = keys;
|
_changedItems[e.UserId] = keys;
|
||||||
|
@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateTimerCallback(object state)
|
private void UpdateTimerCallback(object? state)
|
||||||
{
|
{
|
||||||
lock (_syncLock)
|
lock (_syncLock)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#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.Authentication;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
|
@ -17,9 +18,9 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,291 +0,0 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Net;
|
|
||||||
using MediaBrowser.Controller.Security;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.HttpServer.Security
|
|
||||||
{
|
|
||||||
public class AuthorizationContext : IAuthorizationContext
|
|
||||||
{
|
|
||||||
private readonly IAuthenticationRepository _authRepo;
|
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
|
|
||||||
public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager)
|
|
||||||
{
|
|
||||||
_authRepo = authRepo;
|
|
||||||
_userManager = userManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
|
|
||||||
{
|
|
||||||
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
|
|
||||||
{
|
|
||||||
return (AuthorizationInfo)cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetAuthorization(requestContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
|
|
||||||
{
|
|
||||||
var auth = GetAuthorizationDictionary(requestContext);
|
|
||||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
|
||||||
return authInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authorization.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
|
||||||
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
|
|
||||||
{
|
|
||||||
var auth = GetAuthorizationDictionary(httpReq);
|
|
||||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
|
|
||||||
|
|
||||||
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
|
||||||
return authInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
|
|
||||||
in Dictionary<string, string> auth,
|
|
||||||
in IHeaderDictionary headers,
|
|
||||||
in IQueryCollection queryString)
|
|
||||||
{
|
|
||||||
string deviceId = null;
|
|
||||||
string device = null;
|
|
||||||
string client = null;
|
|
||||||
string version = null;
|
|
||||||
string token = null;
|
|
||||||
|
|
||||||
if (auth != null)
|
|
||||||
{
|
|
||||||
auth.TryGetValue("DeviceId", out deviceId);
|
|
||||||
auth.TryGetValue("Device", out device);
|
|
||||||
auth.TryGetValue("Client", out client);
|
|
||||||
auth.TryGetValue("Version", out version);
|
|
||||||
auth.TryGetValue("Token", out token);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
{
|
|
||||||
token = headers["X-Emby-Token"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
{
|
|
||||||
token = headers["X-MediaBrowser-Token"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
{
|
|
||||||
token = queryString["ApiKey"];
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO deprecate this query parameter.
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
{
|
|
||||||
token = queryString["api_key"];
|
|
||||||
}
|
|
||||||
|
|
||||||
var authInfo = new AuthorizationInfo
|
|
||||||
{
|
|
||||||
Client = client,
|
|
||||||
Device = device,
|
|
||||||
DeviceId = deviceId,
|
|
||||||
Version = version,
|
|
||||||
Token = token,
|
|
||||||
IsAuthenticated = false,
|
|
||||||
HasToken = false
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
|
||||||
{
|
|
||||||
// Request doesn't contain a token.
|
|
||||||
return authInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
authInfo.HasToken = true;
|
|
||||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
|
||||||
{
|
|
||||||
AccessToken = token
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.Items.Count > 0)
|
|
||||||
{
|
|
||||||
authInfo.IsAuthenticated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
|
||||||
|
|
||||||
if (originalAuthenticationInfo != null)
|
|
||||||
{
|
|
||||||
var updateToken = false;
|
|
||||||
|
|
||||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
|
||||||
{
|
|
||||||
authInfo.Client = originalAuthenticationInfo.AppName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
|
||||||
{
|
|
||||||
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
|
||||||
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
|
||||||
{
|
|
||||||
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
|
||||||
}
|
|
||||||
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (allowTokenInfoUpdate)
|
|
||||||
{
|
|
||||||
updateToken = true;
|
|
||||||
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
|
||||||
{
|
|
||||||
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
|
||||||
}
|
|
||||||
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (allowTokenInfoUpdate)
|
|
||||||
{
|
|
||||||
updateToken = true;
|
|
||||||
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
|
||||||
{
|
|
||||||
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
|
||||||
updateToken = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
|
||||||
{
|
|
||||||
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
|
||||||
|
|
||||||
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
|
||||||
updateToken = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
authInfo.IsApiKey = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
authInfo.IsApiKey = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateToken)
|
|
||||||
{
|
|
||||||
_authRepo.Update(originalAuthenticationInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return authInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the auth.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
|
||||||
private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
|
|
||||||
{
|
|
||||||
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(auth))
|
|
||||||
{
|
|
||||||
auth = httpReq.Request.Headers[HeaderNames.Authorization];
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetAuthorization(auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the auth.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
|
||||||
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
|
|
||||||
{
|
|
||||||
var auth = httpReq.Headers["X-Emby-Authorization"];
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(auth))
|
|
||||||
{
|
|
||||||
auth = httpReq.Headers[HeaderNames.Authorization];
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetAuthorization(auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authorization.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="authorizationHeader">The authorization header.</param>
|
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
|
||||||
private Dictionary<string, string> GetAuthorization(string authorizationHeader)
|
|
||||||
{
|
|
||||||
if (authorizationHeader == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts = authorizationHeader.Split(' ', 2);
|
|
||||||
|
|
||||||
// There should be at least to parts
|
|
||||||
if (parts.Length != 2)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var acceptedNames = new[] { "MediaBrowser", "Emby" };
|
|
||||||
|
|
||||||
// It has to be a digest request
|
|
||||||
if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove uptil the first space
|
|
||||||
authorizationHeader = parts[1];
|
|
||||||
parts = authorizationHeader.Split(',');
|
|
||||||
|
|
||||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var item in parts)
|
|
||||||
{
|
|
||||||
var param = item.Trim().Split('=', 2);
|
|
||||||
|
|
||||||
if (param.Length == 2)
|
|
||||||
{
|
|
||||||
var value = NormalizeValue(param[1].Trim('"'));
|
|
||||||
result[param[0]] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeValue(string value)
|
|
||||||
{
|
|
||||||
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
|
@ -9,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;
|
||||||
|
@ -64,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; }
|
||||||
|
|
||||||
|
@ -84,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; }
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -33,7 +35,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task WebSocketRequestHandler(HttpContext context)
|
public async Task WebSocketRequestHandler(HttpContext context)
|
||||||
{
|
{
|
||||||
_ = _authService.Authenticate(context.Request);
|
_ = await _authService.Authenticate(context.Request).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
|
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user