merge branch 'master' into syncplay-sessions-fix

This commit is contained in:
dkanada 2021-09-06 13:42:48 +09:00
commit f6c0db4bb5
1105 changed files with 23997 additions and 14632 deletions

View File

@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
default: 5.0.103
default: 5.0.302
jobs:
- job: CompatibilityCheck

View File

@ -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

View File

@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
DotNetSdkVersion: 5.0.103
DotNetSdkVersion: 5.0.302
jobs:
- job: Build

View File

@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
default: 5.0.103
default: 5.0.302
jobs:
- job: Test

View File

@ -6,7 +6,7 @@ variables:
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion
value: 5.0.103
value: 5.0.302
pr:
autoCancel: true
@ -61,6 +61,3 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-api-client.yml

View File

@ -14,9 +14,11 @@ assignees: ''
- OS: [e.g. Debian, Windows]
- Virtualization: [e.g. Docker, KVM, LXC]
- Clients: [Browser, Android, Fire Stick, etc.]
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
- Browser: [e.g. Firefox 91, Chrome 93, Safari 13]
- Jellyfin Version: [e.g. 10.7.6, unstable 20191231]
- FFmpeg Version: [e.g. 4.3.2-Jellyfin]
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
- Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
- Base URL: [e.g. none, yes: /example]
@ -33,7 +35,13 @@ assignees: ''
**Expected behavior**
<!-- 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. -->
**Screenshots**

View File

@ -6,4 +6,10 @@ updates:
interval: weekly
time: '12:00'
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
time: '12:00'
open-pull-requests-limit: 10

6
.github/stale.yml vendored
View File

@ -17,9 +17,13 @@ staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
# Disable automatic closing of pull requests
pulls:
daysUntilClose: false

76
.github/workflows/automation.yml vendored Normal file
View File

@ -0,0 +1,76 @@
name: Automation
on:
push:
branches:
- master
pull_request_target:
issue_comment:
jobs:
label:
name: Labeling
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
uses: eps1lon/actions-label-merge-conflict@v2.0.1
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
repoToken: ${{ secrets.JF_BOT_TOKEN }}
project:
name: Project board
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Remove from 'Current Release' project
uses: alex-page/github-project-automation-plus@v0.8.1
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
project: Current Release
action: delete
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project
uses: alex-page/github-project-automation-plus@v0.8.1
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true
with:
project: Release Next
column: In progress
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project
uses: alex-page/github-project-automation-plus@v0.8.1
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
project: Current Release
column: In progress
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Check number of comments from the team member
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
id: member_comments
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
- name: Move issue to needs triage
uses: alex-page/github-project-automation-plus@v0.8.1
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true
with:
project: Issue Triage for Main Repo
column: Needs triage
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project
uses: alex-page/github-project-automation-plus@v0.8.1
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true
with:
project: Issue Triage for Main Repo
column: Pending response
repo-token: ${{ secrets.JF_BOT_TOKEN }}

119
.github/workflows/commands.yml vendored Normal file
View File

@ -0,0 +1,119 @@
name: Commands
on:
issue_comment:
types:
- created
- edited
pull_request_target:
types:
- labeled
- synchronize
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@v1.4.5
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.5
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
check-backport:
name: Check Backport
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@v1.4.5
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: eyes
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
uses: peter-evans/create-or-update-comment@v1.4.5
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Running backport tests...
- name: Perform test backport
id: run_tests
run: |
set +o errexit
git config --global user.name "Jellyfin Bot"
git config --global user.email "team@jellyfin.org"
CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
git checkout master
git merge --no-ff ${CURRENT_BRANCH}
MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
git fetch --all
CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
stable_branch="Current stable release branch: ${CURRENT_STABLE}"
echo ${stable_branch}
echo ::set-output name=branch::${stable_branch}
git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
retcode=$?
cat output.txt | grep -v 'hint:'
output="$( grep -v 'hint:' output.txt )"
output="${output//'%'/'%25'}"
output="${output//$'\n'/'%0A'}"
output="${output//$'\r'/'%0D'}"
echo ::set-output name=output::$output
exit ${retcode}
- name: Notify with result success
uses: peter-evans/create-or-update-comment@v1.4.5
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ steps.comment_running.outputs.comment-id }}
body: |
${{ steps.run_tests.outputs.branch }}
Output from `git cherry-pick`:
---
${{ steps.run_tests.outputs.output }}
reactions: hooray
- name: Notify with result failure
uses: peter-evans/create-or-update-comment@v1.4.5
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ steps.comment_running.outputs.comment-id }}
body: |
${{ steps.run_tests.outputs.branch }}
Output from `git cherry-pick`:
---
${{ steps.run_tests.outputs.output }}
reactions: confused

4
.gitignore vendored
View File

@ -268,6 +268,7 @@ doc/
# Deployment artifacts
dist
*.exe
*.dll
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts
@ -277,3 +278,6 @@ web/
web-src.*
MediaBrowser.WebDashboard/jellyfin-web
apiclient/generated
# Omnisharp crash logs
mono_crash.*.json

View File

@ -17,6 +17,7 @@
- [bugfixin](https://github.com/bugfixin)
- [chaosinnovator](https://github.com/chaosinnovator)
- [ckcr4lyf](https://github.com/ckcr4lyf)
- [cocool97](https://github.com/cocool97)
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
- [crankdoofus](https://github.com/crankdoofus)
- [crobibero](https://github.com/crobibero)
@ -45,6 +46,7 @@
- [fruhnow](https://github.com/fruhnow)
- [geilername](https://github.com/geilername)
- [gnattu](https://github.com/gnattu)
- [GodTamIt](https://github.com/GodTamIt)
- [grafixeyehero](https://github.com/grafixeyehero)
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
@ -69,6 +71,7 @@
- [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro)
- [Matt07211](https://github.com/Matt07211)
- [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00)
- [mitchfizz05](https://github.com/mitchfizz05)
- [MrTimscampi](https://github.com/MrTimscampi)
@ -105,10 +108,11 @@
- [shemanaev](https://github.com/shemanaev)
- [skaro13](https://github.com/skaro13)
- [sl1288](https://github.com/sl1288)
- [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
- [ssenart] (https://github.com/ssenart)
- [ssenart](https://github.com/ssenart)
- [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles)
- [SuperSandro2000](https://github.com/SuperSandro2000)
@ -144,6 +148,7 @@
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
- [peterspenler](https://github.com/peterspenler)
# Emby Contributors
@ -208,3 +213,5 @@
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [olsh](https://github.com/olsh)
- [lbenini](https://github.com/lbenini)
- [gnuyent](https://github.com/gnuyent)

14
Directory.Build.props Normal file
View File

@ -0,0 +1,14 @@
<Project>
<!-- Sets defaults for all projects in the repo -->
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
</PropertyGroup>
</Project>

View File

@ -1,22 +1,14 @@
ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder
FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm 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 - \
&& cd jellyfin-web-* \
&& yarn install \
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
FROM debian:buster-slim
FROM debian:bullseye-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@ -25,9 +17,6 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
# https://github.com/intel/compute-runtime/releases
ARG GMMLIB_VERSION=20.3.2
ARG IGC_VERSION=1.0.5435
@ -73,6 +62,19 @@ ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
FROM app
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \

View File

@ -5,27 +5,16 @@
ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder
FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm 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 - \
&& cd jellyfin-web-* \
&& yarn install \
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-arm as qemu
FROM arm32v7/debian:buster-slim
FROM arm32v7/debian:bullseye-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@ -61,14 +50,25 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
FROM app
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \

View File

@ -5,26 +5,16 @@
ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder
FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm 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 - \
&& cd jellyfin-web-* \
&& yarn install \
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM arm64v8/debian:buster-slim
FROM arm64v8/debian:bullseye-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@ -50,14 +40,25 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
FROM app
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \

View File

@ -13,7 +13,8 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
<Nullable>disable</Nullable>
</PropertyGroup>
</Project>

View File

@ -72,7 +72,7 @@ namespace Emby.Dlna.Configuration
/// <summary>
/// Gets or sets the default user account that the dlna server uses.
/// </summary>
public string DefaultUserId { get; set; }
public string? DefaultUserId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether playTo device profiles should be created.

View File

@ -1,4 +1,3 @@
#nullable enable
#pragma warning disable CS1591
using Emby.Dlna.Configuration;

View File

@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
}
/// <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))
{

View File

@ -138,7 +138,7 @@ namespace Emby.Dlna.ContentDirectory
/// </summary>
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
/// <returns>The <see cref="User"/>.</returns>
private User GetUser(DeviceProfile profile)
private User? GetUser(DeviceProfile profile)
{
if (!string.IsNullOrEmpty(profile.UserId))
{

View File

@ -1,5 +1,6 @@
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -7,7 +8,6 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
using Emby.Dlna.Configuration;
using Emby.Dlna.Didl;
using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
@ -121,7 +121,7 @@ namespace Emby.Dlna.ContentDirectory
}
/// <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)
{
@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory
/// <summary>
/// Adds a "XSetBookmark" element to the xml document.
/// </summary>
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
private void HandleXSetBookmark(IDictionary<string, string> sparams)
/// <param name="sparams">The method parameters.</param>
private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams)
{
var id = sparams["ObjectID"];
@ -288,52 +288,28 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The xml feature list.</returns>
private static string WriteFeatureListXml()
{
// TODO: clean this up
var builder = new StringBuilder();
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
builder.Append("<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">");
builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">");
builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>");
builder.Append("<container id=\"A\" type=\"object.item.audioItem\"/>");
builder.Append("<container id=\"V\" type=\"object.item.videoItem\"/>");
builder.Append("</Feature>");
builder.Append("</Features>");
return builder.ToString();
}
/// <summary>
/// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist.
/// </summary>
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
/// <param name="key">The key.</param>
/// <param name="defaultValue">The defaultValue.</param>
/// <returns>The <see cref="string"/>.</returns>
public static string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue)
{
if (sparams != null && sparams.TryGetValue(key, out string val))
{
return val;
}
return defaultValue;
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">"
+ "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"
+ "<container id=\"I\" type=\"object.item.imageItem\"/>"
+ "<container id=\"A\" type=\"object.item.audioItem\"/>"
+ "<container id=\"V\" type=\"object.item.videoItem\"/>"
+ "</Feature>"
+ "</Features>";
}
/// <summary>
/// Builds the "Browse" xml response.
/// </summary>
/// <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>
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 flag = sparams["BrowseFlag"];
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
var provided = 0;
@ -435,9 +411,9 @@ namespace Emby.Dlna.ContentDirectory
/// Builds the response to the "X_BrowseByLetter request.
/// </summary>
/// <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>
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
HandleSearch(xmlWriter, sparams, deviceId);
@ -447,13 +423,13 @@ namespace Emby.Dlna.ContentDirectory
/// Builds a response to the "Search" request.
/// </summary>
/// <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>
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 sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty));
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
// sort example: dc:title, dc:date

View File

@ -17,7 +17,7 @@ namespace Emby.Dlna.ContentDirectory
{
Item = item;
if (item is IItemByName && !(item is Folder))
if (item is IItemByName && item is not Folder)
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System.IO;

View File

@ -6,9 +6,11 @@ namespace Emby.Dlna
{
public class ControlResponse
{
public ControlResponse()
public ControlResponse(string xml, bool isSuccessful)
{
Headers = new Dictionary<string, string>();
Xml = xml;
IsSuccessful = isSuccessful;
}
public IDictionary<string, string> Headers { get; }

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -208,7 +210,8 @@ namespace Emby.Dlna.Didl
var targetWidth = streamInfo.TargetWidth;
var targetHeight = streamInfo.TargetHeight;
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
_profile,
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(),
@ -599,7 +602,8 @@ namespace Emby.Dlna.Didl
? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType;
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
_profile,
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
targetAudioBitrate,
@ -744,7 +748,7 @@ namespace Emby.Dlna.Didl
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
}
if (!(item is Folder))
if (item is not Folder)
{
if (filter.Contains("dc:description"))
{
@ -974,15 +978,28 @@ namespace Emby.Dlna.Didl
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.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
writer.WriteString(albumartUrlInfo.url);
if (!string.IsNullOrEmpty(_profile.AlbumArtPn))
{
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
}
writer.WriteString(albumArtUrlInfo.url);
writer.WriteFullEndElement();
// TOOD: Remove these default values
var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
// TODO: Remove these default values
var iconUrlInfo = GetImageUrl(
imageInfo,
_profile.MaxIconWidth ?? 48,
_profile.MaxIconHeight ?? 48,
"jpg");
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
if (!_profile.EnableAlbumArtInDidl)
@ -1033,8 +1050,7 @@ namespace Emby.Dlna.Didl
var width = albumartUrlInfo.width ?? maxWidth;
var height = albumartUrlInfo.height ?? maxHeight;
var contentFeatures = new ContentFeatureBuilder(_profile)
.BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
writer.WriteAttributeString(
"protocolInfo",
@ -1206,8 +1222,7 @@ namespace Emby.Dlna.Didl
if (width.HasValue && height.HasValue)
{
var newSize = DrawingUtils.Resize(
new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
width = newSize.Width;
height = newSize.Height;

View File

@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
{
public class StringWriterWithEncoding : StringWriter
{
private readonly Encoding _encoding;
private readonly Encoding? _encoding;
public StringWriterWithEncoding()
{

View File

@ -1,4 +1,3 @@
#nullable enable
#pragma warning disable CS1591
using System.Collections.Generic;

View File

@ -1,5 +1,4 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
@ -12,9 +11,9 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Emby.Dlna.Profiles;
using Emby.Dlna.Server;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@ -94,12 +93,14 @@ namespace Emby.Dlna
}
}
/// <inheritdoc />
public DeviceProfile GetDefaultProfile()
{
return new DefaultProfile();
}
public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
/// <inheritdoc />
public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
{
if (deviceInfo == null)
{
@ -109,13 +110,13 @@ namespace Emby.Dlna
var profile = GetProfiles()
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
if (profile != null)
if (profile == null)
{
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
LogUnmatchedProfile(deviceInfo);
}
else
{
LogUnmatchedProfile(deviceInfo);
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
}
return profile;
@ -126,92 +127,57 @@ namespace Emby.Dlna
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
builder.Append("ModelName:").AppendLine(profile.ModelName);
builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
builder.Append("ModelName: ").AppendLine(profile.ModelName);
builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString());
}
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
/// <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))
{
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
{
if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
{
if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
{
if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelName))
{
if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
{
if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
{
if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
{
if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
{
return false;
}
}
return true;
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
}
private bool IsRegexOrSubstringMatch(string input, string pattern)
{
if (string.IsNullOrEmpty(pattern))
{
// In profile identification: An empty pattern matches anything.
return true;
}
if (string.IsNullOrEmpty(input))
{
// The profile contains a value, and the device doesn't.
return false;
}
try
{
return input.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)
{
@ -220,7 +186,8 @@ namespace Emby.Dlna
}
}
public DeviceProfile GetProfile(IHeaderDictionary headers)
/// <inheritdoc />
public DeviceProfile? GetProfile(IHeaderDictionary headers)
{
if (headers == null)
{
@ -228,15 +195,13 @@ namespace Emby.Dlna
}
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
if (profile != null)
if (profile == null)
{
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
_logger.LogDebug("No matching device profile found. {@Headers}", headers);
}
else
{
var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
_logger.LogDebug("No matching device profile found. {0}", headerString);
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
}
return profile;
@ -286,19 +251,19 @@ namespace Emby.Dlna
return xmlFies
.Select(i => ParseProfileFile(i, type))
.Where(i => i != null)
.ToList();
.ToList()!; // We just filtered out all the nulls
}
catch (IOException)
{
return new List<DeviceProfile>();
return Array.Empty<DeviceProfile>();
}
}
private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
{
lock (_profiles)
{
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple))
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
{
return profileTuple.Item2;
}
@ -326,7 +291,8 @@ namespace Emby.Dlna
}
}
public DeviceProfile GetProfile(string id)
/// <inheritdoc />
public DeviceProfile? GetProfile(string id)
{
if (string.IsNullOrEmpty(id))
{
@ -355,6 +321,7 @@ namespace Emby.Dlna
}
}
/// <inheritdoc />
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
{
return GetProfileInfosInternal().Select(i => i.Info);
@ -362,17 +329,14 @@ namespace Emby.Dlna
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
{
return new InternalProfileInfo
{
Path = file.FullName,
Info = new DeviceProfileInfo
return new InternalProfileInfo(
new DeviceProfileInfo
{
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
Name = _fileSystem.GetFileNameWithoutExtension(file),
Type = type
}
};
},
file.FullName);
}
private async Task ExtractSystemProfilesAsync()
@ -392,7 +356,8 @@ namespace Emby.Dlna
systemProfilesPath,
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
using (var stream = _assembly.GetManifestResourceStream(name))
// The stream should exist as we just got its name from GetManifestResourceNames
using (var stream = _assembly.GetManifestResourceStream(name)!)
{
var fileInfo = _fileSystem.GetFileInfo(path);
@ -413,6 +378,7 @@ namespace Emby.Dlna
Directory.CreateDirectory(UserProfilesPath);
}
/// <inheritdoc />
public void DeleteProfile(string id)
{
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
@ -430,6 +396,7 @@ namespace Emby.Dlna
}
}
/// <inheritdoc />
public void CreateProfile(DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@ -445,6 +412,7 @@ namespace Emby.Dlna
SaveProfile(profile, path, DeviceProfileType.User);
}
/// <inheritdoc />
public void UpdateProfile(DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@ -503,9 +471,11 @@ namespace Emby.Dlna
var json = JsonSerializer.Serialize(profile, _jsonOptions);
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
// Output can't be null if the input isn't null
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
}
/// <inheritdoc />
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{
var profile = GetDefaultProfile();
@ -515,6 +485,7 @@ namespace Emby.Dlna
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
}
/// <inheritdoc />
public ImageStream GetIcon(string filename)
{
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
@ -532,9 +503,15 @@ namespace Emby.Dlna
private class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
internal InternalProfileInfo(DeviceProfileInfo info, string path)
{
Info = info;
Path = path;
}
internal string Path { get; set; }
internal DeviceProfileInfo Info { get; }
internal string Path { get; }
}
}

View File

@ -20,7 +20,7 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<!-- Code Analyzers-->
@ -30,10 +30,6 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Images\logo120.jpg" />
<EmbeddedResource Include="Images\logo120.png" />

View File

@ -6,8 +6,10 @@ namespace Emby.Dlna
{
public class EventSubscriptionResponse
{
public EventSubscriptionResponse()
public EventSubscriptionResponse(string content, string contentType)
{
Content = content;
ContentType = contentType;
Headers = new Dictionary<string, string>();
}

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -49,11 +51,7 @@ namespace Emby.Dlna.Eventing
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
}
return new EventSubscriptionResponse
{
Content = string.Empty,
ContentType = "text/plain"
};
return new EventSubscriptionResponse(string.Empty, "text/plain");
}
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
@ -101,20 +99,12 @@ namespace Emby.Dlna.Eventing
_subscriptions.TryRemove(subscriptionId, out _);
return new EventSubscriptionResponse
{
Content = string.Empty,
ContentType = "text/plain"
};
return new EventSubscriptionResponse(string.Empty, "text/plain");
}
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
{
var response = new EventSubscriptionResponse
{
Content = string.Empty,
ContentType = "text/plain"
};
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
response.Headers["SID"] = subscriptionId;
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -5,7 +7,6 @@ using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
@ -26,11 +27,9 @@ using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
using Rssdp;
using Rssdp.Infrastructure;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Dlna.Main
{
@ -203,8 +202,8 @@ namespace Emby.Dlna.Main
{
if (_communicationsServer == null)
{
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
OperatingSystem.Id == OperatingSystemId.Linux;
var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
OperatingSystem.IsLinux();
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{
@ -267,7 +266,12 @@ namespace Emby.Dlna.Main
try
{
_publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
_publisher = new SsdpDevicePublisher(
_communicationsServer,
_networkManager,
MediaBrowser.Common.System.OperatingSystem.Name,
Environment.OSVersion.VersionString,
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
LogFunction = LogMessage,
SupportPnpRootDevice = false

View File

@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
}
/// <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))
{

View File

@ -1,7 +1,6 @@
using System.Collections.Generic;
using Emby.Dlna.Common;
using Emby.Dlna.Service;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -219,7 +221,7 @@ namespace Emby.Dlna.PlayTo
{
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
if (command == null)
{
return false;
@ -259,7 +261,7 @@ namespace Emby.Dlna.PlayTo
{
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
if (command == null)
{
return;
@ -290,7 +292,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
if (command == null)
{
return;
@ -323,7 +325,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
if (command == null)
{
return;
@ -368,6 +370,42 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true);
}
/*
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
* Without that information, the next track command on the device does not work.
*/
public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
url = url.Replace("&", "&amp;", StringComparison.Ordinal);
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
if (command == null)
{
return;
}
var dictionary = new Dictionary<string, string>
{
{ "NextURI", url },
{ "NextURIMetaData", CreateDidlMeta(metaData) }
};
var service = GetAvTransportService();
if (service == null)
{
throw new InvalidOperationException("Unable to find service");
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
.ConfigureAwait(false);
}
private static string CreateDidlMeta(string value)
{
if (string.IsNullOrEmpty(value))
@ -403,6 +441,10 @@ namespace Emby.Dlna.PlayTo
public async Task SetPlay(CancellationToken cancellationToken)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
if (avCommands == null)
{
return;
}
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
@ -413,7 +455,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
if (command == null)
{
return;
@ -437,7 +479,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
if (command == null)
{
return;
@ -565,7 +607,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
if (command == null)
{
return;
@ -615,7 +657,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
if (command == null)
{
return;
@ -702,6 +744,10 @@ namespace Emby.Dlna.PlayTo
}
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
if (rendererCommands == null)
{
return null;
}
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
@ -770,6 +816,11 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
if (rendererCommands == null)
{
return (false, null);
}
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
@ -951,6 +1002,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory);
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
AvCommands = TransportCommands.Create(document);
return AvCommands;
@ -979,6 +1034,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory);
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
RendererCommands = TransportCommands.Create(document);
return RendererCommands;
@ -1010,6 +1069,10 @@ namespace Emby.Dlna.PlayTo
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
var friendlyNames = new List<string>();
@ -1197,10 +1260,7 @@ namespace Emby.Dlna.PlayTo
return;
}
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
{
MediaInfo = mediaInfo
});
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
}
private void OnPlaybackProgress(UBaseObject mediaInfo)
@ -1210,27 +1270,17 @@ namespace Emby.Dlna.PlayTo
return;
}
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
{
MediaInfo = mediaInfo
});
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo));
}
private void OnPlaybackStop(UBaseObject mediaInfo)
{
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
{
MediaInfo = mediaInfo
});
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo));
}
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
{
MediaChanged?.Invoke(this, new MediaChangedEventArgs
{
OldMediaInfo = old,
NewMediaInfo = newMedia
});
MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia));
}
/// <inheritdoc />

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;

View File

@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using System;
@ -6,6 +6,12 @@ namespace Emby.Dlna.PlayTo
{
public class MediaChangedEventArgs : EventArgs
{
public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
{
OldMediaInfo = oldMediaInfo;
NewMediaInfo = newMediaInfo;
}
public UBaseObject OldMediaInfo { get; set; }
public UBaseObject NewMediaInfo { get; set; }

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -102,6 +104,22 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
}
/*
* Send a message to the DLNA device to notify what is the next track in the playlist.
*/
private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
{
if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
{
// The current playing item is indeed in the play list and we are not yet at the end of the playlist.
var nextItemIndex = currentPlayListItemIndex + 1;
var nextItem = _playlist[nextItemIndex];
// Send the SetNextAvTransport message.
await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
}
}
private void OnDeviceUnavailable()
{
try
@ -156,6 +174,15 @@ namespace Emby.Dlna.PlayTo
var newItemProgress = GetProgressInfo(streamInfo);
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
// Send a message to the DLNA device to notify what is the next track in the playlist.
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
if (currentItemIndex >= 0)
{
_currentPlaylistIndex = currentItemIndex;
}
await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
}
catch (Exception ex)
{
@ -425,6 +452,11 @@ namespace Emby.Dlna.PlayTo
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
// Send a message to the DLNA device to notify what is the next track in the play list.
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
return;
}
@ -499,8 +531,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Audio)
{
return new ContentFeatureBuilder(profile)
.BuildAudioHeader(
return ContentFeatureBuilder.BuildAudioHeader(
profile,
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate,
@ -514,8 +546,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Video)
{
var list = new ContentFeatureBuilder(profile)
.BuildVideoHeader(
var list = ContentFeatureBuilder.BuildVideoHeader(
profile,
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(),
@ -623,6 +655,9 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
// Send a message to the DLNA device to notify what is the next track in the play list.
await SendNextTrackMessage(index, cancellationToken);
var streamInfo = currentitem.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);
// 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))
{
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);
// 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)
{
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
@ -943,11 +986,7 @@ namespace Emby.Dlna.PlayTo
request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
// 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.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -171,19 +173,26 @@ namespace Emby.Dlna.PlayTo
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
var sessionInfo = await _sessionManager
.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
.ConfigureAwait(false);
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
if (controller == null)
{
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
if (device == null)
{
_logger.LogError("Ignoring device as xml response is invalid.");
return;
}
string deviceName = device.Properties.Name;
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
controller = new PlayToController(
sessionInfo,

View File

@ -6,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackProgressEventArgs : EventArgs
{
public PlaybackProgressEventArgs(UBaseObject mediaInfo)
{
MediaInfo = mediaInfo;
}
public UBaseObject MediaInfo { get; set; }
}
}

View File

@ -6,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStartEventArgs : EventArgs
{
public PlaybackStartEventArgs(UBaseObject mediaInfo)
{
MediaInfo = mediaInfo;
}
public UBaseObject MediaInfo { get; set; }
}
}

View File

@ -6,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStoppedEventArgs : EventArgs
{
public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
{
MediaInfo = mediaInfo;
}
public UBaseObject MediaInfo { get; set; }
}
}

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using MediaBrowser.Model.Dlna;

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System.IO;

View File

@ -1,8 +1,9 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
@ -94,10 +95,17 @@ namespace Emby.Dlna.PlayTo
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
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);
return await XDocument.LoadAsync(
stream,
LoadOptions.PreserveWhitespace,
cancellationToken).ConfigureAwait(false);
try
{
return await XDocument.LoadAsync(
stream,
LoadOptions.PreserveWhitespace,
cancellationToken).ConfigureAwait(false);
}
catch
{
return null;
}
}
private async Task<HttpResponseMessage> PostSoapDataAsync(

View File

@ -46,7 +46,7 @@ namespace Emby.Dlna.PlayTo
{
var serviceAction = new ServiceAction
{
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
};
var argumentList = serviceAction.ArgumentList;
@ -68,9 +68,9 @@ namespace Emby.Dlna.PlayTo
return new Argument
{
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
};
}
@ -89,8 +89,8 @@ namespace Emby.Dlna.PlayTo
return new StateVariable
{
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
AllowedValues = allowedValues
};
}
@ -166,7 +166,7 @@ namespace Emby.Dlna.PlayTo
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
}
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
{
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@ -250,7 +250,8 @@ namespace Emby.Dlna.Server
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()

View File

@ -6,9 +6,9 @@ using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Diacritics.Extensions;
using Emby.Dlna.Didl;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Service
@ -47,7 +47,7 @@ namespace Emby.Dlna.Service
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
{
ControlRequestInfo requestInfo = null;
ControlRequestInfo? requestInfo = null;
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
{
@ -95,11 +95,7 @@ namespace Emby.Dlna.Service
var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
var controlResponse = new ControlResponse
{
Xml = xml,
IsSuccessful = true
};
var controlResponse = new ControlResponse(xml, true);
controlResponse.Headers.Add("EXT", string.Empty);
@ -151,7 +147,7 @@ namespace Emby.Dlna.Service
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
{
string namespaceURI = null, localName = null;
string? namespaceURI = null, localName = null;
await reader.MoveToContentAsync().ConfigureAwait(false);
await reader.ReadAsync().ConfigureAwait(false);
@ -210,7 +206,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)
{

View File

@ -46,11 +46,7 @@ namespace Emby.Dlna.Service
writer.WriteEndDocument();
}
return new ControlResponse
{
Xml = builder.ToString(),
IsSuccessful = false
};
return new ControlResponse(builder.ToString(), false);
}
}
}

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp
{
Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers,
LocalIpAddress = e.LocalIpAddress
RemoteIpAddress = e.RemoteIpAddress
});
DeviceDiscoveredInternal?.Invoke(this, args);

View File

@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp
{
public static class SsdpExtensions
{
public static string GetValue(this XElement container, XName name)
public static string? GetValue(this XElement container, XName name)
{
var node = container.Element(name);
return node?.Value;
}
public static string GetAttributeValue(this XElement container, XName name)
public static string? GetAttributeValue(this XElement container, XName name)
{
var node = container.Attribute(name);
return node?.Value;
}
public static string GetDescendantValue(this XElement container, XName name)
public static string? GetDescendantValue(this XElement container, XName name)
=> container.Descendants(name).FirstOrDefault()?.Value;
}
}

View File

@ -9,8 +9,7 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<ItemGroup>
@ -30,8 +29,4 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
@ -171,21 +172,31 @@ namespace Emby.Drawing
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
int quality = options.Quality;
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
string cacheFilePath = GetCacheFilePath(originalImagePath, 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
{
if (!File.Exists(cacheFilePath))
{
if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
{
options.CropWhiteSpace = false;
}
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
@ -246,48 +257,111 @@ namespace Emby.Drawing
/// <summary>
/// Gets the cache file path based on a set of parameters.
/// </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
+ "width=" + outputSize.Width
+ "height=" + outputSize.Height
+ "quality=" + quality
+ "datemodified=" + dateModified.Ticks
+ "f=" + format;
var filename = new StringBuilder(256);
filename.Append(originalPath);
filename.Append(",quality=");
filename.Append(quality);
filename.Append(",datemodified=");
filename.Append(dateModified.Ticks);
filename.Append(",f=");
filename.Append(format);
if (width.HasValue)
{
filename.Append(",width=");
filename.Append(width.Value);
}
if (height.HasValue)
{
filename.Append(",height=");
filename.Append(height.Value);
}
if (maxWidth.HasValue)
{
filename.Append(",maxwidth=");
filename.Append(maxWidth.Value);
}
if (maxHeight.HasValue)
{
filename.Append(",maxheight=");
filename.Append(maxHeight.Value);
}
if (fillWidth.HasValue)
{
filename.Append(",fillwidth=");
filename.Append(fillWidth.Value);
}
if (fillHeight.HasValue)
{
filename.Append(",fillheight=");
filename.Append(fillHeight.Value);
}
if (addPlayedIndicator)
{
filename += "pl=true";
filename.Append(",pl=true");
}
if (percentPlayed > 0)
{
filename += "p=" + percentPlayed;
filename.Append(",p=");
filename.Append(percentPlayed);
}
if (unwatchedCount.HasValue)
{
filename += "p=" + unwatchedCount.Value;
filename.Append(",p=");
filename.Append(unwatchedCount.Value);
}
if (blur.HasValue)
{
filename += "blur=" + blur.Value;
filename.Append(",blur=");
filename.Append(blur.Value);
}
if (!string.IsNullOrEmpty(backgroundColor))
{
filename += "b=" + backgroundColor;
filename.Append(",b=");
filename.Append(backgroundColor);
}
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 />

View File

@ -32,7 +32,7 @@ namespace Emby.Drawing
=> throw new NotImplementedException();
/// <inheritdoc />
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
{
throw new NotImplementedException();
}

View File

@ -1,7 +1,7 @@
using System;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Jellyfin.Extensions;
namespace Emby.Naming.Audio
{
@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
/// <returns>True if file at path is audio file.</returns>
public static bool IsAudioFile(string path, NamingOptions options)
{
var extension = Path.GetExtension(path);
return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
var extension = Path.GetExtension(path.AsSpan());
return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -15,7 +15,7 @@ namespace Emby.Naming.AudioBook
/// <param name="files">List of files composing the actual audiobook.</param>
/// <param name="extras">List of extra files.</param>
/// <param name="alternateVersions">Alternative version of files.</param>
public AudioBookInfo(string name, int? year, List<AudioBookFileInfo> files, List<AudioBookFileInfo> extras, List<AudioBookFileInfo> alternateVersions)
public AudioBookInfo(string name, int? year, IReadOnlyList<AudioBookFileInfo> files, IReadOnlyList<AudioBookFileInfo> extras, IReadOnlyList<AudioBookFileInfo> alternateVersions)
{
Name = name;
Year = year;
@ -39,18 +39,18 @@ namespace Emby.Naming.AudioBook
/// Gets or sets the files.
/// </summary>
/// <value>The files.</value>
public List<AudioBookFileInfo> Files { get; set; }
public IReadOnlyList<AudioBookFileInfo> Files { get; set; }
/// <summary>
/// Gets or sets the extras.
/// </summary>
/// <value>The extras.</value>
public List<AudioBookFileInfo> Extras { get; set; }
public IReadOnlyList<AudioBookFileInfo> Extras { get; set; }
/// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public List<AudioBookFileInfo> AlternateVersions { get; set; }
public IReadOnlyList<AudioBookFileInfo> AlternateVersions { get; set; }
}
}

View File

@ -87,7 +87,7 @@ namespace Emby.Naming.AudioBook
foreach (var audioFile in group)
{
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
if (name.Equals("audiobook") ||
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
{

View File

@ -137,7 +137,7 @@ namespace Emby.Naming.Common
CleanStrings = new[]
{
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"(\[.*\])"
};
@ -277,14 +277,14 @@ namespace Emby.Naming.Common
IsNamed = true
},
new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$")
new EpisodeExpression(@"[\\\/\._ \[\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\.[1-9])(?![0-9]))?)([^\\\/]*)$")
{
SupportsAbsoluteEpisodeNumbers = true
},
// Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
// [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
new EpisodeExpression(@".*[\\\/]?.*?(\[.*?\])+.*?(?<seriesname>[-\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
{
IsNamed = true
},
@ -305,6 +305,12 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming
// "Episode 16", "Episode 16 - Title"
new EpisodeExpression(@"[Ee]pisode (?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))?[^\\\/]*$")
{
IsNamed = true
},
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{
IsNamed = true
@ -362,12 +368,6 @@ namespace Emby.Naming.Common
IsOptimistic = true,
IsNamed = true
},
// "Episode 16", "Episode 16 - Title"
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{
IsOptimistic = true,
IsNamed = true
}
};
EpisodeWithoutSeasonExpressions = new[]

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
@ -9,12 +9,11 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Nullable>enable</Nullable>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@ -23,11 +22,12 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\SharedVersion.cs" />
<Compile Include="../SharedVersion.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
</ItemGroup>
<PropertyGroup>
@ -49,8 +49,4 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@ -16,7 +16,7 @@ namespace Emby.Naming.TV
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="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)
{
_options = options;
@ -62,12 +62,16 @@ namespace Emby.Naming.TV
container = extension.TrimStart('.');
}
var flags = new FlagParser(_options).GetFlags(path);
var format3DResult = new Format3DParser(_options).Parse(flags);
var format3DResult = Format3DParser.Parse(path, _options);
var parsingResult = new EpisodePathParser(_options)
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
if (!parsingResult.Success && !isStub)
{
return null;
}
return new EpisodeInfo(path)
{
Container = container,

View File

@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Audio;
using Emby.Naming.Common;
@ -29,72 +28,75 @@ namespace Emby.Naming.Video
/// <param name="path">Path to file.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
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();
if (rule.MediaType == MediaType.Audio)
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
{
if (!AudioFileParser.IsAudioFile(path, _options))
var rule = _options.VideoExtraRules[i];
if (rule.MediaType == MediaType.Audio)
{
if (!AudioFileParser.IsAudioFile(path, _options))
{
continue;
}
}
else if (rule.MediaType == MediaType.Video)
{
if (!VideoResolver.IsVideoFile(path, _options))
{
continue;
}
}
var pathSpan = path.AsSpan();
if (rule.RuleType == ExtraRuleType.Filename)
{
var filename = Path.GetFileNameWithoutExtension(pathSpan);
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
var filename = Path.GetFileNameWithoutExtension(pathSpan);
if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
var filename = Path.GetFileName(path);
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
if (regex.IsMatch(filename))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
if (result.ExtraType != null)
{
return result;
}
}
else if (rule.MediaType == MediaType.Video)
{
if (!new VideoResolver(_options).IsVideoFile(path))
{
return result;
}
}
if (rule.RuleType == ExtraRuleType.Filename)
{
var filename = Path.GetFileNameWithoutExtension(path);
if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
var filename = Path.GetFileNameWithoutExtension(path);
if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
var filename = Path.GetFileName(path);
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
if (regex.IsMatch(filename))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
return result;
}

View File

@ -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);
}
}
}

View File

@ -1,45 +1,37 @@
using System;
using System.Linq;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Parste 3D format related flags.
/// Parse 3D format related flags.
/// </summary>
public class Format3DParser
public static class Format3DParser
{
private readonly NamingOptions _options;
/// <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;
}
// Static default result to save on allocation costs.
private static readonly Format3DResult _defaultResult = new (false, null);
/// <summary>
/// Parse 3D format related flags.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <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;
var delimiters = new char[oldLen + 1];
_options.VideoFlagDelimiters.CopyTo(delimiters, 0);
int oldLen = namingOptions.VideoFlagDelimiters.Length;
Span<char> delimiters = stackalloc char[oldLen + 1];
namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
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)
{
@ -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));
result.Is3D = !string.IsNullOrEmpty(result.Format3D);
if (result.Is3D)
var index = path.IndexOfAny(delimiters);
if (index == -1)
{
result.Tokens.Add(rule.Token);
}
}
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);
}
break;
}
foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
index = path.Length - 1;
}
result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
result.Format3D = format;
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;
}
}
return result;
return is3D ? new Format3DResult(true, format3D) : _defaultResult;
}
}
}

View File

@ -1,5 +1,3 @@
using System.Collections.Generic;
namespace Emby.Naming.Video
{
/// <summary>
@ -10,27 +8,24 @@ namespace Emby.Naming.Video
/// <summary>
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
/// </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>
/// Gets or sets a value indicating whether [is3 d].
/// Gets a value indicating whether [is3 d].
/// </summary>
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
public bool Is3D { get; set; }
public bool Is3D { get; }
/// <summary>
/// Gets or sets the format3 d.
/// Gets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
public string? Format3D { get; set; }
/// <summary>
/// Gets or sets the tokens.
/// </summary>
/// <value>The tokens.</value>
public List<string> Tokens { get; set; }
public string? Format3D { get; }
}
}

View File

@ -85,10 +85,8 @@ namespace Emby.Naming.Video
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
{
var resolver = new VideoResolver(_options);
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)
.ToList();

View File

@ -1,3 +1,4 @@
using System;
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
@ -106,9 +107,9 @@ namespace Emby.Naming.Video
/// Gets the file name without extension.
/// </summary>
/// <value>The file name without extension.</value>
public string FileNameWithoutExtension => !IsDirectory
? System.IO.Path.GetFileNameWithoutExtension(Path)
: System.IO.Path.GetFileName(Path);
public ReadOnlySpan<char> FileNameWithoutExtension => !IsDirectory
? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan())
: System.IO.Path.GetFileName(Path.AsSpan());
/// <inheritdoc />
public override string ToString()

View File

@ -12,31 +12,19 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </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>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <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>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
{
var videoResolver = new VideoResolver(_options);
var videoInfos = files
.Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
.OfType<VideoFileInfo>()
.ToList();
@ -46,7 +34,7 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
var stackResult = new StackResolver(_options)
var stackResult = new StackResolver(namingOptions)
.Resolve(nonExtras).ToList();
var remainingFiles = videoInfos
@ -59,23 +47,17 @@ namespace Emby.Naming.Video
{
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>()
.ToList()
};
info.Year = info.Files[0].Year;
var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
var extras = GetExtras(remainingFiles, extraBaseNames);
var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
if (extras.Count > 0)
{
remainingFiles = remainingFiles
.Except(extras)
.ToList();
info.Extras = extras;
}
@ -88,15 +70,12 @@ namespace Emby.Naming.Video
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;
var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
remainingFiles = remainingFiles
.Except(extras.Concat(new[] { media }))
.ToList();
remainingFiles.Remove(media);
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
info.Extras = extras;
@ -105,8 +84,7 @@ namespace Emby.Naming.Video
if (supportMultiVersion)
{
list = GetVideosGroupedByVersion(list)
.ToList();
list = GetVideosGroupedByVersion(list, namingOptions);
}
// 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 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);
if (!string.IsNullOrEmpty(folderName))
if (!folderName.IsEmpty)
{
var extras = GetExtras(remainingFiles, new List<string> { folderName });
remainingFiles = remainingFiles
.Except(extras)
.ToList();
var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
extras.AddRange(info.Extras);
info.Extras = extras;
}
@ -164,96 +137,168 @@ namespace Emby.Naming.Video
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
Files = new List<VideoFileInfo> { i },
Files = new[] { i },
Year = i.Year
}));
return list;
}
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
{
if (videos.Count == 0)
{
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 (!string.IsNullOrEmpty(folderName)
&& folderName.Length > 1
&& videos.All(i => i.Files.Count == 1
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path))
&& HaveSameYear(videos))
if (folderName.Length <= 1 || !HaveSameYear(videos))
{
var ordered = videos.OrderBy(i => i.Name).ToList();
list.Add(ordered[0]);
var alternateVersionsLen = ordered.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
for (int i = 0; i < alternateVersionsLen; i++)
{
alternateVersions[i] = ordered[i + 1].Files[0];
}
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName;
var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
extras.AddRange(list[0].Extras);
list[0].Extras = extras;
return list;
return videos;
}
return videos;
}
private bool HaveSameYear(List<VideoInfo> videos)
{
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
}
private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
{
string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
// 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++)
{
// Remove the folder name before cleaning as we don't care about cleaning that part
if (folderName.Length <= testFilename.Length)
var video = videos[i];
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
testFilename = testFilename.Substring(folderName.Length).Trim();
return videos;
}
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.Trim().ToString();
}
// The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(testFilename)
|| testFilename[0] == '-'
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
}
return false;
}
// 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));
private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames)
{
foreach (var name in baseNames.ToList())
var list = new List<VideoInfo>
{
var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
baseNames.Add(trimmedName);
videos[0]
};
var alternateVersionsLen = videos.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
var extras = new List<VideoFileInfo>(list[0].Extras);
for (int i = 0; i < alternateVersionsLen; i++)
{
var video = videos[i + 1];
alternateVersions[i] = video.Files[0];
extras.AddRange(video.Extras);
}
return remainingFiles
.Where(i => i.ExtraType != null)
.Where(i => baseNames.Any(b =>
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
.ToList();
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName.ToString();
list[0].Extras = extras;
return list;
}
private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
{
if (videos.Count == 1)
{
return true;
}
var firstYear = videos[0].Year ?? -1;
for (var i = 1; i < videos.Count; i++)
{
if ((videos[i].Year ?? -1) != firstYear)
{
return false;
}
}
return true;
}
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
{
var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Remove the folder name before cleaning as we don't care about cleaning that part
if (folderName.Length <= testFilename.Length)
{
testFilename = testFilename[folderName.Length..].Trim();
}
// There are no span overloads for regex unfortunately
var tmpTestFilename = testFilename.ToString();
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{
tmpTestFilename = cleanName.Trim().ToString();
}
// The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(tmpTestFilename)
|| testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
{
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
}
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
{
if (baseName.IsEmpty)
{
return false;
}
return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|| (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
/// </summary>
/// <param name="remainingFiles">The list of remaining filenames.</param>
/// <param name="baseName">The base name to use for the comparison.</param>
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
/// <returns>A list of video extras for [baseName].</returns>
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
{
return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
}
/// <summary>
/// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
/// </summary>
/// <param name="remainingFiles">The list of remaining filenames.</param>
/// <param name="firstBaseName">The first base name to use for the comparison.</param>
/// <param name="secondBaseName">The second base name to use for the comparison.</param>
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
/// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
{
var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
var result = new List<VideoFileInfo>();
for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
{
var file = remainingFiles[pos];
if (file.ExtraType == null)
{
continue;
}
var filename = file.FileNameWithoutExtension;
if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
|| StartsWith(filename, secondBaseName, trimmedSecondBaseName))
{
result.Add(file);
remainingFiles.RemoveAt(pos);
}
}
return result;
}
}
}

View File

@ -1,46 +1,36 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolves <see cref="VideoFileInfo"/> from file path.
/// </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>
/// Resolves the directory.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
/// <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>
/// Resolves the file.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
/// <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>
@ -48,10 +38,11 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</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>
/// <returns>VideoFileInfo.</returns>
/// <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))
{
@ -59,18 +50,18 @@ namespace Emby.Naming.Video
}
bool isStub = false;
string? container = null;
ReadOnlySpan<char> container = ReadOnlySpan<char>.Empty;
string? stubType = null;
if (!isDirectory)
{
var extension = Path.GetExtension(path);
var extension = Path.GetExtension(path.AsSpan());
// Check supported extensions
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
if (!namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's not supported. Check stub extensions
if (!StubResolver.TryResolveFile(path, _options, out stubType))
if (!StubResolver.TryResolveFile(path, namingOptions, out stubType))
{
return null;
}
@ -81,25 +72,22 @@ namespace Emby.Naming.Video
container = extension.TrimStart('.');
}
var flags = new FlagParser(_options).GetFlags(path);
var format3DResult = new Format3DParser(_options).Parse(flags);
var format3DResult = Format3DParser.Parse(path, namingOptions);
var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
var name = isDirectory
? Path.GetFileName(path)
: Path.GetFileNameWithoutExtension(path);
var name = Path.GetFileNameWithoutExtension(path);
int? year = null;
if (parseName)
{
var cleanDateTimeResult = CleanDateTime(name);
var cleanDateTimeResult = CleanDateTime(name, namingOptions);
name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
&& TryCleanString(name, out ReadOnlySpan<char> newName))
&& TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
{
name = newName.ToString();
}
@ -107,7 +95,7 @@ namespace Emby.Naming.Video
return new VideoFileInfo(
path: path,
container: container,
container: container.IsEmpty ? null : container.ToString(),
isStub: isStub,
name: name,
year: year,
@ -123,43 +111,47 @@ namespace Emby.Naming.Video
/// Determines if path is video file based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <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);
return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
var extension = Path.GetExtension(path.AsSpan());
return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if path is video file stub based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <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);
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
var extension = Path.GetExtension(path.AsSpan());
return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Tries to clean name of clutter.
/// </summary>
/// <param name="name">Raw name.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns>
public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
{
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}
/// <summary>
/// Tries to get name and year from raw name.
/// </summary>
/// <param name="name">Raw name.</param>
/// <param name="namingOptions">The naming options.</param>
/// <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);
}
}
}

View File

@ -9,8 +9,6 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@ -30,8 +28,4 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@ -77,7 +77,6 @@ namespace Emby.Notifications
{
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
return Task.CompletedTask;
@ -132,25 +131,6 @@ namespace Emby.Notifications
return _config.GetConfiguration<NotificationOptions>("notifications");
}
private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
{
if (!_appHost.HasUpdateAvailable)
{
return;
}
var type = NotificationType.ApplicationUpdateAvailable.ToString();
var notification = new NotificationRequest
{
Description = "Please see jellyfin.org for details.",
NotificationType = type,
Name = _localization.GetLocalizedString("NewVersionIsAvailable")
};
await SendNotification(notification, null).ConfigureAwait(false);
}
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
@ -325,7 +305,6 @@ namespace Emby.Notifications
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
_disposed = true;

View File

@ -22,8 +22,6 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- Code Analyzers-->
@ -33,8 +31,4 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
DataPath = Path.Combine(ProgramDataPath, "data");
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
/// <summary>
@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
public string DataPath
{
get => _dataPath;
private set => _dataPath = Directory.CreateDirectory(value).FullName;
}
public string DataPath => _dataPath;
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";

View File

@ -1,3 +1,5 @@
#nullable disable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
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 IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase
/// </summary>
private bool _configurationLoaded;
/// <summary>
/// The _configuration sync lock.
/// </summary>
private readonly object _configurationSyncLock = new object();
/// <summary>
/// The _configuration.
/// </summary>
@ -297,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
/// <inheritdoc />
public object GetConfiguration(string key)
{
return _configurations.GetOrAdd(key, k =>
{
var file = GetConfigurationFile(key);
var configurationInfo = _configurationStores
.FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
if (configurationInfo == null)
return _configurations.GetOrAdd(
key,
(k, configurationManager) =>
{
throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
}
var file = configurationManager.GetConfigurationFile(k);
var configurationType = configurationInfo.ConfigurationType;
var configurationInfo = Array.Find(
configurationManager._configurationStores,
i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
lock (_configurationSyncLock)
{
return LoadConfiguration(file, configurationType);
}
});
if (configurationInfo == null)
{
throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
}
var configurationType = configurationInfo.ConfigurationType;
lock (configurationManager._configurationSyncLock)
{
return configurationManager.LoadConfiguration(file, configurationType);
}
},
this);
}
private object LoadConfiguration(string path, Type configurationType)

View File

@ -1,9 +1,6 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase
@ -36,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
}
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);

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -36,7 +38,6 @@ using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
@ -57,7 +58,6 @@ using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@ -73,7 +73,6 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
@ -101,7 +100,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
@ -116,6 +114,11 @@ namespace Emby.Server.Implementations
/// </summary>
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
/// <summary>
/// The disposable parts.
/// </summary>
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
@ -127,6 +130,62 @@ namespace Emby.Server.Implementations
private ISessionManager _sessionManager;
private string[] _urlPrefixes;
/// <summary>
/// Gets or sets all concrete types.
/// </summary>
/// <value>All concrete types.</value>
private Type[] _allConcreteTypes;
private DeviceId _deviceId;
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection serviceCollection)
{
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
_fileSystemManager = fileSystem;
ServiceCollection = serviceCollection;
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer();
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
}
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
public event EventHandler HasPendingRestartChanged;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
@ -148,13 +207,7 @@ namespace Emby.Server.Implementations
return false;
}
if (OperatingSystem.Id == OperatingSystemId.Windows
|| OperatingSystem.Id == OperatingSystemId.Darwin)
{
return true;
}
return false;
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
}
}
@ -163,11 +216,6 @@ namespace Emby.Server.Implementations
/// </summary>
public INetworkManager NetManager { get; internal set; }
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
public event EventHandler HasPendingRestartChanged;
/// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary>
@ -195,22 +243,11 @@ namespace Emby.Server.Implementations
/// <value>The application paths.</value>
protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary>
/// Gets or sets all concrete types.
/// </summary>
/// <value>All concrete types.</value>
private Type[] _allConcreteTypes;
/// <summary>
/// The disposable parts.
/// </summary>
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
/// <summary>
/// Gets or sets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
protected IConfigurationManager ConfigurationManager { get; set; }
public ServerConfigurationManager ConfigurationManager { get; set; }
/// <summary>
/// Gets or sets the service provider.
@ -232,102 +269,6 @@ namespace Emby.Server.Implementations
/// </summary>
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
/// <summary>
/// Gets the server configuration manager.
/// </summary>
/// <value>The server configuration manager.</value>
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
ServiceCollection = serviceCollection;
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
_fileSystemManager = fileSystem;
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
// Have to migrate settings here as migration subsystem not yet initialised.
MigrateNetworkConfiguration();
// Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_startupOptions = options;
_startupConfig = startupConfig;
// Initialize runtime stat collection
if (ServerConfigurationManager.Configuration.EnableMetrics)
{
DotNetRuntimeStatsBuilder.Default().StartCollecting();
}
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ServerConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
}
/// <summary>
/// Temporary function to migration network settings out of system.xml and into network.xml.
/// TODO: remove at the point when a fixed migration path has been decided upon.
/// </summary>
private void MigrateNetworkConfiguration()
{
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
if (!File.Exists(path))
{
var networkSettings = new NetworkConfiguration();
ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
_xmlSerializer.SerializeToFile(networkSettings, path);
Logger?.LogDebug("Successfully migrated network settings.");
}
}
public string ExpandVirtualPath(string path)
{
var appPaths = ApplicationPaths;
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
.Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
public string ReverseVirtualPath(string path)
{
var appPaths = ApplicationPaths;
return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc />
public Version ApplicationVersion { get; }
@ -352,16 +293,11 @@ namespace Emby.Server.Implementations
/// <value>The application name.</value>
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
private DeviceId _deviceId;
public string SystemId
{
get
{
if (_deviceId == null)
{
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
}
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
@ -370,6 +306,50 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
public string Name => ApplicationProductName;
private CertificateInfo CertificateInfo { get; set; }
public X509Certificate2 Certificate { get; private set; }
/// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
public string FriendlyName =>
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName
: ConfigurationManager.Configuration.ServerName;
/// <summary>
/// Temporary function to migration network settings out of system.xml and into network.xml.
/// TODO: remove at the point when a fixed migration path has been decided upon.
/// </summary>
private void MigrateNetworkConfiguration()
{
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
if (!File.Exists(path))
{
var networkSettings = new NetworkConfiguration();
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
_xmlSerializer.SerializeToFile(networkSettings, path);
Logger.LogDebug("Successfully migrated network settings.");
}
}
public string ExpandVirtualPath(string path)
{
var appPaths = ApplicationPaths;
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
.Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
public string ReverseVirtualPath(string path)
{
var appPaths = ApplicationPaths;
return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Creates an instance of type and resolves all constructor dependencies.
/// </summary>
@ -393,10 +373,7 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type)
{
if (_creatingInstances == null)
{
_creatingInstances = new List<Type>();
}
_creatingInstances ??= new List<Type>();
if (_creatingInstances.IndexOf(type) != -1)
{
@ -490,6 +467,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Runs the startup tasks.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
{
@ -503,7 +481,7 @@ namespace Emby.Server.Implementations
_mediaEncoder.SetFFmpegPath();
Logger.LogInformation("ServerId: {0}", SystemId);
Logger.LogInformation("ServerId: {ServerId}", SystemId);
var entryPoints = GetExports<IServerEntryPoint>();
@ -545,7 +523,21 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
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;
HttpsPort = networkConfiguration.HttpsPortNumber;
@ -563,8 +555,6 @@ namespace Emby.Server.Implementations
};
Certificate = GetCertificate(CertificateInfo);
DiscoverTypes();
RegisterServices();
_pluginManager.RegisterServices(ServiceCollection);
@ -579,7 +569,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddMemoryCache();
ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
@ -606,8 +597,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton(ServerConfigurationManager);
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
@ -617,14 +606,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@ -644,8 +627,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
@ -681,22 +662,21 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
ServiceCollection.AddScoped<ISessionContext, SessionContext>();
ServiceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
ServiceCollection.AddSingleton<TranscodingJobHelper>();
ServiceCollection.AddScoped<MediaInfoHelper>();
ServiceCollection.AddScoped<AudioHelper>();
ServiceCollection.AddScoped<DynamicHlsHelper>();
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
}
/// <summary>
@ -711,8 +691,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
@ -741,7 +719,7 @@ namespace Emby.Server.Implementations
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
logger.LogInformation("Arguments: {Args}", commandLineArgs);
logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name);
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
@ -794,7 +772,7 @@ namespace Emby.Server.Implementations
{
// For now there's no real way to inject these properly
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
BaseItem.ConfigurationManager = ServerConfigurationManager;
BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
@ -816,13 +794,12 @@ namespace Emby.Server.Implementations
/// </summary>
private void FindParts()
{
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
if (!ConfigurationManager.Configuration.IsPortAuthorized)
{
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
ConfigurationManager.Configuration.IsPortAuthorized = true;
ConfigurationManager.SaveConfiguration();
}
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
_pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray();
@ -894,10 +871,6 @@ namespace Emby.Server.Implementations
}
}
private CertificateInfo CertificateInfo { get; set; }
public X509Certificate2 Certificate { get; private set; }
private IEnumerable<string> GetUrlPrefixes()
{
var hosts = new[] { "+" };
@ -926,7 +899,7 @@ namespace Emby.Server.Implementations
protected void OnConfigurationUpdated(object sender, EventArgs e)
{
var requiresRestart = false;
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
// Don't do anything if these haven't been set yet
if (HttpPort != 0 && HttpsPort != 0)
@ -935,10 +908,10 @@ namespace Emby.Server.Implementations
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
networkConfiguration.HttpsPortNumber != HttpsPort)
{
if (ServerConfigurationManager.Configuration.IsPortAuthorized)
if (ConfigurationManager.Configuration.IsPortAuthorized)
{
ServerConfigurationManager.Configuration.IsPortAuthorized = false;
ServerConfigurationManager.SaveConfiguration();
ConfigurationManager.Configuration.IsPortAuthorized = false;
ConfigurationManager.SaveConfiguration();
requiresRestart = true;
}
@ -1119,16 +1092,14 @@ namespace Emby.Server.Implementations
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
OperatingSystem = OperatingSystem.Id.ToString(),
OperatingSystemDisplayName = OperatingSystem.Name,
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
CanSelfRestart = CanSelfRestart,
CanLaunchWebBrowser = CanLaunchWebBrowser,
HasUpdateAvailable = HasUpdateAvailable,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(source),
SupportsLibraryMonitor = true,
EncoderLocation = _mediaEncoder.EncoderLocation,
SystemArchitecture = RuntimeInformation.OSArchitecture,
PackageName = _startupOptions.PackageName
};
@ -1139,25 +1110,22 @@ namespace Emby.Server.Implementations
.Select(i => new WakeOnLanInfo(i))
.ToList();
public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
public PublicSystemInfo GetPublicSystemInfo(IPAddress address)
{
return new PublicSystemInfo
{
Version = ApplicationVersionString,
ProductName = ApplicationProductName,
Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(),
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(source),
LocalAddress = GetSmartApiUrl(address),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
/// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
/// <inheritdoc/>
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
{
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
@ -1166,7 +1134,7 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(ipAddress, out port);
string smart = NetManager.GetBindInterface(remoteAddr, out port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
@ -1229,27 +1197,20 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
public string GetLocalApiUrl(string host, string scheme = null, int? port = null)
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
{
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
// not. For consistency, always trim the trailing slash.
return new UriBuilder
{
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
Host = host,
Host = hostname,
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
}.ToString().TrimEnd('/');
}
public string FriendlyName =>
string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
? Environment.MachineName
: ServerConfigurationManager.Configuration.ServerName;
/// <summary>
/// Shuts down.
/// </summary>
/// <inheritdoc />
public async Task Shutdown()
{
if (IsShuttingDown)
@ -1273,26 +1234,6 @@ namespace Emby.Server.Implementations
protected abstract void ShutdownInternal();
public event EventHandler HasUpdateAvailableChanged;
private bool _hasUpdateAvailable;
public bool HasUpdateAvailable
{
get => _hasUpdateAvailable;
set
{
var fireEvent = value && !_hasUpdateAvailable;
_hasUpdateAvailable = value;
if (fireEvent)
{
HasUpdateAvailableChanged?.Invoke(this, EventArgs.Empty);
}
}
}
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes
@ -1307,41 +1248,7 @@ namespace Emby.Server.Implementations
}
}
public virtual void LaunchUrl(string url)
{
if (!CanLaunchWebBrowser)
{
throw new NotSupportedException();
}
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = url,
UseShellExecute = true,
ErrorDialog = false
},
EnableRaisingEvents = true
};
process.Exited += (sender, args) => ((Process)sender).Dispose();
try
{
process.Start();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error launching url: {url}", url);
throw;
}
}
private bool _disposed = false;
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <inheritdoc />
public void Dispose()
{
Dispose(true);

View File

@ -1,16 +1,17 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@ -101,7 +102,7 @@ namespace Emby.Server.Implementations.Channels
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
return !(channel is IDisableMediaSourceDisplay);
return channel is not IDisableMediaSourceDisplay;
}
/// <inheritdoc />
@ -879,7 +880,7 @@ namespace Emby.Server.Implementations.Channels
}
}
private async Task CacheResponse(object result, string path)
private async Task CacheResponse(ChannelItemResult result, string path)
{
try
{
@ -1078,11 +1079,11 @@ namespace Emby.Server.Implementations.Channels
// was used for status
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
//{
// {
// item.ExternalEtag = info.Etag;
// forceUpdate = true;
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
//}
// }
if (!internalChannelId.Equals(item.ChannelId))
{

View File

@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections
return null;
})
.Where(i => i != null)
.GroupBy(x => x.Id)
.GroupBy(x => x!.Id) // We removed the null values
.Select(x => x.First())
.ToList();
.ToList()!; // Again... the list doesn't contain any null values
}
/// <inheritdoc />

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
@ -8,11 +7,9 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@ -64,13 +61,13 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
public event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
/// <inheritdoc />
public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
public event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
/// <inheritdoc />
public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
public event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
private IEnumerable<Folder> FindFolders(string path)
{
@ -81,14 +78,12 @@ namespace Emby.Server.Implementations.Collections
.Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
}
internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded)
internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
{
var existingFolders = FindFolders(path)
.ToList();
if (existingFolders.Count > 0)
var existingFolder = FindFolders(path).FirstOrDefault();
if (existingFolder != null)
{
return existingFolders[0];
return existingFolder;
}
if (!createIfNeeded)
@ -100,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
var libraryOptions = new LibraryOptions
{
PathInfos = new[] { new MediaPathInfo { Path = path } },
PathInfos = new[] { new MediaPathInfo(path) },
EnableRealtimeMonitor = false,
SaveLocalMetadata = true
};
@ -117,14 +112,14 @@ namespace Emby.Server.Implementations.Collections
return Path.Combine(_appPaths.DataPath, "collections");
}
private Task<Folder> GetCollectionsFolder(bool createIfNeeded)
private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
{
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
}
private IEnumerable<BoxSet> GetCollections(User user)
{
var folder = GetCollectionsFolder(false).Result;
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
return folder == null
? Enumerable.Empty<BoxSet>()
@ -165,9 +160,9 @@ namespace Emby.Server.Implementations.Collections
DateCreated = DateTime.UtcNow
};
parentFolder.AddChild(collection, CancellationToken.None);
parentFolder.AddChild(collection);
if (options.ItemIdList.Length > 0)
if (options.ItemIdList.Count > 0)
{
await AddToCollectionAsync(
collection.Id,
@ -201,13 +196,12 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
=> AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
=> AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
if (collection == null)
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
{
throw new ArgumentException("No collection exists with the supplied Id");
}
@ -251,11 +245,7 @@ namespace Emby.Server.Implementations.Collections
if (fireEvent)
{
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs
{
Collection = collection,
ItemsChanged = itemList
});
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
}
}
}
@ -263,9 +253,7 @@ namespace Emby.Server.Implementations.Collections
/// <inheritdoc />
public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
if (collection == null)
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
{
throw new ArgumentException("No collection exists with the supplied Id");
}
@ -307,11 +295,7 @@ namespace Emby.Server.Implementations.Collections
},
RefreshPriority.High);
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
{
Collection = collection,
ItemsChanged = itemList
});
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
}
/// <inheritdoc />
@ -319,47 +303,57 @@ namespace Emby.Server.Implementations.Collections
{
var results = new Dictionary<Guid, BaseItem>();
var allBoxsets = GetCollections(user).ToList();
var allBoxSets = GetCollections(user).ToList();
foreach (var item in items)
{
if (!(item is ISupportsBoxSetGrouping))
{
results[item.Id] = item;
}
else
if (item is ISupportsBoxSetGrouping)
{
var itemId = item.Id;
var currentBoxSets = allBoxsets
.Where(i => i.ContainsLinkedChildByItemId(itemId))
.ToList();
if (currentBoxSets.Count > 0)
var itemIsInBoxSet = false;
foreach (var boxSet in allBoxSets)
{
foreach (var boxset in currentBoxSets)
if (!boxSet.ContainsLinkedChildByItemId(itemId))
{
results[boxset.Id] = boxset;
continue;
}
itemIsInBoxSet = true;
results.TryAdd(boxSet.Id, boxSet);
}
// skip any item that is in a box set
if (itemIsInBoxSet)
{
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)
{
foreach (var childId in video.GetLocalAlternateVersionIds())
{
if (!results.ContainsKey(childId))
{
continue;
}
alreadyInResults = true;
break;
}
}
else
{
var alreadyInResults = false;
foreach (var child in item.GetMediaSources(true))
{
if (Guid.TryParse(child.Id, out var id) && results.ContainsKey(id))
{
alreadyInResults = true;
break;
}
}
if (!alreadyInResults)
{
results[item.Id] = item;
}
if (alreadyInResults)
{
continue;
}
}
results[item.Id] = item;
}
return results.Values;

View File

@ -1,3 +1,5 @@
#nullable disable
using System;
using System.Globalization;
using System.IO;

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations

View File

@ -1,5 +1,3 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Security.Cryptography;

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -59,7 +61,7 @@ namespace Emby.Server.Implementations.Data
protected virtual int? CacheSize => null;
/// <summary>
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
/// </summary>
/// <value>The journal mode.</value>
protected virtual string JournalMode => "TRUNCATE";
@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data
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(name);
columnNames.Add(columnName);
}
}

View File

@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Data
{
public class ManagedConnection : IDisposable
{
private SQLiteDatabaseConnection _db;
private SQLiteDatabaseConnection? _db;
private readonly SemaphoreSlim _writeLock;
private bool _disposed = false;
@ -54,12 +54,12 @@ namespace Emby.Server.Implementations.Data
return _db.RunInTransaction(action, mode);
}
public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql)
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string 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);
}

View File

@ -1,3 +1,4 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Data
});
}
public static Guid ReadGuidFromBlob(this IResultSetValue result)
public static Guid ReadGuidFromBlob(this ResultSetValue result)
{
return new Guid(result.ToBlob());
}
@ -85,7 +86,7 @@ namespace Emby.Server.Implementations.Data
private static string GetDateTimeKindFormat(DateTimeKind kind)
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
public static DateTime ReadDateTime(this IResultSetValue result)
public static DateTime ReadDateTime(this ResultSetValue result)
{
var dateText = result.ToString();
@ -96,49 +97,139 @@ namespace Emby.Server.Implementations.Data
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))
{
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;
}
result = item.ReadGuidFromBlob();
return true;
}
public static string GetString(this IReadOnlyList<IResultSetValue> result, int index)
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();
}
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();
}
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;
}
result = item.ToBool();
return true;
}
public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index)
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();
}
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;
}
result = item.ToInt64();
return true;
}
public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index)
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();
}
@ -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())
{

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -127,19 +129,17 @@ namespace Emby.Server.Implementations.Data
return list;
}
/// <summary>
/// Saves the user data.
/// </summary>
public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
/// <inheritdoc />
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
{
if (userData == null)
{
throw new ArgumentNullException(nameof(userData));
}
if (internalUserId <= 0)
if (userId <= 0)
{
throw new ArgumentNullException(nameof(internalUserId));
throw new ArgumentNullException(nameof(userId));
}
if (string.IsNullOrEmpty(key))
@ -147,22 +147,23 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(key));
}
PersistUserData(internalUserId, key, userData, cancellationToken);
PersistUserData(userId, key, userData, cancellationToken);
}
public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken)
/// <inheritdoc />
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
{
if (userData == null)
{
throw new ArgumentNullException(nameof(userData));
}
if (internalUserId <= 0)
if (userId <= 0)
{
throw new ArgumentNullException(nameof(internalUserId));
throw new ArgumentNullException(nameof(userId));
}
PersistAllUserData(internalUserId, userData, cancellationToken);
PersistAllUserData(userId, userData, cancellationToken);
}
/// <summary>
@ -172,7 +173,6 @@ namespace Emby.Server.Implementations.Data
/// <param name="key">The key.</param>
/// <param name="userData">The user data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@ -262,19 +262,19 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets the user data.
/// </summary>
/// <param name="internalUserId">The user id.</param>
/// <param name="userId">The user id.</param>
/// <param name="key">The key.</param>
/// <returns>Task{UserItemData}.</returns>
/// <exception cref="ArgumentNullException">
/// userId
/// or
/// key
/// key.
/// </exception>
public UserItemData GetUserData(long internalUserId, string key)
public UserItemData GetUserData(long userId, string key)
{
if (internalUserId <= 0)
if (userId <= 0)
{
throw new ArgumentNullException(nameof(internalUserId));
throw new ArgumentNullException(nameof(userId));
}
if (string.IsNullOrEmpty(key))
@ -286,7 +286,7 @@ namespace Emby.Server.Implementations.Data
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
statement.TryBind("@UserId", internalUserId);
statement.TryBind("@UserId", userId);
statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery())
@ -299,7 +299,7 @@ namespace Emby.Server.Implementations.Data
}
}
public UserItemData GetUserData(long internalUserId, List<string> keys)
public UserItemData GetUserData(long userId, List<string> keys)
{
if (keys == null)
{
@ -311,19 +311,19 @@ namespace Emby.Server.Implementations.Data
return null;
}
return GetUserData(internalUserId, keys[0]);
return GetUserData(userId, keys[0]);
}
/// <summary>
/// Return all user-data associated with the given user.
/// </summary>
/// <param name="internalUserId"></param>
/// <returns></returns>
public List<UserItemData> GetAllUserData(long internalUserId)
/// <param name="userId">The internal user id.</param>
/// <returns>The list of user item data.</returns>
public List<UserItemData> GetAllUserData(long userId)
{
if (internalUserId <= 0)
if (userId <= 0)
{
throw new ArgumentNullException(nameof(internalUserId));
throw new ArgumentNullException(nameof(userId));
}
var list = new List<UserItemData>();
@ -332,7 +332,7 @@ namespace Emby.Server.Implementations.Data
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
{
statement.TryBind("@UserId", internalUserId);
statement.TryBind("@UserId", userId);
foreach (var row in statement.ExecuteQuery())
{
@ -347,17 +347,18 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Read a row from the specified reader into the provided userData object.
/// </summary>
/// <param name="reader"></param>
private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
/// <param name="reader">The list of result set values.</param>
/// <returns>The user item data.</returns>
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
{
var userData = new UserItemData();
userData.Key = reader[0].ToString();
// 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();
@ -365,19 +366,19 @@ namespace Emby.Server.Implementations.Data
userData.IsFavorite = reader[5].ToBool();
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;

View File

@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Data
/// This holds all the types in the running assemblies
/// so that we can de-serialize properly when we don't have strong types.
/// </summary>
private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>();
private readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
/// <summary>
/// Gets the type.
@ -21,26 +21,16 @@ namespace Emby.Server.Implementations.Data
/// <param name="typeName">Name of the type.</param>
/// <returns>Type.</returns>
/// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
public Type GetType(string typeName)
public Type? GetType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
{
throw new ArgumentNullException(nameof(typeName));
}
return _typeMap.GetOrAdd(typeName, LookupType);
}
/// <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);
return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType(k))
.FirstOrDefault(t => t != null));
}
}
}

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@ -1,144 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
namespace Emby.Server.Implementations.Devices
{
public class DeviceManager : IDeviceManager
{
private readonly IUserManager _userManager;
private readonly IAuthenticationRepository _authRepo;
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
{
_userManager = userManager;
_authRepo = authRepo;
}
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
{
_capabilitiesMap[deviceId] = capabilities;
}
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
{
_authRepo.UpdateDeviceOptions(deviceId, options);
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
}
public DeviceOptions GetDeviceOptions(string deviceId)
{
return _authRepo.GetDeviceOptions(deviceId);
}
public ClientCapabilities GetCapabilities(string id)
{
return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
? result
: new ClientCapabilities();
}
public DeviceInfo GetDevice(string id)
{
var session = _authRepo.Get(new AuthenticationInfoQuery
{
DeviceId = id
}).Items.FirstOrDefault();
var device = session == null ? null : ToDeviceInfo(session);
return device;
}
public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
{
IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
{
// UserId = query.UserId
HasUser = true
}).Items;
// TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
if (query.SupportsSync.HasValue)
{
var val = query.SupportsSync.Value;
sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val);
}
if (!query.UserId.Equals(Guid.Empty))
{
var user = _userManager.GetUserById(query.UserId);
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
}
var array = sessions.Select(ToDeviceInfo).ToArray();
return new QueryResult<DeviceInfo>(array);
}
private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
{
var caps = GetCapabilities(authInfo.DeviceId);
return new DeviceInfo
{
AppName = authInfo.AppName,
AppVersion = authInfo.AppVersion,
Id = authInfo.DeviceId,
LastUserId = authInfo.UserId,
LastUserName = authInfo.UserName,
Name = authInfo.DeviceName,
DateLastActivity = authInfo.DateLastActivity,
IconUrl = caps?.IconUrl
};
}
public bool CanAccessDevice(User user, string deviceId)
{
if (user == null)
{
throw new ArgumentException("user not found");
}
if (string.IsNullOrEmpty(deviceId))
{
throw new ArgumentNullException(nameof(deviceId));
}
if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
{
return true;
}
if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
{
var capabilities = GetCapabilities(deviceId);
if (capabilities != null && capabilities.SupportsPersistentIdentifier)
{
return false;
}
}
return true;
}
}
}

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -49,8 +51,6 @@ namespace Emby.Server.Implementations.Dto
private readonly IMediaSourceManager _mediaSourceManager;
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
public DtoService(
ILogger<DtoService> logger,
ILibraryManager libraryManager,
@ -73,6 +73,8 @@ namespace Emby.Server.Implementations.Dto
_livetvManagerFactory = livetvManagerFactory;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
@ -505,7 +507,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <returns>Task.</returns>
private void AttachPeople(BaseItemDto dto, BaseItem item)
{
// Ordering by person type to ensure actors and artists are at the front.
@ -614,7 +615,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <returns>Task.</returns>
private void AttachStudios(BaseItemDto dto, BaseItem item)
{
dto.Studios = item.Studios
@ -665,10 +665,7 @@ namespace Emby.Server.Implementations.Dto
var tag = GetImageCacheTag(item, image);
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))
{
@ -702,10 +699,7 @@ namespace Emby.Server.Implementations.Dto
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;
}
@ -811,7 +805,7 @@ namespace Emby.Server.Implementations.Dto
dto.MediaType = item.MediaType;
if (!(item is LiveTvProgram))
if (item is not LiveTvProgram)
{
dto.LocationType = item.LocationType;
}
@ -898,10 +892,7 @@ namespace Emby.Server.Implementations.Dto
dto.Taglines = new string[] { item.Tagline };
}
if (dto.Taglines == null)
{
dto.Taglines = Array.Empty<string>();
}
dto.Taglines ??= Array.Empty<string>();
}
dto.Type = item.GetBaseItemKind();
@ -935,9 +926,9 @@ namespace Emby.Server.Implementations.Dto
}
// if (options.ContainsField(ItemFields.MediaSourceCount))
//{
// {
// Songs always have one
//}
// }
}
if (item is IHasArtist hasArtist)
@ -945,10 +936,10 @@ namespace Emby.Server.Implementations.Dto
dto.Artists = hasArtist.Artists;
// var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
//{
// {
// EnableTotalRecordCount = false,
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
//});
// });
// dto.ArtistItems = artistItems.Items
// .Select(i =>
@ -965,7 +956,7 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
dto.ArtistItems = hasArtist.Artists
//.Except(foundArtists, new DistinctNameComparer())
// .Except(foundArtists, new DistinctNameComparer())
.Select(i =>
{
// This should not be necessary but we're seeing some cases of it
@ -997,10 +988,10 @@ namespace Emby.Server.Implementations.Dto
dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
// var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
//{
// {
// EnableTotalRecordCount = false,
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
//});
// });
// dto.AlbumArtists = artistItems.Items
// .Select(i =>
@ -1015,7 +1006,7 @@ namespace Emby.Server.Implementations.Dto
// .ToList();
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
//.Except(foundArtists, new DistinctNameComparer())
// .Except(foundArtists, new DistinctNameComparer())
.Select(i =>
{
// This should not be necessary but we're seeing some cases of it
@ -1042,8 +1033,7 @@ namespace Emby.Server.Implementations.Dto
}
// Add video info
var video = item as Video;
if (video != null)
if (item is Video video)
{
dto.VideoType = video.VideoType;
dto.Video3DFormat = video.Video3DFormat;
@ -1082,9 +1072,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.MediaStreams))
{
// Add VideoInfo
var iHasMediaSources = item as IHasMediaSources;
if (iHasMediaSources != null)
if (item is IHasMediaSources)
{
MediaStream[] mediaStreams;
@ -1153,7 +1141,7 @@ namespace Emby.Server.Implementations.Dto
// TODO maybe remove the if statement entirely
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
{
episodeSeries = episodeSeries ?? episode.Series;
episodeSeries ??= episode.Series;
if (episodeSeries != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
@ -1166,7 +1154,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.SeriesStudio))
{
episodeSeries = episodeSeries ?? episode.Series;
episodeSeries ??= episode.Series;
if (episodeSeries != null)
{
dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
@ -1179,7 +1167,7 @@ namespace Emby.Server.Implementations.Dto
{
dto.AirDays = series.AirDays;
dto.AirTime = series.AirTime;
dto.Status = series.Status.HasValue ? series.Status.Value.ToString() : null;
dto.Status = series.Status?.ToString();
}
// Add SeasonInfo
@ -1192,7 +1180,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.SeriesStudio))
{
series = series ?? season.Series;
series ??= season.Series;
if (series != null)
{
dto.SeriesStudio = series.Studios.FirstOrDefault();
@ -1203,7 +1191,7 @@ namespace Emby.Server.Implementations.Dto
// TODO maybe remove the if statement entirely
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
{
series = series ?? season.Series;
series ??= season.Series;
if (series != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
@ -1290,7 +1278,7 @@ namespace Emby.Server.Implementations.Dto
var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
if (parent == null && !(originalItem is UserRootFolder) && !(originalItem is UserView) && !(originalItem is AggregateFolder) && !(originalItem is ICollectionFolder) && !(originalItem is Channel))
if (parent == null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel)
{
parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
}
@ -1323,9 +1311,12 @@ namespace Emby.Server.Implementations.Dto
var imageTags = dto.ImageTags;
while (((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) &&
(parent = parent ?? (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null)
while ((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
|| parent is Series)
{
parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
if (parent == null)
{
break;
@ -1355,7 +1346,7 @@ namespace Emby.Server.Implementations.Dto
}
}
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
{
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
@ -1405,7 +1396,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <returns>Task.</returns>
public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
{
dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);

View File

@ -9,6 +9,7 @@
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.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.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
@ -22,16 +23,17 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscUtils.Udf" Version="0.16.4" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
<PackageReference Include="sharpcompress" Version="0.28.1" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.0" />
<PackageReference Include="sharpcompress" Version="0.28.3" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup>
@ -43,9 +45,13 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
@ -55,10 +61,6 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Localization\iso6392.txt" />
<EmbeddedResource Include="Localization\countries.json" />

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -106,8 +108,6 @@ namespace Emby.Server.Implementations.EntryPoints
NatUtility.StartDiscovery();
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
}
private void Stop()
@ -118,13 +118,6 @@ namespace Emby.Server.Implementations.EntryPoints
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
_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)

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -147,7 +149,7 @@ namespace Emby.Server.Implementations.EntryPoints
private static bool EnableRefreshMessage(BaseItem item)
{
if (!(item is Folder folder))
if (item is not Folder folder)
{
return false;
}
@ -401,7 +403,7 @@ namespace Emby.Server.Implementations.EntryPoints
return false;
}
if (item is IItemByName && !(item is MusicArtist))
if (item is IItemByName && item is not MusicArtist)
{
return false;
}

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@ -1,5 +1,3 @@
#nullable enable
using System;
using System.Net.Sockets;
using System.Threading;
@ -39,6 +37,9 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
public UdpServerEntryPoint(
ILogger<UdpServerEntryPoint> logger,
IServerApplicationHost appHost,
@ -56,8 +57,8 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
_udpServer = new UdpServer(_logger, _appHost, _config);
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
_udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
_udpServer.Start(_cancellationTokenSource.Token);
}
catch (SocketException ex)
{

View File

@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
private readonly object _syncLock = new object();
private Timer _updateTimer;
private Timer? _updateTimer;
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
{
@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
{
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
{
@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
_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>();
_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)
{

View File

@ -1,6 +1,6 @@
#pragma warning disable CS1591
using System;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
@ -18,9 +18,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_authorizationContext = authorizationContext;
}
public AuthorizationInfo Authenticate(HttpRequest request)
public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
if (!auth.HasToken)
{

Some files were not shown because too many files have changed in this diff Show More