mirror of https://github.com/jellyfin/jellyfin.git
Merge remote-tracking branch 'upstream/master' into baseitemkind-fixes
This commit is contained in:
commit
23dd6e2d9f
|
@ -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
|
|
|
@ -160,7 +160,6 @@ jobs:
|
||||||
dependsOn:
|
dependsOn:
|
||||||
- BuildPackage
|
- BuildPackage
|
||||||
- BuildDocker
|
- BuildDocker
|
||||||
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
|
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
@ -186,9 +185,6 @@ jobs:
|
||||||
|
|
||||||
- job: PublishNuget
|
- job: PublishNuget
|
||||||
displayName: 'Publish NuGet packages'
|
displayName: 'Publish NuGet packages'
|
||||||
dependsOn:
|
|
||||||
- BuildPackage
|
|
||||||
condition: succeeded('BuildPackage')
|
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
|
@ -94,5 +94,5 @@ jobs:
|
||||||
displayName: 'Publish OpenAPI Artifact'
|
displayName: 'Publish OpenAPI Artifact'
|
||||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||||
inputs:
|
inputs:
|
||||||
targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json"
|
targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
|
||||||
artifactName: 'OpenAPI Spec'
|
artifactName: 'OpenAPI Spec'
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -33,7 +33,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**
|
||||||
|
|
|
@ -7,3 +7,9 @@ updates:
|
||||||
time: '12:00'
|
time: '12:00'
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
time: '12:00'
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
name: Automation
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request_target:
|
||||||
|
issue_comment:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label:
|
||||||
|
name: Labeling
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Apply label
|
||||||
|
uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||||
|
with:
|
||||||
|
dirtyLabel: 'merge conflict'
|
||||||
|
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
project:
|
||||||
|
name: Project board
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Remove from 'Current Release' project
|
||||||
|
uses: alex-page/github-project-automation-plus@v0.7.1
|
||||||
|
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
project: Current Release
|
||||||
|
action: delete
|
||||||
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Add to 'Release Next' project
|
||||||
|
uses: alex-page/github-project-automation-plus@v0.7.1
|
||||||
|
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
project: Release Next
|
||||||
|
column: In progress
|
||||||
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Add to 'Current Release' project
|
||||||
|
uses: alex-page/github-project-automation-plus@v0.7.1
|
||||||
|
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
project: Current Release
|
||||||
|
column: In progress
|
||||||
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Check number of comments from the team member
|
||||||
|
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
|
||||||
|
id: member_comments
|
||||||
|
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
||||||
|
|
||||||
|
- name: Move issue to needs triage
|
||||||
|
uses: alex-page/github-project-automation-plus@v0.7.1
|
||||||
|
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
project: Issue Triage for Main Repo
|
||||||
|
column: Needs triage
|
||||||
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Add issue to triage project
|
||||||
|
uses: alex-page/github-project-automation-plus@v0.7.1
|
||||||
|
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
project: Issue Triage for Main Repo
|
||||||
|
column: Pending response
|
||||||
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
|
@ -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
|
|
@ -268,6 +268,7 @@ doc/
|
||||||
# Deployment artifacts
|
# Deployment artifacts
|
||||||
dist
|
dist
|
||||||
*.exe
|
*.exe
|
||||||
|
*.dll
|
||||||
|
|
||||||
# BenchmarkDotNet artifacts
|
# BenchmarkDotNet artifacts
|
||||||
BenchmarkDotNet.Artifacts
|
BenchmarkDotNet.Artifacts
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
- [bugfixin](https://github.com/bugfixin)
|
- [bugfixin](https://github.com/bugfixin)
|
||||||
- [chaosinnovator](https://github.com/chaosinnovator)
|
- [chaosinnovator](https://github.com/chaosinnovator)
|
||||||
- [ckcr4lyf](https://github.com/ckcr4lyf)
|
- [ckcr4lyf](https://github.com/ckcr4lyf)
|
||||||
|
- [cocool97](https://github.com/cocool97)
|
||||||
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
||||||
- [crankdoofus](https://github.com/crankdoofus)
|
- [crankdoofus](https://github.com/crankdoofus)
|
||||||
- [crobibero](https://github.com/crobibero)
|
- [crobibero](https://github.com/crobibero)
|
||||||
|
@ -49,6 +50,7 @@
|
||||||
- [h1nk](https://github.com/h1nk)
|
- [h1nk](https://github.com/h1nk)
|
||||||
- [hawken93](https://github.com/hawken93)
|
- [hawken93](https://github.com/hawken93)
|
||||||
- [HelloWorld017](https://github.com/HelloWorld017)
|
- [HelloWorld017](https://github.com/HelloWorld017)
|
||||||
|
- [ikomhoog](https://github.com/ikomhoog)
|
||||||
- [jftuga](https://github.com/jftuga)
|
- [jftuga](https://github.com/jftuga)
|
||||||
- [joern-h](https://github.com/joern-h)
|
- [joern-h](https://github.com/joern-h)
|
||||||
- [joshuaboniface](https://github.com/joshuaboniface)
|
- [joshuaboniface](https://github.com/joshuaboniface)
|
||||||
|
@ -68,6 +70,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)
|
||||||
|
@ -104,10 +107,11 @@
|
||||||
- [shemanaev](https://github.com/shemanaev)
|
- [shemanaev](https://github.com/shemanaev)
|
||||||
- [skaro13](https://github.com/skaro13)
|
- [skaro13](https://github.com/skaro13)
|
||||||
- [sl1288](https://github.com/sl1288)
|
- [sl1288](https://github.com/sl1288)
|
||||||
|
- [Smith00101010](https://github.com/Smith00101010)
|
||||||
- [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)
|
||||||
|
@ -143,6 +147,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
|
||||||
|
|
||||||
|
|
|
@ -1,14 +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-* \
|
||||||
&& yarn install \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder
|
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
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-* \
|
||||||
&& yarn install \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
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-* \
|
||||||
&& yarn install \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
namespace Emby.Dlna.Configuration
|
namespace Emby.Dlna.Configuration
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#nullable enable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using Emby.Dlna.Configuration;
|
using Emby.Dlna.Configuration;
|
||||||
|
|
|
@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||||
{
|
{
|
||||||
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -7,7 +8,6 @@ using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Emby.Dlna.Configuration;
|
|
||||||
using Emby.Dlna.Didl;
|
using Emby.Dlna.Didl;
|
||||||
using Emby.Dlna.Service;
|
using Emby.Dlna.Service;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
@ -121,7 +121,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||||
{
|
{
|
||||||
if (xmlWriter == null)
|
if (xmlWriter == null)
|
||||||
{
|
{
|
||||||
|
@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a "XSetBookmark" element to the xml document.
|
/// Adds a "XSetBookmark" element to the xml document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
/// <param name="sparams">The method parameters.</param>
|
||||||
private void HandleXSetBookmark(IDictionary<string, string> sparams)
|
private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams)
|
||||||
{
|
{
|
||||||
var id = sparams["ObjectID"];
|
var id = sparams["ObjectID"];
|
||||||
|
|
||||||
|
@ -305,35 +305,18 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
|
||||||
/// <param name="key">The key.</param>
|
|
||||||
/// <param name="defaultValue">The defaultValue.</param>
|
|
||||||
/// <returns>The <see cref="string"/>.</returns>
|
|
||||||
public static string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue)
|
|
||||||
{
|
|
||||||
if (sparams != null && sparams.TryGetValue(key, out string val))
|
|
||||||
{
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds the "Browse" xml response.
|
/// Builds the "Browse" xml response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||||
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
/// <param name="sparams">The method parameters.</param>
|
||||||
/// <param name="deviceId">The device Id to use.</param>
|
/// <param name="deviceId">The device Id to use.</param>
|
||||||
private void HandleBrowse(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
|
private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
|
||||||
{
|
{
|
||||||
var id = sparams["ObjectID"];
|
var id = sparams["ObjectID"];
|
||||||
var flag = sparams["BrowseFlag"];
|
var flag = sparams["BrowseFlag"];
|
||||||
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
|
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
|
||||||
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
|
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
|
||||||
|
|
||||||
var provided = 0;
|
var provided = 0;
|
||||||
|
|
||||||
|
@ -435,9 +418,9 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
/// Builds the response to the "X_BrowseByLetter request.
|
/// Builds the response to the "X_BrowseByLetter request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||||
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
|
/// <param name="sparams">The method parameters.</param>
|
||||||
/// <param name="deviceId">The device id.</param>
|
/// <param name="deviceId">The device id.</param>
|
||||||
private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
|
private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
|
||||||
{
|
{
|
||||||
// TODO: Implement this method
|
// TODO: Implement this method
|
||||||
HandleSearch(xmlWriter, sparams, deviceId);
|
HandleSearch(xmlWriter, sparams, deviceId);
|
||||||
|
@ -447,13 +430,13 @@ namespace Emby.Dlna.ContentDirectory
|
||||||
/// Builds a response to the "Search" request.
|
/// Builds a response to the "Search" request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param>
|
/// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param>
|
||||||
/// <param name="sparams">The sparams<see cref="IDictionary"/>.</param>
|
/// <param name="sparams">The method parameters.</param>
|
||||||
/// <param name="deviceId">The deviceId<see cref="string"/>.</param>
|
/// <param name="deviceId">The deviceId<see cref="string"/>.</param>
|
||||||
private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
|
private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
|
||||||
{
|
{
|
||||||
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
|
var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty));
|
||||||
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
|
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
|
||||||
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
|
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
|
||||||
|
|
||||||
// sort example: dc:title, dc:date
|
// sort example: dc:title, dc:date
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
#pragma warning disable SA1602
|
|
||||||
|
|
||||||
namespace Emby.Dlna.ContentDirectory
|
namespace Emby.Dlna.ContentDirectory
|
||||||
{
|
{
|
||||||
|
|
|
@ -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.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -208,7 +210,8 @@ namespace Emby.Dlna.Didl
|
||||||
var targetWidth = streamInfo.TargetWidth;
|
var targetWidth = streamInfo.TargetWidth;
|
||||||
var targetHeight = streamInfo.TargetHeight;
|
var targetHeight = streamInfo.TargetHeight;
|
||||||
|
|
||||||
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
|
var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
|
||||||
|
_profile,
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
|
@ -599,7 +602,8 @@ namespace Emby.Dlna.Didl
|
||||||
? MimeTypes.GetMimeType(filename)
|
? MimeTypes.GetMimeType(filename)
|
||||||
: mediaProfile.MimeType;
|
: mediaProfile.MimeType;
|
||||||
|
|
||||||
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
|
var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
|
||||||
|
_profile,
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
targetAudioBitrate,
|
targetAudioBitrate,
|
||||||
|
@ -974,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)
|
||||||
|
@ -1033,8 +1050,7 @@ namespace Emby.Dlna.Didl
|
||||||
var width = albumartUrlInfo.width ?? maxWidth;
|
var width = albumartUrlInfo.width ?? maxWidth;
|
||||||
var height = albumartUrlInfo.height ?? maxHeight;
|
var height = albumartUrlInfo.height ?? maxHeight;
|
||||||
|
|
||||||
var contentFeatures = new ContentFeatureBuilder(_profile)
|
var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
|
||||||
.BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
|
|
||||||
|
|
||||||
writer.WriteAttributeString(
|
writer.WriteAttributeString(
|
||||||
"protocolInfo",
|
"protocolInfo",
|
||||||
|
@ -1206,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,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -36,7 +38,7 @@ namespace Emby.Dlna
|
||||||
private readonly ILogger<DlnaManager> _logger;
|
private readonly ILogger<DlnaManager> _logger;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
||||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
|
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||||
|
|
||||||
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
@ -111,7 +113,7 @@ namespace Emby.Dlna
|
||||||
|
|
||||||
if (profile != null)
|
if (profile != null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -126,92 +128,57 @@ namespace Emby.Dlna
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
||||||
builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
|
builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
|
||||||
builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
|
builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
|
||||||
builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
|
builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
|
||||||
builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
|
builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
|
||||||
builder.Append("ModelName:").AppendLine(profile.ModelName);
|
builder.Append("ModelName: ").AppendLine(profile.ModelName);
|
||||||
builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
|
builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
|
||||||
builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
|
builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
|
||||||
builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
|
builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
|
||||||
|
|
||||||
_logger.LogInformation(builder.ToString());
|
_logger.LogInformation(builder.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
@ -333,7 +300,12 @@ namespace Emby.Dlna
|
||||||
throw new ArgumentNullException(nameof(id));
|
throw new ArgumentNullException(nameof(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (info == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return ParseProfileFile(info.Path, info.Info.Type);
|
return ParseProfileFile(info.Path, info.Info.Type);
|
||||||
}
|
}
|
||||||
|
@ -395,7 +367,8 @@ namespace Emby.Dlna
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(systemProfilesPath);
|
Directory.CreateDirectory(systemProfilesPath);
|
||||||
|
|
||||||
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -553,7 +526,7 @@ namespace Emby.Dlna
|
||||||
|
|
||||||
private void DumpProfiles()
|
private void DumpProfiles()
|
||||||
{
|
{
|
||||||
DeviceProfile[] list = new []
|
DeviceProfile[] list = new[]
|
||||||
{
|
{
|
||||||
new SamsungSmartTvProfile(),
|
new SamsungSmartTvProfile(),
|
||||||
new XboxOneProfile(),
|
new XboxOneProfile(),
|
||||||
|
|
|
@ -21,11 +21,11 @@
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -5,7 +7,6 @@ using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna.PlayTo;
|
using Emby.Dlna.PlayTo;
|
||||||
using Emby.Dlna.Ssdp;
|
using Emby.Dlna.Ssdp;
|
||||||
|
@ -128,7 +129,8 @@ namespace Emby.Dlna.Main
|
||||||
|
|
||||||
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
||||||
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
|
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
|
||||||
if (_disabled)
|
|
||||||
|
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
||||||
{
|
{
|
||||||
_logger.LogError("The DLNA specification does not support HTTPS.");
|
_logger.LogError("The DLNA specification does not support HTTPS.");
|
||||||
}
|
}
|
||||||
|
@ -227,9 +229,12 @@ namespace Emby.Dlna.Main
|
||||||
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
|
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
if (communicationsServer != null)
|
||||||
{
|
{
|
||||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error starting device discovery");
|
_logger.LogError(ex, "Error starting device discovery");
|
||||||
|
@ -313,9 +318,12 @@ namespace Emby.Dlna.Main
|
||||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
||||||
|
|
||||||
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
|
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
|
||||||
// DLNA will only work over http, so we must reset to http:// : {port}
|
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
|
||||||
uri.Scheme = "http://";
|
{
|
||||||
|
// DLNA will only work over http, so we must reset to http:// : {port}.
|
||||||
|
uri.Scheme = "http";
|
||||||
uri.Port = _netConfig.HttpServerPortNumber;
|
uri.Port = _netConfig.HttpServerPortNumber;
|
||||||
|
}
|
||||||
|
|
||||||
var device = new SsdpRootDevice
|
var device = new SsdpRootDevice
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||||
{
|
{
|
||||||
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
using Emby.Dlna.Service;
|
using Emby.Dlna.Service;
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -219,7 +221,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
@ -235,7 +237,13 @@ namespace Emby.Dlna.PlayTo
|
||||||
_logger.LogDebug("Setting mute");
|
_logger.LogDebug("Setting mute");
|
||||||
var value = mute ? 1 : 0;
|
var value = mute ? 1 : 0;
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
rendererCommands.BuildPost(command, service.ServiceType, value),
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
IsMuted = mute;
|
IsMuted = mute;
|
||||||
|
@ -253,7 +261,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -270,7 +278,13 @@ namespace Emby.Dlna.PlayTo
|
||||||
// Remote control will perform better
|
// Remote control will perform better
|
||||||
Volume = value;
|
Volume = value;
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
rendererCommands.BuildPost(command, service.ServiceType, value),
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,7 +292,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -291,7 +305,13 @@ namespace Emby.Dlna.PlayTo
|
||||||
throw new InvalidOperationException("Unable to find service");
|
throw new InvalidOperationException("Unable to find service");
|
||||||
}
|
}
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
|
@ -305,7 +325,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -325,14 +345,21 @@ namespace Emby.Dlna.PlayTo
|
||||||
}
|
}
|
||||||
|
|
||||||
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
post,
|
||||||
|
header: header,
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
await Task.Delay(50).ConfigureAwait(false);
|
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
|
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
@ -343,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))
|
||||||
|
@ -378,6 +441,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
public async Task SetPlay(CancellationToken cancellationToken)
|
public async Task SetPlay(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (avCommands == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -388,7 +455,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -396,7 +463,13 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
avCommands.BuildPost(command, service.ServiceType, 1),
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
|
@ -406,7 +479,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -414,7 +487,13 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
await new SsdpHttpClient(_httpClientFactory)
|
||||||
|
.SendCommandAsync(
|
||||||
|
Properties.BaseUrl,
|
||||||
|
service,
|
||||||
|
command.Name,
|
||||||
|
avCommands.BuildPost(command, service.ServiceType, 1),
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
TransportState = TransportState.Paused;
|
TransportState = TransportState.Paused;
|
||||||
|
@ -528,7 +607,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -578,7 +657,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -665,6 +744,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
}
|
}
|
||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (rendererCommands == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
|
@ -733,6 +816,11 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (rendererCommands == null)
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
|
@ -914,6 +1002,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
||||||
|
|
||||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
AvCommands = TransportCommands.Create(document);
|
AvCommands = TransportCommands.Create(document);
|
||||||
return AvCommands;
|
return AvCommands;
|
||||||
|
@ -942,6 +1034,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
||||||
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
|
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
|
||||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
RendererCommands = TransportCommands.Create(document);
|
RendererCommands = TransportCommands.Create(document);
|
||||||
return RendererCommands;
|
return RendererCommands;
|
||||||
|
@ -973,6 +1069,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
|
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
|
||||||
|
|
||||||
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
|
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var friendlyNames = new List<string>();
|
var friendlyNames = new List<string>();
|
||||||
|
|
||||||
|
@ -990,7 +1090,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
var deviceProperties = new DeviceInfo()
|
var deviceProperties = new DeviceInfo()
|
||||||
{
|
{
|
||||||
Name = string.Join(" ", friendlyNames),
|
Name = string.Join(' ', friendlyNames),
|
||||||
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
|
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -132,7 +150,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,8 +531,8 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
||||||
{
|
{
|
||||||
return new ContentFeatureBuilder(profile)
|
return ContentFeatureBuilder.BuildAudioHeader(
|
||||||
.BuildAudioHeader(
|
profile,
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioBitrate,
|
streamInfo.TargetAudioBitrate,
|
||||||
|
@ -514,8 +546,8 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
if (streamInfo.MediaType == DlnaProfileType.Video)
|
if (streamInfo.MediaType == DlnaProfileType.Video)
|
||||||
{
|
{
|
||||||
var list = new ContentFeatureBuilder(profile)
|
var list = ContentFeatureBuilder.BuildVideoHeader(
|
||||||
.BuildVideoHeader(
|
profile,
|
||||||
streamInfo.Container,
|
streamInfo.Container,
|
||||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
|
@ -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);
|
||||||
|
@ -777,7 +820,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
var currentWait = 0;
|
var currentWait = 0;
|
||||||
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
|
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
|
||||||
{
|
{
|
||||||
await Task.Delay(Interval).ConfigureAwait(false);
|
await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
|
||||||
currentWait += Interval;
|
currentWait += Interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -943,11 +986,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
||||||
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
||||||
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
||||||
|
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
||||||
// Be careful, IsDirectStream==true by default (Static != false or not in query).
|
|
||||||
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
|
|
||||||
request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
||||||
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
||||||
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -178,12 +180,17 @@ namespace Emby.Dlna.PlayTo
|
||||||
if (controller == null)
|
if (controller == null)
|
||||||
{
|
{
|
||||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (device == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Ignoring device as xml response is invalid.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string deviceName = device.Properties.Name;
|
string deviceName = device.Properties.Name;
|
||||||
|
|
||||||
_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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 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,8 +1,9 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -45,10 +46,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
return await XDocument.LoadAsync(
|
||||||
return XDocument.Parse(
|
stream,
|
||||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
LoadOptions.PreserveWhitespace,
|
||||||
LoadOptions.PreserveWhitespace);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||||
|
@ -94,10 +95,17 @@ namespace Emby.Dlna.PlayTo
|
||||||
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
try
|
||||||
return XDocument.Parse(
|
{
|
||||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
return await XDocument.LoadAsync(
|
||||||
LoadOptions.PreserveWhitespace);
|
stream,
|
||||||
|
LoadOptions.PreserveWhitespace,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
||||||
|
|
|
@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
public class TransportCommands
|
public class TransportCommands
|
||||||
{
|
{
|
||||||
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
|
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
|
||||||
private List<StateVariable> _stateVariables = new List<StateVariable>();
|
|
||||||
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
|
|
||||||
|
|
||||||
public List<StateVariable> StateVariables => _stateVariables;
|
public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
|
||||||
|
|
||||||
public List<ServiceAction> ServiceActions => _serviceActions;
|
public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
|
||||||
|
|
||||||
public static TransportCommands Create(XDocument document)
|
public static TransportCommands Create(XDocument document)
|
||||||
{
|
{
|
||||||
|
@ -48,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;
|
||||||
|
@ -70,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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -168,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,5 +1,4 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
#pragma warning disable SA1602
|
|
||||||
|
|
||||||
namespace Emby.Dlna.PlayTo
|
namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
||||||
|
@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
|
||||||
{
|
{
|
||||||
public DefaultProfile()
|
public DefaultProfile()
|
||||||
{
|
{
|
||||||
|
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||||
Name = "Generic Device";
|
Name = "Generic Device";
|
||||||
|
|
||||||
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
|
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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))
|
||||||
{
|
{
|
||||||
|
@ -151,7 +151,7 @@ namespace Emby.Dlna.Service
|
||||||
|
|
||||||
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
||||||
{
|
{
|
||||||
string namespaceURI = null, localName = null;
|
string? namespaceURI = null, localName = null;
|
||||||
|
|
||||||
await reader.MoveToContentAsync().ConfigureAwait(false);
|
await reader.MoveToContentAsync().ConfigureAwait(false);
|
||||||
await reader.ReadAsync().ConfigureAwait(false);
|
await reader.ReadAsync().ConfigureAwait(false);
|
||||||
|
@ -210,7 +210,7 @@ namespace Emby.Dlna.Service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
|
protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter);
|
||||||
|
|
||||||
private void LogRequest(ControlRequest request)
|
private void LogRequest(ControlRequest request)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -69,7 +71,7 @@ namespace Emby.Dlna.Ssdp
|
||||||
{
|
{
|
||||||
lock (_syncLock)
|
lock (_syncLock)
|
||||||
{
|
{
|
||||||
if (_listenerCount > 0 && _deviceLocator == null)
|
if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
|
||||||
{
|
{
|
||||||
_deviceLocator = new SsdpDeviceLocator(_commsServer);
|
_deviceLocator = new SsdpDeviceLocator(_commsServer);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
|
|
||||||
<!-- Code analysers-->
|
<!-- Code analysers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
|
@ -171,21 +172,31 @@ namespace Emby.Drawing
|
||||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
|
|
||||||
int quality = options.Quality;
|
int quality = options.Quality;
|
||||||
|
|
||||||
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
|
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
|
||||||
string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
|
string cacheFilePath = GetCacheFilePath(
|
||||||
|
originalImagePath,
|
||||||
|
options.Width,
|
||||||
|
options.Height,
|
||||||
|
options.MaxWidth,
|
||||||
|
options.MaxHeight,
|
||||||
|
options.FillWidth,
|
||||||
|
options.FillHeight,
|
||||||
|
quality,
|
||||||
|
dateModified,
|
||||||
|
outputFormat,
|
||||||
|
options.AddPlayedIndicator,
|
||||||
|
options.PercentPlayed,
|
||||||
|
options.UnplayedCount,
|
||||||
|
options.Blur,
|
||||||
|
options.BackgroundColor,
|
||||||
|
options.ForegroundLayer);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(cacheFilePath))
|
if (!File.Exists(cacheFilePath))
|
||||||
{
|
{
|
||||||
if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
|
|
||||||
{
|
|
||||||
options.CropWhiteSpace = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
|
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
|
||||||
|
|
||||||
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
@ -246,48 +257,111 @@ namespace Emby.Drawing
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the cache file path based on a set of parameters.
|
/// Gets the cache file path based on a set of parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
|
private string GetCacheFilePath(
|
||||||
|
string originalPath,
|
||||||
|
int? width,
|
||||||
|
int? height,
|
||||||
|
int? maxWidth,
|
||||||
|
int? maxHeight,
|
||||||
|
int? fillWidth,
|
||||||
|
int? fillHeight,
|
||||||
|
int quality,
|
||||||
|
DateTime dateModified,
|
||||||
|
ImageFormat format,
|
||||||
|
bool addPlayedIndicator,
|
||||||
|
double percentPlayed,
|
||||||
|
int? unwatchedCount,
|
||||||
|
int? blur,
|
||||||
|
string backgroundColor,
|
||||||
|
string foregroundLayer)
|
||||||
{
|
{
|
||||||
var filename = originalPath
|
var filename = new StringBuilder(256);
|
||||||
+ "width=" + outputSize.Width
|
filename.Append(originalPath);
|
||||||
+ "height=" + outputSize.Height
|
|
||||||
+ "quality=" + quality
|
filename.Append(",quality=");
|
||||||
+ "datemodified=" + dateModified.Ticks
|
filename.Append(quality);
|
||||||
+ "f=" + format;
|
|
||||||
|
filename.Append(",datemodified=");
|
||||||
|
filename.Append(dateModified.Ticks);
|
||||||
|
|
||||||
|
filename.Append(",f=");
|
||||||
|
filename.Append(format);
|
||||||
|
|
||||||
|
if (width.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",width=");
|
||||||
|
filename.Append(width.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",height=");
|
||||||
|
filename.Append(height.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxWidth.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",maxwidth=");
|
||||||
|
filename.Append(maxWidth.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxHeight.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",maxheight=");
|
||||||
|
filename.Append(maxHeight.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fillWidth.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",fillwidth=");
|
||||||
|
filename.Append(fillWidth.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fillHeight.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",fillheight=");
|
||||||
|
filename.Append(fillHeight.Value);
|
||||||
|
}
|
||||||
|
|
||||||
if (addPlayedIndicator)
|
if (addPlayedIndicator)
|
||||||
{
|
{
|
||||||
filename += "pl=true";
|
filename.Append(",pl=true");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (percentPlayed > 0)
|
if (percentPlayed > 0)
|
||||||
{
|
{
|
||||||
filename += "p=" + percentPlayed;
|
filename.Append(",p=");
|
||||||
|
filename.Append(percentPlayed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unwatchedCount.HasValue)
|
if (unwatchedCount.HasValue)
|
||||||
{
|
{
|
||||||
filename += "p=" + unwatchedCount.Value;
|
filename.Append(",p=");
|
||||||
|
filename.Append(unwatchedCount.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blur.HasValue)
|
if (blur.HasValue)
|
||||||
{
|
{
|
||||||
filename += "blur=" + blur.Value;
|
filename.Append(",blur=");
|
||||||
|
filename.Append(blur.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(backgroundColor))
|
if (!string.IsNullOrEmpty(backgroundColor))
|
||||||
{
|
{
|
||||||
filename += "b=" + backgroundColor;
|
filename.Append(",b=");
|
||||||
|
filename.Append(backgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(foregroundLayer))
|
if (!string.IsNullOrEmpty(foregroundLayer))
|
||||||
{
|
{
|
||||||
filename += "fl=" + foregroundLayer;
|
filename.Append(",fl=");
|
||||||
|
filename.Append(foregroundLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
filename += "v=" + Version;
|
filename.Append(",v=");
|
||||||
|
filename.Append(Version);
|
||||||
|
|
||||||
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant());
|
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -352,8 +426,13 @@ namespace Emby.Drawing
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string GetImageCacheTag(User user)
|
public string? GetImageCacheTag(User user)
|
||||||
{
|
{
|
||||||
|
if (user.ProfileImage == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
|
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
|
||||||
.ToString("N", CultureInfo.InvariantCulture);
|
.ToString("N", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ namespace Emby.Drawing
|
||||||
=> throw new NotImplementedException();
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
|
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using MediaBrowser.Common.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook
|
||||||
|
|
||||||
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
|
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
|
||||||
var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
|
var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
|
||||||
var nameWithReplacedDots = nameParserResult.Name.Replace(" ", ".");
|
var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.');
|
||||||
|
|
||||||
foreach (var group in groupedBy)
|
foreach (var group in groupedBy)
|
||||||
{
|
{
|
||||||
|
|
|
@ -23,11 +23,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>
|
||||||
|
@ -44,7 +45,6 @@
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
|
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
|
|
|
@ -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,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
|
@ -16,8 +17,14 @@ namespace Emby.Naming.Video
|
||||||
/// <param name="expressions">List of regex to parse name and year from.</param>
|
/// <param name="expressions">List of regex to parse name and year from.</param>
|
||||||
/// <param name="newName">Parsing result string.</param>
|
/// <param name="newName">Parsing result string.</param>
|
||||||
/// <returns>True if parsing was successful.</returns>
|
/// <returns>True if parsing was successful.</returns>
|
||||||
public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
|
public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var len = expressions.Count;
|
var len = expressions.Count;
|
||||||
for (int i = 0; i < len; i++)
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
|
@ -41,7 +48,7 @@ namespace Emby.Naming.Video
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
newName = string.Empty;
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,36 +29,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 +63,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 +85,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(List<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,95 +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))
|
|
||||||
{
|
|
||||||
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
|
|
||||||
{
|
|
||||||
testFilename = cleanName.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folderName.Length <= testFilename.Length)
|
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
|
||||||
|
{
|
||||||
|
var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
|
||||||
|
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
testFilename = testFilename.Substring(folderName.Length).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.IsNullOrEmpty(testFilename)
|
|
||||||
|| testFilename[0] == '-'
|
|
||||||
|| testFilename[0] == '_'
|
|
||||||
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
|
|
||||||
}
|
|
||||||
|
|
||||||
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,45 +1,36 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using MediaBrowser.Common.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>
|
||||||
|
@ -47,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))
|
||||||
{
|
{
|
||||||
|
@ -58,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;
|
||||||
}
|
}
|
||||||
|
@ -80,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();
|
||||||
}
|
}
|
||||||
|
@ -106,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,
|
||||||
|
@ -122,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(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -25,14 +27,9 @@
|
||||||
|
|
||||||
<!-- Code analyzers-->
|
<!-- Code analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -24,18 +24,15 @@
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
CachePath = cacheDirectoryPath;
|
CachePath = cacheDirectoryPath;
|
||||||
WebPath = webDirectoryPath;
|
WebPath = webDirectoryPath;
|
||||||
|
|
||||||
DataPath = 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,9 +1,6 @@
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.AppBase
|
namespace Emby.Server.Implementations.AppBase
|
||||||
|
@ -36,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);
|
||||||
|
@ -53,7 +51,8 @@ namespace Emby.Server.Implementations.AppBase
|
||||||
|
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
// Save it after load in case we got new items
|
// Save it after load in case we got new items
|
||||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
fs.Write(newBytes, 0, newBytesLen);
|
fs.Write(newBytes, 0, newBytesLen);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -10,8 +12,6 @@ using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna;
|
using Emby.Dlna;
|
||||||
|
@ -43,6 +43,7 @@ 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;
|
||||||
using Emby.Server.Implementations.TV;
|
using Emby.Server.Implementations.TV;
|
||||||
|
using Emby.Server.Implementations.Udp;
|
||||||
using Emby.Server.Implementations.Updates;
|
using Emby.Server.Implementations.Updates;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Networking.Configuration;
|
using Jellyfin.Networking.Configuration;
|
||||||
|
@ -50,7 +51,6 @@ using Jellyfin.Networking.Manager;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Events;
|
using MediaBrowser.Common.Events;
|
||||||
using MediaBrowser.Common.Json;
|
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Common.Updates;
|
using MediaBrowser.Common.Updates;
|
||||||
|
@ -99,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
|
||||||
using MediaBrowser.XbmcMetadata.Providers;
|
using MediaBrowser.XbmcMetadata.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
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;
|
||||||
|
@ -118,6 +119,7 @@ namespace Emby.Server.Implementations
|
||||||
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystemManager;
|
private readonly IFileSystem _fileSystemManager;
|
||||||
|
private readonly IConfiguration _startupConfig;
|
||||||
private readonly IXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
private readonly IStartupOptions _startupOptions;
|
private readonly IStartupOptions _startupOptions;
|
||||||
private readonly IPluginManager _pluginManager;
|
private readonly IPluginManager _pluginManager;
|
||||||
|
@ -126,7 +128,6 @@ namespace Emby.Server.Implementations
|
||||||
private IMediaEncoder _mediaEncoder;
|
private IMediaEncoder _mediaEncoder;
|
||||||
private ISessionManager _sessionManager;
|
private ISessionManager _sessionManager;
|
||||||
private string[] _urlPrefixes;
|
private string[] _urlPrefixes;
|
||||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether this instance can self restart.
|
/// Gets a value indicating whether this instance can self restart.
|
||||||
|
@ -211,7 +212,7 @@ namespace Emby.Server.Implementations
|
||||||
/// Gets or sets the configuration manager.
|
/// Gets or sets the configuration manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The configuration manager.</value>
|
/// <value>The configuration manager.</value>
|
||||||
protected IConfigurationManager ConfigurationManager { get; set; }
|
public ServerConfigurationManager ConfigurationManager { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the service provider.
|
/// Gets or sets the service provider.
|
||||||
|
@ -229,10 +230,9 @@ namespace Emby.Server.Implementations
|
||||||
public int HttpsPort { get; private set; }
|
public int HttpsPort { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the server configuration manager.
|
/// Gets the value of the PublishedServerUrl setting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The server configuration manager.</value>
|
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
||||||
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
||||||
|
@ -240,51 +240,37 @@ namespace Emby.Server.Implementations
|
||||||
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
||||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||||
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
||||||
|
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||||
public ApplicationHost(
|
public ApplicationHost(
|
||||||
IServerApplicationPaths applicationPaths,
|
IServerApplicationPaths applicationPaths,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IStartupOptions options,
|
IStartupOptions options,
|
||||||
|
IConfiguration startupConfig,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IServiceCollection serviceCollection)
|
IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
_xmlSerializer = new MyXmlSerializer();
|
|
||||||
|
|
||||||
ServiceCollection = serviceCollection;
|
|
||||||
|
|
||||||
ApplicationPaths = applicationPaths;
|
ApplicationPaths = applicationPaths;
|
||||||
LoggerFactory = loggerFactory;
|
LoggerFactory = loggerFactory;
|
||||||
|
_startupOptions = options;
|
||||||
|
_startupConfig = startupConfig;
|
||||||
_fileSystemManager = fileSystem;
|
_fileSystemManager = fileSystem;
|
||||||
|
ServiceCollection = serviceCollection;
|
||||||
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
|
|
||||||
// Have to migrate settings here as migration subsystem not yet initialised.
|
|
||||||
MigrateNetworkConfiguration();
|
|
||||||
|
|
||||||
// Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
|
|
||||||
ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
|
|
||||||
NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
|
||||||
|
|
||||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||||
|
|
||||||
_startupOptions = options;
|
|
||||||
|
|
||||||
// Initialize runtime stat collection
|
|
||||||
if (ServerConfigurationManager.Configuration.EnableMetrics)
|
|
||||||
{
|
|
||||||
DotNetRuntimeStatsBuilder.Default().StartCollecting();
|
|
||||||
}
|
|
||||||
|
|
||||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
||||||
|
|
||||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||||
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
||||||
|
|
||||||
|
_xmlSerializer = new MyXmlSerializer();
|
||||||
|
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
|
||||||
_pluginManager = new PluginManager(
|
_pluginManager = new PluginManager(
|
||||||
LoggerFactory.CreateLogger<PluginManager>(),
|
LoggerFactory.CreateLogger<PluginManager>(),
|
||||||
this,
|
this,
|
||||||
ServerConfigurationManager.Configuration,
|
ConfigurationManager.Configuration,
|
||||||
ApplicationPaths.PluginsPath,
|
ApplicationPaths.PluginsPath,
|
||||||
ApplicationVersion);
|
ApplicationVersion);
|
||||||
}
|
}
|
||||||
|
@ -299,9 +285,9 @@ namespace Emby.Server.Implementations
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
var networkSettings = new NetworkConfiguration();
|
var networkSettings = new NetworkConfiguration();
|
||||||
ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
|
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
|
||||||
_xmlSerializer.SerializeToFile(networkSettings, path);
|
_xmlSerializer.SerializeToFile(networkSettings, path);
|
||||||
Logger?.LogDebug("Successfully migrated network settings.");
|
Logger.LogDebug("Successfully migrated network settings.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,10 +337,7 @@ namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_deviceId == null)
|
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
|
||||||
{
|
|
||||||
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _deviceId.Value;
|
return _deviceId.Value;
|
||||||
}
|
}
|
||||||
|
@ -374,7 +357,7 @@ namespace Emby.Server.Implementations
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instance of type and resolves all constructor dependencies.
|
/// Creates an instance of type and resolves all constructor dependencies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// /// <typeparam name="T">The type.</typeparam>
|
/// <typeparam name="T">The type.</typeparam>
|
||||||
/// <returns>T.</returns>
|
/// <returns>T.</returns>
|
||||||
public T CreateInstance<T>()
|
public T CreateInstance<T>()
|
||||||
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
|
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
|
||||||
|
@ -386,10 +369,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)
|
||||||
{
|
{
|
||||||
|
@ -460,7 +440,7 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
|
public IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
|
||||||
{
|
{
|
||||||
// Convert to list so this isn't executed for each iteration
|
// Convert to list so this isn't executed for each iteration
|
||||||
var parts = GetExportTypes<T>()
|
var parts = GetExportTypes<T>()
|
||||||
|
@ -484,8 +464,9 @@ namespace Emby.Server.Implementations
|
||||||
/// Runs the startup tasks.
|
/// Runs the startup tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns><see cref="Task" />.</returns>
|
/// <returns><see cref="Task" />.</returns>
|
||||||
public async Task RunStartupTasksAsync()
|
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
Logger.LogInformation("Running startup tasks");
|
Logger.LogInformation("Running startup tasks");
|
||||||
|
|
||||||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||||
|
@ -499,14 +480,21 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
var entryPoints = GetExports<IServerEntryPoint>();
|
var entryPoints = GetExports<IServerEntryPoint>();
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var stopWatch = new Stopwatch();
|
var stopWatch = new Stopwatch();
|
||||||
stopWatch.Start();
|
stopWatch.Start();
|
||||||
|
|
||||||
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
|
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
|
||||||
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||||
|
|
||||||
Logger.LogInformation("Core startup complete");
|
Logger.LogInformation("Core startup complete");
|
||||||
CoreStartupHasCompleted = true;
|
CoreStartupHasCompleted = true;
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
stopWatch.Restart();
|
stopWatch.Restart();
|
||||||
|
|
||||||
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
||||||
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
|
@ -530,7 +518,21 @@ namespace Emby.Server.Implementations
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Init()
|
public void Init()
|
||||||
{
|
{
|
||||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
DiscoverTypes();
|
||||||
|
|
||||||
|
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||||
|
|
||||||
|
// Have to migrate settings here as migration subsystem not yet initialised.
|
||||||
|
MigrateNetworkConfiguration();
|
||||||
|
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
||||||
|
|
||||||
|
// Initialize runtime stat collection
|
||||||
|
if (ConfigurationManager.Configuration.EnableMetrics)
|
||||||
|
{
|
||||||
|
DotNetRuntimeStatsBuilder.Default().StartCollecting();
|
||||||
|
}
|
||||||
|
|
||||||
|
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||||
HttpPort = networkConfiguration.HttpServerPortNumber;
|
HttpPort = networkConfiguration.HttpServerPortNumber;
|
||||||
HttpsPort = networkConfiguration.HttpsPortNumber;
|
HttpsPort = networkConfiguration.HttpsPortNumber;
|
||||||
|
|
||||||
|
@ -548,8 +550,6 @@ namespace Emby.Server.Implementations
|
||||||
};
|
};
|
||||||
Certificate = GetCertificate(CertificateInfo);
|
Certificate = GetCertificate(CertificateInfo);
|
||||||
|
|
||||||
DiscoverTypes();
|
|
||||||
|
|
||||||
RegisterServices();
|
RegisterServices();
|
||||||
|
|
||||||
_pluginManager.RegisterServices(ServiceCollection);
|
_pluginManager.RegisterServices(ServiceCollection);
|
||||||
|
@ -564,7 +564,8 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
ServiceCollection.AddMemoryCache();
|
ServiceCollection.AddMemoryCache();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton(ConfigurationManager);
|
ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
|
||||||
|
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
|
||||||
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
||||||
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
||||||
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||||
|
@ -591,8 +592,6 @@ namespace Emby.Server.Implementations
|
||||||
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
||||||
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
||||||
|
|
||||||
ServiceCollection.AddSingleton(ServerConfigurationManager);
|
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||||
|
@ -604,12 +603,8 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
||||||
|
|
||||||
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
|
||||||
ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
|
|
||||||
|
|
||||||
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
|
||||||
ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
|
|
||||||
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||||
|
ServiceCollection.AddSingleton<EncodingHelper>();
|
||||||
|
|
||||||
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
||||||
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
||||||
|
@ -674,14 +669,14 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<EncodingHelper>();
|
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<TranscodingJobHelper>();
|
ServiceCollection.AddSingleton<TranscodingJobHelper>();
|
||||||
ServiceCollection.AddScoped<MediaInfoHelper>();
|
ServiceCollection.AddScoped<MediaInfoHelper>();
|
||||||
ServiceCollection.AddScoped<AudioHelper>();
|
ServiceCollection.AddScoped<AudioHelper>();
|
||||||
ServiceCollection.AddScoped<DynamicHlsHelper>();
|
ServiceCollection.AddScoped<DynamicHlsHelper>();
|
||||||
|
|
||||||
|
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -779,7 +774,7 @@ namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
// For now there's no real way to inject these properly
|
// For now there's no real way to inject these properly
|
||||||
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
||||||
BaseItem.ConfigurationManager = ServerConfigurationManager;
|
BaseItem.ConfigurationManager = ConfigurationManager;
|
||||||
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
||||||
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
||||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||||
|
@ -801,13 +796,12 @@ namespace Emby.Server.Implementations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void FindParts()
|
private void FindParts()
|
||||||
{
|
{
|
||||||
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
|
if (!ConfigurationManager.Configuration.IsPortAuthorized)
|
||||||
{
|
{
|
||||||
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
|
ConfigurationManager.Configuration.IsPortAuthorized = true;
|
||||||
ConfigurationManager.SaveConfiguration();
|
ConfigurationManager.SaveConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
|
||||||
_pluginManager.CreatePlugins();
|
_pluginManager.CreatePlugins();
|
||||||
|
|
||||||
_urlPrefixes = GetUrlPrefixes().ToArray();
|
_urlPrefixes = GetUrlPrefixes().ToArray();
|
||||||
|
@ -911,7 +905,7 @@ namespace Emby.Server.Implementations
|
||||||
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var requiresRestart = false;
|
var requiresRestart = false;
|
||||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||||
|
|
||||||
// Don't do anything if these haven't been set yet
|
// Don't do anything if these haven't been set yet
|
||||||
if (HttpPort != 0 && HttpsPort != 0)
|
if (HttpPort != 0 && HttpsPort != 0)
|
||||||
|
@ -920,10 +914,10 @@ namespace Emby.Server.Implementations
|
||||||
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
||||||
networkConfiguration.HttpsPortNumber != HttpsPort)
|
networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||||
{
|
{
|
||||||
if (ServerConfigurationManager.Configuration.IsPortAuthorized)
|
if (ConfigurationManager.Configuration.IsPortAuthorized)
|
||||||
{
|
{
|
||||||
ServerConfigurationManager.Configuration.IsPortAuthorized = false;
|
ConfigurationManager.Configuration.IsPortAuthorized = false;
|
||||||
ServerConfigurationManager.SaveConfiguration();
|
ConfigurationManager.SaveConfiguration();
|
||||||
|
|
||||||
requiresRestart = true;
|
requiresRestart = true;
|
||||||
}
|
}
|
||||||
|
@ -1139,16 +1133,16 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
||||||
|
@ -1165,10 +1159,10 @@ namespace Emby.Server.Implementations
|
||||||
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(request, out port);
|
string smart = NetManager.GetBindInterface(request, out port);
|
||||||
|
@ -1185,10 +1179,10 @@ namespace Emby.Server.Implementations
|
||||||
public string GetSmartApiUrl(string hostname, int? port = null)
|
public string GetSmartApiUrl(string hostname, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(hostname, out port);
|
string smart = NetManager.GetBindInterface(hostname, out port);
|
||||||
|
@ -1223,14 +1217,14 @@ namespace Emby.Server.Implementations
|
||||||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
||||||
Host = host,
|
Host = host,
|
||||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
||||||
Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
|
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||||
}.ToString().TrimEnd('/');
|
}.ToString().TrimEnd('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FriendlyName =>
|
public string FriendlyName =>
|
||||||
string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
|
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
|
||||||
? Environment.MachineName
|
? Environment.MachineName
|
||||||
: ServerConfigurationManager.Configuration.ServerName;
|
: ConfigurationManager.Configuration.ServerName;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shuts down.
|
/// Shuts down.
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
private readonly IProviderManager _providerManager;
|
private readonly IProviderManager _providerManager;
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly IMemoryCache _memoryCache;
|
||||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
|
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
|
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -8,11 +9,9 @@ using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Collections;
|
using MediaBrowser.Controller.Collections;
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Plugins;
|
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
@ -107,7 +106,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
|
|
||||||
var name = _localizationManager.GetLocalizedString("Collections");
|
var name = _localizationManager.GetLocalizedString("Collections");
|
||||||
|
|
||||||
await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false);
|
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
|
||||||
|
|
||||||
return FindFolders(path).First();
|
return FindFolders(path).First();
|
||||||
}
|
}
|
||||||
|
@ -124,7 +123,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
|
|
||||||
private IEnumerable<BoxSet> GetCollections(User user)
|
private IEnumerable<BoxSet> GetCollections(User user)
|
||||||
{
|
{
|
||||||
var folder = GetCollectionsFolder(false).Result;
|
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
|
||||||
|
|
||||||
return folder == null
|
return folder == null
|
||||||
? Enumerable.Empty<BoxSet>()
|
? Enumerable.Empty<BoxSet>()
|
||||||
|
@ -167,7 +166,7 @@ namespace Emby.Server.Implementations.Collections
|
||||||
|
|
||||||
parentFolder.AddChild(collection, CancellationToken.None);
|
parentFolder.AddChild(collection, CancellationToken.None);
|
||||||
|
|
||||||
if (options.ItemIdList.Length > 0)
|
if (options.ItemIdList.Count > 0)
|
||||||
{
|
{
|
||||||
await AddToCollectionAsync(
|
await AddToCollectionAsync(
|
||||||
collection.Id,
|
collection.Id,
|
||||||
|
@ -251,11 +250,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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,11 +302,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 />
|
||||||
|
@ -319,11 +310,11 @@ namespace Emby.Server.Implementations.Collections
|
||||||
{
|
{
|
||||||
var results = new Dictionary<Guid, BaseItem>();
|
var results = new Dictionary<Guid, BaseItem>();
|
||||||
|
|
||||||
var allBoxsets = GetCollections(user).ToList();
|
var allBoxSets = GetCollections(user).ToList();
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
if (!(item is ISupportsBoxSetGrouping))
|
if (item is not ISupportsBoxSetGrouping)
|
||||||
{
|
{
|
||||||
results[item.Id] = item;
|
results[item.Id] = item;
|
||||||
}
|
}
|
||||||
|
@ -331,20 +322,44 @@ namespace Emby.Server.Implementations.Collections
|
||||||
{
|
{
|
||||||
var itemId = item.Id;
|
var itemId = item.Id;
|
||||||
|
|
||||||
var currentBoxSets = allBoxsets
|
var itemIsInBoxSet = false;
|
||||||
.Where(i => i.ContainsLinkedChildByItemId(itemId))
|
foreach (var boxSet in allBoxSets)
|
||||||
.ToList();
|
{
|
||||||
|
if (!boxSet.ContainsLinkedChildByItemId(itemId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentBoxSets.Count > 0)
|
itemIsInBoxSet = true;
|
||||||
|
|
||||||
|
results.TryAdd(boxSet.Id, boxSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip any item that is in a box set
|
||||||
|
if (itemIsInBoxSet)
|
||||||
{
|
{
|
||||||
foreach (var boxset in currentBoxSets)
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alreadyInResults = false;
|
||||||
|
// 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)
|
||||||
{
|
{
|
||||||
results[boxset.Id] = boxset;
|
foreach (var childId in video.GetLocalAlternateVersionIds())
|
||||||
|
{
|
||||||
|
if (!results.ContainsKey(childId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
alreadyInResults = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (!alreadyInResults)
|
||||||
{
|
{
|
||||||
results[item.Id] = item;
|
results[itemId] = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Server.Implementations.HttpServer;
|
|
||||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations
|
namespace Emby.Server.Implementations
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -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,7 +1,9 @@
|
||||||
|
#nullable disable
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using SQLitePCL.pretty;
|
using SQLitePCL.pretty;
|
||||||
|
|
||||||
|
@ -59,11 +61,11 @@ namespace Emby.Server.Implementations.Data
|
||||||
|
|
||||||
connection.RunInTransaction(conn =>
|
connection.RunInTransaction(conn =>
|
||||||
{
|
{
|
||||||
conn.ExecuteAll(string.Join(";", queries));
|
conn.ExecuteAll(string.Join(';', queries));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Guid ReadGuidFromBlob(this IResultSetValue result)
|
public static Guid ReadGuidFromBlob(this ResultSetValue result)
|
||||||
{
|
{
|
||||||
return new Guid(result.ToBlob());
|
return new Guid(result.ToBlob());
|
||||||
}
|
}
|
||||||
|
@ -84,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();
|
||||||
|
|
||||||
|
@ -95,58 +97,147 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
private static void CheckName(string name)
|
private static void CheckName(string name)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
|
||||||
throw new ArgumentException("Invalid param name: " + name, nameof(name));
|
throw new ArgumentException("Invalid param name: " + name, nameof(name));
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, double value)
|
public static void TryBind(this IStatement statement, string name, double value)
|
||||||
|
@ -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;
|
||||||
|
@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
connection.RunInTransaction(
|
connection.RunInTransaction(
|
||||||
db =>
|
db =>
|
||||||
{
|
{
|
||||||
db.ExecuteAll(string.Join(";", new[] {
|
db.ExecuteAll(string.Join(';', new[] {
|
||||||
|
|
||||||
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
||||||
|
|
||||||
|
@ -348,16 +350,16 @@ namespace Emby.Server.Implementations.Data
|
||||||
/// 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"></param>
|
||||||
private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
|
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 +367,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,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;
|
||||||
|
@ -665,10 +667,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 +701,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;
|
||||||
}
|
}
|
||||||
|
@ -898,10 +894,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();
|
||||||
|
|
|
@ -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" />
|
||||||
|
@ -27,10 +28,11 @@
|
||||||
<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.7" />
|
||||||
<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.1.0" />
|
||||||
<PackageReference Include="sharpcompress" Version="0.27.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,22 +45,20 @@
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
||||||
<NoWarn>AD0001</NoWarn>
|
<NoWarn>AD0001</NoWarn>
|
||||||
|
<AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
|
||||||
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Localization\iso6392.txt" />
|
<EmbeddedResource Include="Localization\iso6392.txt" />
|
||||||
<EmbeddedResource Include="Localization\countries.json" />
|
<EmbeddedResource Include="Localization\countries.json" />
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -29,7 +30,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The UDP server.
|
/// The UDP server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private UdpServer _udpServer;
|
private UdpServer? _udpServer;
|
||||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
@ -49,10 +50,12 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task RunAsync()
|
public Task RunAsync()
|
||||||
{
|
{
|
||||||
|
CheckDisposed();
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
@ -62,6 +65,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CheckDisposed()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(this.GetType().Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
@ -71,9 +82,8 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
_cancellationTokenSource.Cancel();
|
_cancellationTokenSource.Cancel();
|
||||||
_udpServer.Dispose();
|
|
||||||
_cancellationTokenSource.Dispose();
|
_cancellationTokenSource.Dispose();
|
||||||
_cancellationTokenSource = null;
|
_udpServer?.Dispose();
|
||||||
_udpServer = null;
|
_udpServer = null;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|
|
@ -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,6 +1,5 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Security;
|
using MediaBrowser.Controller.Security;
|
||||||
|
@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
{
|
{
|
||||||
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
|
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
|
||||||
{
|
{
|
||||||
return (AuthorizationInfo)cached;
|
return (AuthorizationInfo)cached!; // Cache should never contain null
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetAuthorization(requestContext);
|
return GetAuthorization(requestContext);
|
||||||
|
@ -55,15 +55,15 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
|
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
|
||||||
in Dictionary<string, string> auth,
|
in Dictionary<string, string>? auth,
|
||||||
in IHeaderDictionary headers,
|
in IHeaderDictionary headers,
|
||||||
in IQueryCollection queryString)
|
in IQueryCollection queryString)
|
||||||
{
|
{
|
||||||
string deviceId = null;
|
string? deviceId = null;
|
||||||
string device = null;
|
string? device = null;
|
||||||
string client = null;
|
string? client = null;
|
||||||
string version = null;
|
string? version = null;
|
||||||
string token = null;
|
string? token = null;
|
||||||
|
|
||||||
if (auth != null)
|
if (auth != null)
|
||||||
{
|
{
|
||||||
|
@ -206,7 +206,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
/// <param name="httpReq">The HTTP req.</param>
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
|
private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
|
||||||
{
|
{
|
||||||
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
|
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
|
||||||
|
|
||||||
|
@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
auth = httpReq.Request.Headers[HeaderNames.Authorization];
|
auth = httpReq.Request.Headers[HeaderNames.Authorization];
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetAuthorization(auth);
|
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
/// <param name="httpReq">The HTTP req.</param>
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
|
private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
|
||||||
{
|
{
|
||||||
var auth = httpReq.Headers["X-Emby-Authorization"];
|
var auth = httpReq.Headers["X-Emby-Authorization"];
|
||||||
|
|
||||||
|
@ -232,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
auth = httpReq.Headers[HeaderNames.Authorization];
|
auth = httpReq.Headers[HeaderNames.Authorization];
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetAuthorization(auth);
|
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -240,43 +240,43 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="authorizationHeader">The authorization header.</param>
|
/// <param name="authorizationHeader">The authorization header.</param>
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
private Dictionary<string, string> GetAuthorization(string authorizationHeader)
|
private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
|
||||||
{
|
{
|
||||||
if (authorizationHeader == null)
|
if (authorizationHeader == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = authorizationHeader.Split(' ', 2);
|
var firstSpace = authorizationHeader.IndexOf(' ');
|
||||||
|
|
||||||
// There should be at least to parts
|
// There should be at least two parts
|
||||||
if (parts.Length != 2)
|
if (firstSpace == -1)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var acceptedNames = new[] { "MediaBrowser", "Emby" };
|
var name = authorizationHeader[..firstSpace];
|
||||||
|
|
||||||
// It has to be a digest request
|
if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
|
||||||
if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
|
&& !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove uptil the first space
|
authorizationHeader = authorizationHeader[(firstSpace + 1)..];
|
||||||
authorizationHeader = parts[1];
|
|
||||||
parts = authorizationHeader.Split(',');
|
|
||||||
|
|
||||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var item in parts)
|
foreach (var item in authorizationHeader.Split(','))
|
||||||
{
|
{
|
||||||
var param = item.Trim().Split('=', 2);
|
var trimmedItem = item.Trim();
|
||||||
|
var firstEqualsSign = trimmedItem.IndexOf('=');
|
||||||
|
|
||||||
if (param.Length == 2)
|
if (firstEqualsSign > 0)
|
||||||
{
|
{
|
||||||
var value = NormalizeValue(param[1].Trim('"'));
|
var key = trimmedItem[..firstEqualsSign].ToString();
|
||||||
result[param[0]] = value;
|
var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString());
|
||||||
|
result[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
var authorization = _authContext.GetAuthorizationInfo(requestContext);
|
var authorization = _authContext.GetAuthorizationInfo(requestContext);
|
||||||
|
|
||||||
var user = authorization.User;
|
var user = authorization.User;
|
||||||
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user);
|
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SessionInfo GetSession(object requestContext)
|
public SessionInfo GetSession(object requestContext)
|
||||||
|
@ -36,14 +36,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
return GetSession((HttpContext)requestContext);
|
return GetSession((HttpContext)requestContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User GetUser(HttpContext requestContext)
|
public User? GetUser(HttpContext requestContext)
|
||||||
{
|
{
|
||||||
var session = GetSession(requestContext);
|
var session = GetSession(requestContext);
|
||||||
|
|
||||||
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 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;
|
||||||
|
@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
RemoteEndPoint = remoteEndPoint;
|
RemoteEndPoint = remoteEndPoint;
|
||||||
QueryString = query;
|
QueryString = query;
|
||||||
|
|
||||||
_jsonOptions = JsonDefaults.GetOptions();
|
_jsonOptions = JsonDefaults.Options;
|
||||||
LastActivityDate = DateTime.Now;
|
LastActivityDate = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -14,15 +16,18 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
public class WebSocketManager : IWebSocketManager
|
public class WebSocketManager : IWebSocketManager
|
||||||
{
|
{
|
||||||
private readonly IWebSocketListener[] _webSocketListeners;
|
private readonly IWebSocketListener[] _webSocketListeners;
|
||||||
|
private readonly IAuthService _authService;
|
||||||
private readonly ILogger<WebSocketManager> _logger;
|
private readonly ILogger<WebSocketManager> _logger;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
|
||||||
public WebSocketManager(
|
public WebSocketManager(
|
||||||
|
IAuthService authService,
|
||||||
IEnumerable<IWebSocketListener> webSocketListeners,
|
IEnumerable<IWebSocketListener> webSocketListeners,
|
||||||
ILogger<WebSocketManager> logger,
|
ILogger<WebSocketManager> logger,
|
||||||
ILoggerFactory loggerFactory)
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_webSocketListeners = webSocketListeners.ToArray();
|
_webSocketListeners = webSocketListeners.ToArray();
|
||||||
|
_authService = authService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
}
|
}
|
||||||
|
@ -30,6 +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);
|
||||||
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