diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml
deleted file mode 100644
index 0e944e6f4..000000000
--- a/.ci/azure-pipelines-api-client.yml
+++ /dev/null
@@ -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
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 20f4dfe33..543fd7fc6 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -160,7 +160,6 @@ jobs:
dependsOn:
- BuildPackage
- BuildDocker
- condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
@@ -186,9 +185,6 @@ jobs:
- job: PublishNuget
displayName: 'Publish NuGet packages'
- dependsOn:
- - BuildPackage
- condition: succeeded('BuildPackage')
pool:
vmImage: 'ubuntu-latest'
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index 95e0d8c58..7838b3b02 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -94,5 +94,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
- targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json"
+ targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
artifactName: 'OpenAPI Spec'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 6430503f9..c028b6e3e 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -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
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index d67e1c98b..12f1f5ed5 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -33,7 +33,13 @@ assignees: ''
**Expected behavior**
-**Logs**
+**Server Logs**
+
+
+**FFmpeg Logs**
+
+
+**Browser Console Logs**
**Screenshots**
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0874cae2e..70bcd4973 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -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
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
new file mode 100644
index 000000000..8da2349c8
--- /dev/null
+++ b/.github/workflows/automation.yml
@@ -0,0 +1,74 @@
+name: Automation
+
+on:
+ push:
+ branches:
+ - master
+ pull_request_target:
+ issue_comment:
+
+jobs:
+ label:
+ name: Labeling
+ runs-on: ubuntu-latest
+ steps:
+ - name: Apply label
+ uses: eps1lon/actions-label-merge-conflict@v2.0.1
+ if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
+ with:
+ dirtyLabel: 'merge conflict'
+ repoToken: ${{ secrets.JF_BOT_TOKEN }}
+
+ project:
+ name: Project board
+ runs-on: ubuntu-latest
+ steps:
+ - name: Remove from 'Current Release' project
+ uses: alex-page/github-project-automation-plus@v0.7.1
+ if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
+ continue-on-error: true
+ with:
+ project: Current Release
+ action: delete
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Add to 'Release Next' project
+ uses: alex-page/github-project-automation-plus@v0.7.1
+ if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
+ continue-on-error: true
+ with:
+ project: Release Next
+ column: In progress
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Add to 'Current Release' project
+ uses: alex-page/github-project-automation-plus@v0.7.1
+ if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
+ continue-on-error: true
+ with:
+ project: Current Release
+ column: In progress
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Check number of comments from the team member
+ if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
+ id: member_comments
+ run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
+
+ - name: Move issue to needs triage
+ uses: alex-page/github-project-automation-plus@v0.7.1
+ if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
+ continue-on-error: true
+ with:
+ project: Issue Triage for Main Repo
+ column: Needs triage
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Add issue to triage project
+ uses: alex-page/github-project-automation-plus@v0.7.1
+ if: github.event.issue.pull_request == '' && github.event.action == 'opened'
+ continue-on-error: true
+ with:
+ project: Issue Triage for Main Repo
+ column: Pending response
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
new file mode 100644
index 000000000..e0b91ecee
--- /dev/null
+++ b/.github/workflows/commands.yml
@@ -0,0 +1,119 @@
+name: Commands
+on:
+ issue_comment:
+ types:
+ - created
+ - edited
+ pull_request_target:
+ types:
+ - labeled
+ - synchronize
+
+jobs:
+ rebase:
+ name: Rebase
+ if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify as seen
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ github.event.comment.id }}
+ reactions: '+1'
+
+ - name: Checkout the latest code
+ uses: actions/checkout@v2
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ fetch-depth: 0
+
+ - name: Automatic Rebase
+ uses: cirrus-actions/rebase@1.4
+ env:
+ GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
+
+ check-backport:
+ name: Check Backport
+ if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify as seen
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ github.event.comment.id }}
+ reactions: eyes
+
+ - name: Checkout the latest code
+ uses: actions/checkout@v2
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ fetch-depth: 0
+
+ - name: Notify as running
+ id: comment_running
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ issue-number: ${{ github.event.issue.number }}
+ body: |
+ Running backport tests...
+
+ - name: Perform test backport
+ id: run_tests
+ run: |
+ set +o errexit
+ git config --global user.name "Jellyfin Bot"
+ git config --global user.email "team@jellyfin.org"
+ CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
+ git checkout master
+ git merge --no-ff ${CURRENT_BRANCH}
+ MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
+ git fetch --all
+ CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
+ stable_branch="Current stable release branch: ${CURRENT_STABLE}"
+ echo ${stable_branch}
+ echo ::set-output name=branch::${stable_branch}
+ git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
+ git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
+ retcode=$?
+ cat output.txt | grep -v 'hint:'
+ output="$( grep -v 'hint:' output.txt )"
+ output="${output//'%'/'%25'}"
+ output="${output//$'\n'/'%0A'}"
+ output="${output//$'\r'/'%0D'}"
+ echo ::set-output name=output::$output
+ exit ${retcode}
+
+ - name: Notify with result success
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null && success() }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ steps.comment_running.outputs.comment-id }}
+ body: |
+ ${{ steps.run_tests.outputs.branch }}
+ Output from `git cherry-pick`:
+
+ ---
+
+ ${{ steps.run_tests.outputs.output }}
+ reactions: hooray
+
+ - name: Notify with result failure
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null && failure() }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ steps.comment_running.outputs.comment-id }}
+ body: |
+ ${{ steps.run_tests.outputs.branch }}
+ Output from `git cherry-pick`:
+
+ ---
+
+ ${{ steps.run_tests.outputs.output }}
+ reactions: confused
diff --git a/.gitignore b/.gitignore
index 7cd3d0068..252210e57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -268,6 +268,7 @@ doc/
# Deployment artifacts
dist
*.exe
+*.dll
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 89788b234..59addc7fa 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -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)
@@ -49,6 +50,7 @@
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
+ - [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
@@ -68,6 +70,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)
@@ -104,10 +107,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)
@@ -143,6 +147,7 @@
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
+ - [peterspenler](https://github.com/peterspenler)
# Emby Contributors
diff --git a/Dockerfile b/Dockerfile
index 41dd3d081..4e2d06b82 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,14 +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
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
diff --git a/Dockerfile.arm b/Dockerfile.arm
index e0eaca0ed..25a0de7db 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -5,12 +5,12 @@
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
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index db7de935c..c9f19c5a3 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -5,12 +5,12 @@
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
diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs
index e63a85860..5ceeb5530 100644
--- a/Emby.Dlna/Configuration/DlnaOptions.cs
+++ b/Emby.Dlna/Configuration/DlnaOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace Emby.Dlna.Configuration
diff --git a/Emby.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs
index fc02e1751..3ca43052a 100644
--- a/Emby.Dlna/ConfigurationExtension.cs
+++ b/Emby.Dlna/ConfigurationExtension.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using Emby.Dlna.Configuration;
diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs
index 2f8d197a7..1a1790ee6 100644
--- a/Emby.Dlna/ConnectionManager/ControlHandler.cs
+++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs
@@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
}
///
- protected override void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter)
+ protected override void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter)
{
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
{
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
index 2f3107450..7b8c50440 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 27f1fdaba..27c5b2268 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -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
}
///
- protected override void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter)
+ protected override void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter)
{
if (xmlWriter == null)
{
@@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory
///
/// Adds a "XSetBookmark" element to the xml document.
///
- /// The .
- private void HandleXSetBookmark(IDictionary sparams)
+ /// The method parameters.
+ private void HandleXSetBookmark(IReadOnlyDictionary sparams)
{
var id = sparams["ObjectID"];
@@ -305,35 +305,18 @@ namespace Emby.Dlna.ContentDirectory
return builder.ToString();
}
- ///
- /// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist.
- ///
- /// The .
- /// The key.
- /// The defaultValue.
- /// The .
- public static string GetValueOrDefault(IDictionary sparams, string key, string defaultValue)
- {
- if (sparams != null && sparams.TryGetValue(key, out string val))
- {
- return val;
- }
-
- return defaultValue;
- }
-
///
/// Builds the "Browse" xml response.
///
/// The .
- /// The .
+ /// The method parameters.
/// The device Id to use.
- private void HandleBrowse(XmlWriter xmlWriter, IDictionary sparams, string deviceId)
+ private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary 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 +418,9 @@ namespace Emby.Dlna.ContentDirectory
/// Builds the response to the "X_BrowseByLetter request.
///
/// The .
- /// The .
+ /// The method parameters.
/// The device id.
- private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary sparams, string deviceId)
+ private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary sparams, string deviceId)
{
// TODO: Implement this method
HandleSearch(xmlWriter, sparams, deviceId);
@@ -447,13 +430,13 @@ namespace Emby.Dlna.ContentDirectory
/// Builds a response to the "Search" request.
///
/// The xmlWriter.
- /// The sparams.
+ /// The method parameters.
/// The deviceId.
- private void HandleSearch(XmlWriter xmlWriter, IDictionary sparams, string deviceId)
+ private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary 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
diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs
index 982ae5d68..187dc1d75 100644
--- a/Emby.Dlna/ContentDirectory/StubType.cs
+++ b/Emby.Dlna/ContentDirectory/StubType.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1602
namespace Emby.Dlna.ContentDirectory
{
diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs
index 4ea4e4e48..8ee6325e9 100644
--- a/Emby.Dlna/ControlRequest.cs
+++ b/Emby.Dlna/ControlRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs
index d827eef26..a7f2d4a73 100644
--- a/Emby.Dlna/ControlResponse.cs
+++ b/Emby.Dlna/ControlResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 8b50d47fb..2982ce97e 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -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,
@@ -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;
diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs
index 2b86ea333..b66f53ece 100644
--- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs
+++ b/Emby.Dlna/Didl/StringWriterWithEncoding.cs
@@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
{
public class StringWriterWithEncoding : StringWriter
{
- private readonly Encoding _encoding;
+ private readonly Encoding? _encoding;
public StringWriterWithEncoding()
{
diff --git a/Emby.Dlna/DlnaConfigurationFactory.cs b/Emby.Dlna/DlnaConfigurationFactory.cs
index 4c6ca869a..6cc6b73a0 100644
--- a/Emby.Dlna/DlnaConfigurationFactory.cs
+++ b/Emby.Dlna/DlnaConfigurationFactory.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 21ba1c755..a1b106704 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -36,7 +38,7 @@ namespace Emby.Dlna
private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly Dictionary> _profiles = new Dictionary>(StringComparer.Ordinal);
@@ -111,7 +113,7 @@ namespace Emby.Dlna
if (profile != null)
{
- _logger.LogDebug("Found matching device profile: {0}", profile.Name);
+ _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
}
else
{
@@ -126,92 +128,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)
+ ///
+ /// 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.
+ ///
+ /// The of the device.
+ /// The of the profile.
+ /// True if they match.
+ 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)
{
@@ -333,7 +300,12 @@ namespace Emby.Dlna
throw new ArgumentNullException(nameof(id));
}
- var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
+ var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
+
+ if (info == null)
+ {
+ return null;
+ }
return ParseProfileFile(info.Path, info.Info.Type);
}
@@ -395,7 +367,8 @@ namespace Emby.Dlna
{
Directory.CreateDirectory(systemProfilesPath);
- using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
@@ -553,7 +526,7 @@ namespace Emby.Dlna
private void DumpProfiles()
{
- DeviceProfile[] list = new []
+ DeviceProfile[] list = new[]
{
new SamsungSmartTvProfile(),
new XboxOneProfile(),
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 8b057a095..a40578e40 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -21,11 +21,11 @@
false
true
true
+ enable
-
diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs
index 1b1bd426c..8c82dcbf6 100644
--- a/Emby.Dlna/EventSubscriptionResponse.cs
+++ b/Emby.Dlna/EventSubscriptionResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index ff81e83b5..2e672b886 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/Eventing/EventSubscription.cs b/Emby.Dlna/Eventing/EventSubscription.cs
index 40d73ee0e..4fd7f8169 100644
--- a/Emby.Dlna/Eventing/EventSubscription.cs
+++ b/Emby.Dlna/Eventing/EventSubscription.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 82490ec31..0309926ab 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -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;
@@ -128,7 +129,8 @@ namespace Emby.Dlna.Main
_netConfig = config.GetConfiguration("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
- if (_disabled)
+
+ if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
@@ -228,7 +230,10 @@ namespace Emby.Dlna.Main
{
try
{
- ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+ if (communicationsServer != null)
+ {
+ ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+ }
}
catch (Exception ex)
{
@@ -313,9 +318,12 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
- // DLNA will only work over http, so we must reset to http:// : {port}
- uri.Scheme = "http://";
- uri.Port = _netConfig.HttpServerPortNumber;
+ if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
+ {
+ // DLNA will only work over http, so we must reset to http:// : {port}.
+ uri.Scheme = "http";
+ uri.Port = _netConfig.HttpServerPortNumber;
+ }
var device = new SsdpRootDevice
{
diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
index 464f71a6f..d8fb12742 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
@@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
}
///
- protected override void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter)
+ protected override void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter)
{
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
{
diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
index 37840cd09..f3789a791 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using Emby.Dlna.Common;
using Emby.Dlna.Service;
-using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 938ce5fbf..6c580d15b 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -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;
@@ -235,7 +237,13 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("Setting mute");
var value = mute ? 1 : 0;
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ rendererCommands.BuildPost(command, service.ServiceType, value),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
IsMuted = mute;
@@ -253,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;
@@ -270,7 +278,13 @@ namespace Emby.Dlna.PlayTo
// Remote control will perform better
Volume = value;
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ rendererCommands.BuildPost(command, service.ServiceType, value),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
@@ -278,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;
@@ -291,7 +305,13 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service");
}
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
RestartTimer(true);
@@ -305,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;
@@ -325,14 +345,21 @@ namespace Emby.Dlna.PlayTo
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ post,
+ header: header,
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
- await Task.Delay(50).ConfigureAwait(false);
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
try
{
- await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
+ await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
}
catch
{
@@ -343,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("&", "&", 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
+ {
+ { "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))
@@ -378,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);
@@ -388,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;
@@ -396,7 +463,13 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ avCommands.BuildPost(command, service.ServiceType, 1),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
RestartTimer(true);
@@ -406,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;
@@ -414,7 +487,13 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ avCommands.BuildPost(command, service.ServiceType, 1),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
TransportState = TransportState.Paused;
@@ -528,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;
@@ -578,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;
@@ -665,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,
@@ -733,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,
@@ -914,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;
@@ -942,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;
@@ -973,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();
@@ -990,7 +1090,7 @@ namespace Emby.Dlna.PlayTo
var deviceProperties = new DeviceInfo()
{
- Name = string.Join(" ", friendlyNames),
+ Name = string.Join(' ', friendlyNames),
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
};
diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs
index d3daab9e0..2acfff4eb 100644
--- a/Emby.Dlna/PlayTo/DeviceInfo.cs
+++ b/Emby.Dlna/PlayTo/DeviceInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
index dabd079af..2bc4d8cc2 100644
--- a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
+++ b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 315be1e8b..0e49fd2c0 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -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
@@ -132,7 +150,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{
- if (_disposed)
+ if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
return;
}
@@ -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);
@@ -777,7 +820,7 @@ namespace Emby.Dlna.PlayTo
var currentWait = 0;
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
{
- await Task.Delay(Interval).ConfigureAwait(false);
+ await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
currentWait += Interval;
}
@@ -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");
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index a6793a708..35bf5927c 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -178,12 +180,17 @@ namespace Emby.Dlna.PlayTo
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,
diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
index d14617c8a..c7d2b28df 100644
--- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
index 3f8d55263..f8a14f411 100644
--- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
index deeb47918..6661f92ac 100644
--- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs
index 85846166c..5056e69ae 100644
--- a/Emby.Dlna/PlayTo/PlaylistItem.cs
+++ b/Emby.Dlna/PlayTo/PlaylistItem.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.Dlna;
diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
index e28840a89..657491303 100644
--- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
+++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index 557bc69a7..f14f73bb6 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -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;
@@ -45,10 +46,10 @@ namespace Emby.Dlna.PlayTo
cancellationToken)
.ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var reader = new StreamReader(stream, Encoding.UTF8);
- return XDocument.Parse(
- await reader.ReadToEndAsync().ConfigureAwait(false),
- LoadOptions.PreserveWhitespace);
+ return await XDocument.LoadAsync(
+ stream,
+ LoadOptions.PreserveWhitespace,
+ cancellationToken).ConfigureAwait(false);
}
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@@ -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);
- using var reader = new StreamReader(stream, Encoding.UTF8);
- return XDocument.Parse(
- await reader.ReadToEndAsync().ConfigureAwait(false),
- LoadOptions.PreserveWhitespace);
+ try
+ {
+ return await XDocument.LoadAsync(
+ stream,
+ LoadOptions.PreserveWhitespace,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ return null;
+ }
}
private async Task PostSoapDataAsync(
diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs
index 0865968ad..b58669355 100644
--- a/Emby.Dlna/PlayTo/TransportCommands.cs
+++ b/Emby.Dlna/PlayTo/TransportCommands.cs
@@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
public class TransportCommands
{
private const string CommandBase = "\r\n" + "" + "" + "" + "{2}" + "" + "";
- private List _stateVariables = new List();
- private List _serviceActions = new List();
- public List StateVariables => _stateVariables;
+ public List StateVariables { get; } = new List();
- public List ServiceActions => _serviceActions;
+ public List ServiceActions { get; } = new List();
public static TransportCommands Create(XDocument document)
{
@@ -48,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;
@@ -70,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
};
}
@@ -91,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
};
}
@@ -168,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));
diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs
index 7068a5d24..2058e9dc7 100644
--- a/Emby.Dlna/PlayTo/TransportState.cs
+++ b/Emby.Dlna/PlayTo/TransportState.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1602
namespace Emby.Dlna.PlayTo
{
diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs
index 0d9478e42..02d2da58d 100644
--- a/Emby.Dlna/PlayTo/uBaseObject.cs
+++ b/Emby.Dlna/PlayTo/uBaseObject.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs
index d4af72b62..8eaf12ba9 100644
--- a/Emby.Dlna/Profiles/DefaultProfile.cs
+++ b/Emby.Dlna/Profiles/DefaultProfile.cs
@@ -1,5 +1,7 @@
#pragma warning disable CS1591
+using System;
+using System.Globalization;
using System.Linq;
using MediaBrowser.Model.Dlna;
@@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
{
public DefaultProfile()
{
+ Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
Name = "Generic Device";
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 09525aae4..3f3dfccd3 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -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 GetIcons()
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 8d2486fee..904c23d99 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -47,7 +47,7 @@ namespace Emby.Dlna.Service
private async Task ProcessControlRequestInternalAsync(ControlRequest request)
{
- ControlRequestInfo requestInfo = null;
+ ControlRequestInfo? requestInfo = null;
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
{
@@ -151,7 +151,7 @@ namespace Emby.Dlna.Service
private async Task 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 +210,7 @@ namespace Emby.Dlna.Service
}
}
- protected abstract void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter);
+ protected abstract void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter);
private void LogRequest(ControlRequest request)
{
diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
index 8c7d961f3..391dda147 100644
--- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs
+++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -69,7 +71,7 @@ namespace Emby.Dlna.Ssdp
{
lock (_syncLock)
{
- if (_listenerCount > 0 && _deviceLocator == null)
+ if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
{
_deviceLocator = new SsdpDeviceLocator(_commsServer);
@@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp
{
Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers,
- LocalIpAddress = e.LocalIpAddress
+ RemoteIpAddress = e.RemoteIpAddress
});
DeviceDiscoveredInternal?.Invoke(this, args);
diff --git a/Emby.Dlna/Ssdp/SsdpExtensions.cs b/Emby.Dlna/Ssdp/SsdpExtensions.cs
index e7a52f168..d00eb02b4 100644
--- a/Emby.Dlna/Ssdp/SsdpExtensions.cs
+++ b/Emby.Dlna/Ssdp/SsdpExtensions.cs
@@ -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;
}
}
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj
index 7d479a5c6..5c5afe1c6 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -25,7 +25,6 @@
-
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 8a2301d2d..7d952aa23 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -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
///
/// Gets the cache file path based on a set of parameters.
///
- 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());
}
///
@@ -352,8 +426,13 @@ namespace Emby.Drawing
}
///
- public string GetImageCacheTag(User user)
+ public string? GetImageCacheTag(User user)
{
+ if (user.ProfileImage == null)
+ {
+ return null;
+ }
+
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
.ToString("N", CultureInfo.InvariantCulture);
}
diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs
index 2a1cfd3da..1c05aa916 100644
--- a/Emby.Drawing/NullImageEncoder.cs
+++ b/Emby.Drawing/NullImageEncoder.cs
@@ -32,7 +32,7 @@ namespace Emby.Drawing
=> throw new NotImplementedException();
///
- 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();
}
diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs
index 8b47dd12e..af4aa0059 100644
--- a/Emby.Naming/Audio/AudioFileParser.cs
+++ b/Emby.Naming/Audio/AudioFileParser.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using MediaBrowser.Common.Extensions;
namespace Emby.Naming.Audio
{
@@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
/// True if file at path is audio file.
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);
}
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index e9ea9b7a5..ca5322890 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
- var nameWithReplacedDots = nameParserResult.Name.Replace(" ", ".");
+ var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.');
foreach (var group in groupedBy)
{
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index b43203e9d..3224ff412 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -23,11 +23,12 @@
-
+
-
+
+
@@ -44,7 +45,6 @@
-
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs
index f7df58786..5e952e47b 100644
--- a/Emby.Naming/TV/EpisodeResolver.cs
+++ b/Emby.Naming/TV/EpisodeResolver.cs
@@ -16,7 +16,7 @@ namespace Emby.Naming.TV
///
/// Initializes a new instance of the class.
///
- /// object containing VideoFileExtensions and passed to , , and .
+ /// object containing VideoFileExtensions and passed to , and .
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,
diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs
index 09a0cd189..4eef3ebc5 100644
--- a/Emby.Naming/Video/CleanStringParser.cs
+++ b/Emby.Naming/Video/CleanStringParser.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Emby.Naming.Video
@@ -16,8 +17,14 @@ namespace Emby.Naming.Video
/// List of regex to parse name and year from.
/// Parsing result string.
/// True if parsing was successful.
- public static bool TryClean(string name, IReadOnlyList expressions, out ReadOnlySpan newName)
+ public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList expressions, out ReadOnlySpan newName)
{
+ if (string.IsNullOrEmpty(name))
+ {
+ newName = ReadOnlySpan.Empty;
+ return false;
+ }
+
var len = expressions.Count;
for (int i = 0; i < len; i++)
{
@@ -41,7 +48,7 @@ namespace Emby.Naming.Video
return true;
}
- newName = string.Empty;
+ newName = ReadOnlySpan.Empty;
return false;
}
}
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index 1d3b36a1a..1fade985b 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -29,72 +29,75 @@ namespace Emby.Naming.Video
/// Path to file.
/// Returns object.
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;
}
diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs
deleted file mode 100644
index 439de1813..000000000
--- a/Emby.Naming/Video/FlagParser.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.IO;
-using Emby.Naming.Common;
-
-namespace Emby.Naming.Video
-{
- ///
- /// Parses list of flags from filename based on delimiters.
- ///
- public class FlagParser
- {
- private readonly NamingOptions _options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// object containing VideoFlagDelimiters.
- public FlagParser(NamingOptions options)
- {
- _options = options;
- }
-
- ///
- /// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
- ///
- /// Path to file.
- /// List of found flags.
- public string[] GetFlags(string path)
- {
- return GetFlags(path, _options.VideoFlagDelimiters);
- }
-
- ///
- /// Parses flags from filename based on delimiters.
- ///
- /// Path to file.
- /// Delimiters used to extract flags.
- /// List of found flags.
- public string[] GetFlags(string path, char[] delimiters)
- {
- if (string.IsNullOrEmpty(path))
- {
- return Array.Empty();
- }
-
- // 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);
- }
- }
-}
diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs
index 4fd5d78ba..089089989 100644
--- a/Emby.Naming/Video/Format3DParser.cs
+++ b/Emby.Naming/Video/Format3DParser.cs
@@ -1,45 +1,37 @@
using System;
-using System.Linq;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
///
- /// Parste 3D format related flags.
+ /// Parse 3D format related flags.
///
- public class Format3DParser
+ public static class Format3DParser
{
- private readonly NamingOptions _options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// object containing VideoFlagDelimiters and passes options to .
- public Format3DParser(NamingOptions options)
- {
- _options = options;
- }
+ // Static default result to save on allocation costs.
+ private static readonly Format3DResult _defaultResult = new (false, null);
///
/// Parse 3D format related flags.
///
/// Path to file.
+ /// The naming options.
/// Returns object.
- public Format3DResult Parse(string path)
+ public static Format3DResult Parse(ReadOnlySpan 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 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 path, ReadOnlySpan 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 path, Format3DRule rule, ReadOnlySpan 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;
}
}
}
diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs
index ac935f203..aac959c13 100644
--- a/Emby.Naming/Video/Format3DResult.cs
+++ b/Emby.Naming/Video/Format3DResult.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace Emby.Naming.Video
{
///
@@ -10,27 +8,24 @@ namespace Emby.Naming.Video
///
/// Initializes a new instance of the class.
///
- public Format3DResult()
+ /// A value indicating whether the parsed string contains 3D tokens.
+ /// The 3D format. Value might be null if [is3D] is false.
+ public Format3DResult(bool is3D, string? format3D)
{
- Tokens = new List();
+ Is3D = is3D;
+ Format3D = format3D;
}
///
- /// Gets or sets a value indicating whether [is3 d].
+ /// Gets a value indicating whether [is3 d].
///
/// true if [is3 d]; otherwise, false.
- public bool Is3D { get; set; }
+ public bool Is3D { get; }
///
- /// Gets or sets the format3 d.
+ /// Gets the format3 d.
///
/// The format3 d.
- public string? Format3D { get; set; }
-
- ///
- /// Gets or sets the tokens.
- ///
- /// The tokens.
- public List Tokens { get; set; }
+ public string? Format3D { get; }
}
}
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index 550c42961..36f65a562 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -85,10 +85,8 @@ namespace Emby.Naming.Video
/// Enumerable of videos.
public IEnumerable Resolve(IEnumerable 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();
diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs
index 1457db737..481773ff6 100644
--- a/Emby.Naming/Video/VideoFileInfo.cs
+++ b/Emby.Naming/Video/VideoFileInfo.cs
@@ -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.
///
/// The file name without extension.
- public string FileNameWithoutExtension => !IsDirectory
- ? System.IO.Path.GetFileNameWithoutExtension(Path)
- : System.IO.Path.GetFileName(Path);
+ public ReadOnlySpan FileNameWithoutExtension => !IsDirectory
+ ? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan())
+ : System.IO.Path.GetFileName(Path.AsSpan());
///
public override string ToString()
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 09a030d2d..7da2dcd7a 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -12,31 +12,19 @@ namespace Emby.Naming.Video
///
/// Resolves alternative versions and extras from list of video files.
///
- public class VideoListResolver
+ public static class VideoListResolver
{
- private readonly NamingOptions _options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// object containing CleanStringRegexes and VideoFlagDelimiters and passes options to and .
- public VideoListResolver(NamingOptions options)
- {
- _options = options;
- }
-
///
/// Resolves alternative versions and extras from list of video files.
///
/// List of related video files.
+ /// The naming options.
/// Indication we should consider multi-versions of content.
/// Returns enumerable of which groups files together when related.
- public IEnumerable Resolve(List files, bool supportMultiVersion = true)
+ public static IEnumerable Resolve(List 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()
.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()
.ToList()
};
info.Year = info.Files[0].Year;
- var extraBaseNames = new List { 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 { media } };
+ var info = new VideoInfo(media.Name) { Files = new[] { media } };
info.Year = info.Files[0].Year;
- var extras = GetExtras(remainingFiles, new List { 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 { folderName });
-
- remainingFiles = remainingFiles
- .Except(extras)
- .ToList();
-
+ var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
extras.AddRange(info.Extras);
info.Extras = extras;
}
@@ -164,95 +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 { i },
+ Files = new[] { i },
Year = i.Year
}));
return list;
}
- private IEnumerable GetVideosGroupedByVersion(List videos)
+ private static List GetVideosGroupedByVersion(List videos, NamingOptions namingOptions)
{
if (videos.Count == 0)
{
return videos;
}
- var list = new List();
+ 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 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++)
{
- if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
+ var video = videos[i];
+ if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
- testFilename = cleanName.ToString();
+ return videos;
}
+ }
- if (folderName.Length <= testFilename.Length)
+ // The list is created and overwritten in the caller, so we are allowed to do in-place sorting
+ videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
+
+ var list = new List
+ {
+ videos[0]
+ };
+
+ var alternateVersionsLen = videos.Count - 1;
+ var alternateVersions = new VideoFileInfo[alternateVersionsLen];
+ var extras = new List(list[0].Extras);
+ for (int i = 0; i < alternateVersionsLen; i++)
+ {
+ var video = videos[i + 1];
+ alternateVersions[i] = video.Files[0];
+ extras.AddRange(video.Extras);
+ }
+
+ list[0].AlternateVersions = alternateVersions;
+ list[0].Name = folderName.ToString();
+ list[0].Extras = extras;
+
+ return list;
+ }
+
+ private static bool HaveSameYear(IReadOnlyList 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)
{
- testFilename = testFilename.Substring(folderName.Length).Trim();
+ return false;
}
+ }
- return string.IsNullOrEmpty(testFilename)
+ return true;
+ }
+
+ private static bool IsEligibleForMultiVersion(ReadOnlySpan 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] == '-'
- || testFilename[0] == '_'
- || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
- }
-
- return false;
+ || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
- private List GetExtras(IEnumerable remainingFiles, List baseNames)
+ private static ReadOnlySpan TrimFilenameDelimiters(ReadOnlySpan name, ReadOnlySpan videoFlagDelimiters)
{
- foreach (var name in baseNames.ToList())
+ return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
+ }
+
+ private static bool StartsWith(ReadOnlySpan fileName, ReadOnlySpan baseName, ReadOnlySpan trimmedBaseName)
+ {
+ if (baseName.IsEmpty)
{
- var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
- baseNames.Add(trimmedName);
+ return false;
}
- return remainingFiles
- .Where(i => i.ExtraType != null)
- .Where(i => baseNames.Any(b =>
- i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
- .ToList();
+ return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
+ || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ ///
+ /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
+ ///
+ /// The list of remaining filenames.
+ /// The base name to use for the comparison.
+ /// The video flag delimiters.
+ /// A list of video extras for [baseName].
+ private static List ExtractExtras(IList remainingFiles, ReadOnlySpan baseName, ReadOnlySpan videoFlagDelimiters)
+ {
+ return ExtractExtras(remainingFiles, baseName, ReadOnlySpan.Empty, videoFlagDelimiters);
+ }
+
+ ///
+ /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
+ ///
+ /// The list of remaining filenames.
+ /// The first base name to use for the comparison.
+ /// The second base name to use for the comparison.
+ /// The video flag delimiters.
+ /// A list of video extras for [firstBaseName] and [secondBaseName].
+ private static List ExtractExtras(IList remainingFiles, ReadOnlySpan firstBaseName, ReadOnlySpan secondBaseName, ReadOnlySpan videoFlagDelimiters)
+ {
+ var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
+ var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
+
+ var result = new List();
+ 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;
}
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 619d1520e..c4ac5fdc6 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -1,45 +1,36 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using MediaBrowser.Common.Extensions;
namespace Emby.Naming.Video
{
///
/// Resolves from file path.
///
- public class VideoResolver
+ public static class VideoResolver
{
- private readonly NamingOptions _options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
- /// and passes options in , , and .
- public VideoResolver(NamingOptions options)
- {
- _options = options;
- }
-
///
/// Resolves the directory.
///
/// The path.
+ /// The naming options.
/// VideoFileInfo.
- public VideoFileInfo? ResolveDirectory(string? path)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
{
- return Resolve(path, true);
+ return Resolve(path, true, namingOptions);
}
///
/// Resolves the file.
///
/// The path.
+ /// The naming options.
/// VideoFileInfo.
- public VideoFileInfo? ResolveFile(string? path)
+ public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
{
- return Resolve(path, false);
+ return Resolve(path, false, namingOptions);
}
///
@@ -47,10 +38,11 @@ namespace Emby.Naming.Video
///
/// The path.
/// if set to true [is folder].
+ /// The naming options.
/// Whether or not the name should be parsed for info.
/// VideoFileInfo.
/// path is null.
- 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))
{
@@ -58,18 +50,18 @@ namespace Emby.Naming.Video
}
bool isStub = false;
- string? container = null;
+ ReadOnlySpan container = ReadOnlySpan.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;
}
@@ -80,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 newName))
+ && TryCleanString(name, namingOptions, out ReadOnlySpan newName))
{
name = newName.ToString();
}
@@ -106,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,
@@ -122,43 +111,47 @@ namespace Emby.Naming.Video
/// Determines if path is video file based on extension.
///
/// Path to file.
+ /// The naming options.
/// True if is video file.
- 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);
}
///
/// Determines if path is video file stub based on extension.
///
/// Path to file.
+ /// The naming options.
/// True if is video file stub.
- 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);
}
///
/// Tries to clean name of clutter.
///
/// Raw name.
+ /// The naming options.
/// Clean name.
/// True if cleaning of name was successful.
- public bool TryCleanString(string name, out ReadOnlySpan newName)
+ public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan newName)
{
- return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
+ return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}
///
/// Tries to get name and year from raw name.
///
/// Raw name.
+ /// The naming options.
/// Returns with name and optional year.
- 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);
}
}
}
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 16ee918c4..5a2aea642 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -11,6 +11,8 @@
true
true
enable
+ AllEnabledByDefault
+ ../jellyfin.ruleset
@@ -25,14 +27,9 @@
-
-
- ../jellyfin.ruleset
-
-
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 62e33e6c4..2b6618159 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -24,18 +24,15 @@
true
true
enable
+ AllEnabledByDefault
+ ../jellyfin.ruleset
-
-
- ../jellyfin.ruleset
-
-
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 660bbb2de..6edfad575 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -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;
}
///
@@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
///
/// The data directory.
- public string DataPath
- {
- get => _dataPath;
- private set => _dataPath = Directory.CreateDirectory(value).FullName;
- }
+ public string DataPath => _dataPath;
///
public string VirtualDataPath => "%AppDataPath%";
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 4f72c8ce1..d38535634 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -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 _configurations = new ConcurrentDictionary();
+ ///
+ /// The _configuration sync lock.
+ ///
+ private readonly object _configurationSyncLock = new object();
+
private ConfigurationStore[] _configurationStores = Array.Empty();
private IConfigurationFactory[] _configurationFactories = Array.Empty();
@@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase
///
private bool _configurationLoaded;
- ///
- /// The _configuration sync lock.
- ///
- private readonly object _configurationSyncLock = new object();
-
///
/// The _configuration.
///
@@ -297,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
///
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)
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 77819c764..0308a68e4 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -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, e.g. CreateInstance(typeof(int?)) returns null.
+ configuration = Activator.CreateInstance(type)!;
}
using var stream = new MemoryStream(buffer?.Length ?? 0);
@@ -53,7 +51,8 @@ namespace Emby.Server.Implementations.AppBase
Directory.CreateDirectory(directory);
// Save it after load in case we got new items
- using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
fs.Write(newBytes, 0, newBytesLen);
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 1b9bb86bb..82995deb3 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,8 +12,6 @@ using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
@@ -43,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
+using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.Networking.Configuration;
@@ -50,7 +51,6 @@ using Jellyfin.Networking.Manager;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
@@ -99,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
@@ -118,6 +119,7 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager;
+ private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
@@ -126,7 +128,6 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private string[] _urlPrefixes;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
///
/// Gets a value indicating whether this instance can self restart.
@@ -211,7 +212,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the configuration manager.
///
/// The configuration manager.
- protected IConfigurationManager ConfigurationManager { get; set; }
+ public ServerConfigurationManager ConfigurationManager { get; set; }
///
/// Gets or sets the service provider.
@@ -229,10 +230,9 @@ namespace Emby.Server.Implementations
public int HttpsPort { get; private set; }
///
- /// Gets the server configuration manager.
+ /// Gets the value of the PublishedServerUrl setting.
///
- /// The server configuration manager.
- public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
+ public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
///
/// Initializes a new instance of the class.
@@ -240,51 +240,37 @@ namespace Emby.Server.Implementations
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
+ /// The interface.
/// Instance of the interface.
/// Instance of the interface.
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
+ IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection serviceCollection)
{
- _xmlSerializer = new MyXmlSerializer();
-
- ServiceCollection = serviceCollection;
-
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
+ _startupOptions = options;
+ _startupConfig = startupConfig;
_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();
- NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger());
+ ServiceCollection = serviceCollection;
Logger = LoggerFactory.CreateLogger();
-
- _startupOptions = options;
-
- // 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;
+ _xmlSerializer = new MyXmlSerializer();
+ ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger(),
this,
- ServerConfigurationManager.Configuration,
+ ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
}
@@ -299,9 +285,9 @@ namespace Emby.Server.Implementations
if (!File.Exists(path))
{
var networkSettings = new NetworkConfiguration();
- ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
+ ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
_xmlSerializer.SerializeToFile(networkSettings, path);
- Logger?.LogDebug("Successfully migrated network settings.");
+ Logger.LogDebug("Successfully migrated network settings.");
}
}
@@ -351,10 +337,7 @@ namespace Emby.Server.Implementations
{
get
{
- if (_deviceId == null)
- {
- _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
- }
+ _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
@@ -374,7 +357,7 @@ namespace Emby.Server.Implementations
///
/// Creates an instance of type and resolves all constructor dependencies.
///
- /// /// The type.
+ /// The type.
/// T.
public T CreateInstance()
=> ActivatorUtilities.CreateInstance(ServiceProvider);
@@ -386,10 +369,7 @@ namespace Emby.Server.Implementations
/// System.Object.
protected object CreateInstanceSafe(Type type)
{
- if (_creatingInstances == null)
- {
- _creatingInstances = new List();
- }
+ _creatingInstances ??= new List();
if (_creatingInstances.IndexOf(type) != -1)
{
@@ -460,7 +440,7 @@ namespace Emby.Server.Implementations
}
///
- public IReadOnlyCollection GetExports(CreationDelegate defaultFunc, bool manageLifetime = true)
+ public IReadOnlyCollection GetExports(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
{
// Convert to list so this isn't executed for each iteration
var parts = GetExportTypes()
@@ -484,8 +464,9 @@ namespace Emby.Server.Implementations
/// Runs the startup tasks.
///
/// .
- public async Task RunStartupTasksAsync()
+ public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
{
+ cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks");
Resolve().AddTasks(GetExports(false));
@@ -499,14 +480,21 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports();
+ cancellationToken.ThrowIfCancellationRequested();
+
var stopWatch = new Stopwatch();
stopWatch.Start();
+
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
stopWatch.Restart();
+
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
stopWatch.Stop();
@@ -530,7 +518,21 @@ namespace Emby.Server.Implementations
///
public void Init()
{
- var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
+ DiscoverTypes();
+
+ ConfigurationManager.AddParts(GetExports());
+
+ // Have to migrate settings here as migration subsystem not yet initialised.
+ MigrateNetworkConfiguration();
+ NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger());
+
+ // Initialize runtime stat collection
+ if (ConfigurationManager.Configuration.EnableMetrics)
+ {
+ DotNetRuntimeStatsBuilder.Default().StartCollecting();
+ }
+
+ var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
HttpPort = networkConfiguration.HttpServerPortNumber;
HttpsPort = networkConfiguration.HttpsPortNumber;
@@ -548,8 +550,6 @@ namespace Emby.Server.Implementations
};
Certificate = GetCertificate(CertificateInfo);
- DiscoverTypes();
-
RegisterServices();
_pluginManager.RegisterServices(ServiceCollection);
@@ -564,7 +564,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddMemoryCache();
- ServiceCollection.AddSingleton(ConfigurationManager);
+ ServiceCollection.AddSingleton(ConfigurationManager);
+ ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton(this);
ServiceCollection.AddSingleton(_pluginManager);
ServiceCollection.AddSingleton(ApplicationPaths);
@@ -591,8 +592,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(this);
ServiceCollection.AddSingleton(ApplicationPaths);
- ServiceCollection.AddSingleton(ServerConfigurationManager);
-
ServiceCollection.AddSingleton();
ServiceCollection.AddSingleton();
@@ -604,12 +603,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton();
- // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required
- ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
-
- // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required
- ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
ServiceCollection.AddSingleton();
+ ServiceCollection.AddSingleton();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy isn't required
ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService));
@@ -674,14 +669,14 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton();
-
ServiceCollection.AddSingleton();
ServiceCollection.AddSingleton();
ServiceCollection.AddScoped();
ServiceCollection.AddScoped();
ServiceCollection.AddScoped();
+
+ ServiceCollection.AddSingleton();
}
///
@@ -779,7 +774,7 @@ namespace Emby.Server.Implementations
{
// For now there's no real way to inject these properly
BaseItem.Logger = Resolve>();
- BaseItem.ConfigurationManager = ServerConfigurationManager;
+ BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.LibraryManager = Resolve();
BaseItem.ProviderManager = Resolve();
BaseItem.LocalizationManager = Resolve();
@@ -801,13 +796,12 @@ namespace Emby.Server.Implementations
///
private void FindParts()
{
- if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
+ if (!ConfigurationManager.Configuration.IsPortAuthorized)
{
- ServerConfigurationManager.Configuration.IsPortAuthorized = true;
+ ConfigurationManager.Configuration.IsPortAuthorized = true;
ConfigurationManager.SaveConfiguration();
}
- ConfigurationManager.AddParts(GetExports());
_pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray();
@@ -911,7 +905,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)
@@ -920,10 +914,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;
}
@@ -1139,16 +1133,16 @@ namespace Emby.Server.Implementations
}
///
- public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
+ public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
///
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
{
// Published server ends with a /
- if (_startupOptions.PublishedServerUrl != null)
+ if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
- return _startupOptions.PublishedServerUrl.ToString().Trim('/');
+ return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(ipAddress, out port);
@@ -1165,10 +1159,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(HttpRequest request, int? port = null)
{
// Published server ends with a /
- if (_startupOptions.PublishedServerUrl != null)
+ if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
- return _startupOptions.PublishedServerUrl.ToString().Trim('/');
+ return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(request, out port);
@@ -1185,10 +1179,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(string hostname, int? port = null)
{
// Published server ends with a /
- if (_startupOptions.PublishedServerUrl != null)
+ if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
- return _startupOptions.PublishedServerUrl.ToString().Trim('/');
+ return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(hostname, out port);
@@ -1223,14 +1217,14 @@ namespace Emby.Server.Implementations
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
Host = host,
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
- Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
+ Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
}.ToString().TrimEnd('/');
}
public string FriendlyName =>
- string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
+ string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName
- : ServerConfigurationManager.Configuration.ServerName;
+ : ConfigurationManager.Configuration.ServerName;
///
/// Shuts down.
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 8c5fa09f6..448f12403 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -1,9 +1,10 @@
+#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;
@@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Channels
private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
///
/// Initializes a new instance of the class.
diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
index c69a07e83..ca8409402 100644
--- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
+++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
@@ -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
}
///
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 3011a37e3..82d80fc83 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -1,6 +1,7 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
@@ -8,11 +9,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;
@@ -107,7 +106,7 @@ namespace Emby.Server.Implementations.Collections
var name = _localizationManager.GetLocalizedString("Collections");
- await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false);
+ await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
return FindFolders(path).First();
}
@@ -124,7 +123,7 @@ namespace Emby.Server.Implementations.Collections
private IEnumerable GetCollections(User user)
{
- var folder = GetCollectionsFolder(false).Result;
+ var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
return folder == null
? Enumerable.Empty()
@@ -167,7 +166,7 @@ namespace Emby.Server.Implementations.Collections
parentFolder.AddChild(collection, CancellationToken.None);
- if (options.ItemIdList.Length > 0)
+ if (options.ItemIdList.Count > 0)
{
await AddToCollectionAsync(
collection.Id,
@@ -251,11 +250,7 @@ namespace Emby.Server.Implementations.Collections
if (fireEvent)
{
- ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs
- {
- Collection = collection,
- ItemsChanged = itemList
- });
+ ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
}
}
}
@@ -307,11 +302,7 @@ namespace Emby.Server.Implementations.Collections
},
RefreshPriority.High);
- ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
- {
- Collection = collection,
- ItemsChanged = itemList
- });
+ ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
}
///
@@ -319,11 +310,11 @@ namespace Emby.Server.Implementations.Collections
{
var results = new Dictionary();
- var allBoxsets = GetCollections(user).ToList();
+ var allBoxSets = GetCollections(user).ToList();
foreach (var item in items)
{
- if (!(item is ISupportsBoxSetGrouping))
+ if (item is not ISupportsBoxSetGrouping)
{
results[item.Id] = item;
}
@@ -331,20 +322,44 @@ namespace Emby.Server.Implementations.Collections
{
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
+
+ if (!alreadyInResults)
{
- results[item.Id] = item;
+ results[itemId] = item;
}
}
}
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index 7a8ed8c29..ff5602f24 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Globalization;
using System.IO;
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index cd9dbb1bd..01dc728c1 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using Emby.Server.Implementations.HttpServer;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 12a9e44e7..4a9b28085 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 8c756a7f4..6f23a0888 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -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);
}
}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
index 5c094ddd2..afc8966f9 100644
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -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> Query(string sql)
+ public IEnumerable> Query(string sql)
{
return _db.Query(sql);
}
- public IEnumerable> Query(string sql, params object[] values)
+ public IEnumerable> Query(string sql, params object[] values)
{
return _db.Query(sql, values);
}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 1af301ceb..3289e7609 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -1,7 +1,9 @@
+#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using SQLitePCL.pretty;
@@ -59,11 +61,11 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(conn =>
{
- conn.ExecuteAll(string.Join(";", queries));
+ conn.ExecuteAll(string.Join(';', queries));
});
}
- public static Guid ReadGuidFromBlob(this IResultSetValue result)
+ public static Guid ReadGuidFromBlob(this ResultSetValue result)
{
return new Guid(result.ToBlob());
}
@@ -84,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();
@@ -95,58 +97,147 @@ namespace Emby.Server.Implementations.Data
DateTimeStyles.None).ToUniversalTime();
}
- public static DateTime? TryReadDateTime(this IResultSetValue result)
+ public static bool TryReadDateTime(this IReadOnlyList 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 result, int index)
+ public static bool TryGetGuid(this IReadOnlyList 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 result, int index)
+ public static bool IsDbNull(this ResultSetValue result)
+ {
+ return result.SQLiteType == SQLiteType.Null;
+ }
+
+ public static string GetString(this IReadOnlyList result, int index)
{
return result[index].ToString();
}
- public static bool GetBoolean(this IReadOnlyList result, int index)
+ public static bool TryGetString(this IReadOnlyList 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 result, int index)
{
return result[index].ToBool();
}
- public static int GetInt32(this IReadOnlyList result, int index)
+ public static bool TryGetBoolean(this IReadOnlyList 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 result, int index)
+ public static bool TryGetInt32(this IReadOnlyList 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 result, int index)
{
return result[index].ToInt64();
}
- public static float GetFloat(this IReadOnlyList result, int index)
+ public static bool TryGetInt64(this IReadOnlyList 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 result, int index)
+ public static bool TryGetSingle(this IReadOnlyList 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 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 result, int index)
{
return result[index].ReadGuidFromBlob();
}
+ [Conditional("DEBUG")]
private static void CheckName(string name)
{
-#if DEBUG
throw new ArgumentException("Invalid param name: " + name, nameof(name));
-#endif
}
public static void TryBind(this IStatement statement, string name, double value)
@@ -350,7 +441,7 @@ namespace Emby.Server.Implementations.Data
}
}
- public static IEnumerable> ExecuteQuery(this IStatement statement)
+ public static IEnumerable> ExecuteQuery(this IStatement statement)
{
while (statement.MoveNext())
{
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 6e1f2feae..9b147b5d7 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -1,6 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
+using System.Buffers.Text;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -40,6 +43,7 @@ namespace Emby.Server.Implementations.Data
///
public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
{
+ private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2";
private readonly IServerConfigurationManager _config;
@@ -88,7 +92,7 @@ namespace Emby.Server.Implementations.Data
_imageProcessor = imageProcessor;
_typeMapper = new TypeMapper();
- _jsonOptions = JsonDefaults.GetOptions();
+ _jsonOptions = JsonDefaults.Options;
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
}
@@ -502,7 +506,7 @@ namespace Emby.Server.Implementations.Data
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
saveImagesStatement.TryBind("@Id", item.Id.ToByteArray());
- saveImagesStatement.TryBind("@Images", SerializeImages(item));
+ saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
saveImagesStatement.MoveNext();
}
@@ -687,7 +691,7 @@ namespace Emby.Server.Implementations.Data
if (item.Genres.Length > 0)
{
- saveItemStatement.TryBind("@Genres", string.Join("|", item.Genres));
+ saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres));
}
else
{
@@ -749,7 +753,7 @@ namespace Emby.Server.Implementations.Data
if (item.LockedFields.Length > 0)
{
- saveItemStatement.TryBind("@LockedFields", string.Join("|", item.LockedFields));
+ saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields));
}
else
{
@@ -758,7 +762,7 @@ namespace Emby.Server.Implementations.Data
if (item.Studios.Length > 0)
{
- saveItemStatement.TryBind("@Studios", string.Join("|", item.Studios));
+ saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios));
}
else
{
@@ -785,7 +789,7 @@ namespace Emby.Server.Implementations.Data
if (item.Tags.Length > 0)
{
- saveItemStatement.TryBind("@Tags", string.Join("|", item.Tags));
+ saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags));
}
else
{
@@ -807,7 +811,7 @@ namespace Emby.Server.Implementations.Data
if (item is Trailer trailer && trailer.TrailerTypes.Length > 0)
{
- saveItemStatement.TryBind("@TrailerTypes", string.Join("|", trailer.TrailerTypes));
+ saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes));
}
else
{
@@ -897,12 +901,12 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId);
saveItemStatement.TryBind("@Tagline", item.Tagline);
- saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item));
- saveItemStatement.TryBind("@Images", SerializeImages(item));
+ saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds));
+ saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
if (item.ProductionLocations.Length > 0)
{
- saveItemStatement.TryBind("@ProductionLocations", string.Join("|", item.ProductionLocations));
+ saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations));
}
else
{
@@ -911,7 +915,7 @@ namespace Emby.Server.Implementations.Data
if (item.ExtraIds.Length > 0)
{
- saveItemStatement.TryBind("@ExtraIds", string.Join("|", item.ExtraIds));
+ saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds));
}
else
{
@@ -931,7 +935,7 @@ namespace Emby.Server.Implementations.Data
string artists = null;
if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0)
{
- artists = string.Join("|", hasArtists.Artists);
+ artists = string.Join('|', hasArtists.Artists);
}
saveItemStatement.TryBind("@Artists", artists);
@@ -940,7 +944,7 @@ namespace Emby.Server.Implementations.Data
if (item is IHasAlbumArtist hasAlbumArtists
&& hasAlbumArtists.AlbumArtists.Count > 0)
{
- albumArtists = string.Join("|", hasAlbumArtists.AlbumArtists);
+ albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists);
}
saveItemStatement.TryBind("@AlbumArtists", albumArtists);
@@ -968,10 +972,10 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.MoveNext();
}
- private static string SerializeProviderIds(BaseItem item)
+ internal static string SerializeProviderIds(Dictionary providerIds)
{
StringBuilder str = new StringBuilder();
- foreach (var i in item.ProviderIds)
+ foreach (var i in providerIds)
{
// Ideally we shouldn't need this IsNullOrWhiteSpace check,
// but we're seeing some cases of bad data slip through
@@ -995,35 +999,25 @@ namespace Emby.Server.Implementations.Data
return str.ToString();
}
- private static void DeserializeProviderIds(string value, BaseItem item)
+ internal static void DeserializeProviderIds(string value, IHasProviderIds item)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
- if (item.ProviderIds.Count > 0)
+ foreach (var part in value.SpanSplit('|'))
{
- return;
- }
-
- var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
-
- foreach (var part in parts)
- {
- var idParts = part.Split('=');
-
- if (idParts.Length == 2)
+ var providerDelimiterIndex = part.IndexOf('=');
+ if (providerDelimiterIndex != -1 && providerDelimiterIndex == part.LastIndexOf('='))
{
- item.SetProviderId(idParts[0], idParts[1]);
+ item.SetProviderId(part.Slice(0, providerDelimiterIndex).ToString(), part.Slice(providerDelimiterIndex + 1).ToString());
}
}
}
- private string SerializeImages(BaseItem item)
+ internal string SerializeImages(ItemImageInfo[] images)
{
- var images = item.ImageInfos;
-
if (images.Length == 0)
{
return null;
@@ -1045,39 +1039,48 @@ namespace Emby.Server.Implementations.Data
return str.ToString();
}
- private void DeserializeImages(string value, BaseItem item)
+ internal ItemImageInfo[] DeserializeImages(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
- return;
+ return Array.Empty();
}
- if (item.ImageInfos.Length > 0)
- {
- return;
- }
+ // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
+ var valueSpan = value.AsSpan();
+ var count = valueSpan.Count('|') + 1;
- var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries);
- var list = new List();
- foreach (var part in parts)
+ var position = 0;
+ var result = new ItemImageInfo[count];
+ foreach (var part in valueSpan.Split('|'))
{
var image = ItemImageInfoFromValueString(part);
if (image != null)
{
- list.Add(image);
+ result[position++] = image;
}
}
- item.ImageInfos = list.ToArray();
+ if (position == count)
+ {
+ return result;
+ }
+
+ if (position == 0)
+ {
+ return Array.Empty();
+ }
+
+ // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
+ return result[..position];
}
- public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
+ private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
{
const char Delimiter = '*';
var path = image.Path ?? string.Empty;
- var hash = image.BlurHash ?? string.Empty;
bldr.Append(GetPathToSave(path))
.Append(Delimiter)
@@ -1087,48 +1090,105 @@ namespace Emby.Server.Implementations.Data
.Append(Delimiter)
.Append(image.Width)
.Append(Delimiter)
- .Append(image.Height)
- .Append(Delimiter)
- // Replace delimiters with other characters.
- // This can be removed when we migrate to a proper DB.
- .Append(hash.Replace('*', '/').Replace('|', '\\'));
+ .Append(image.Height);
+
+ var hash = image.BlurHash;
+ if (!string.IsNullOrEmpty(hash))
+ {
+ bldr.Append(Delimiter)
+ // Replace delimiters with other characters.
+ // This can be removed when we migrate to a proper DB.
+ .Append(hash.Replace('*', '/').Replace('|', '\\'));
+ }
}
- public ItemImageInfo ItemImageInfoFromValueString(string value)
+ internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan value)
{
- var parts = value.Split('*', StringSplitOptions.None);
-
- if (parts.Length < 3)
+ var nextSegment = value.IndexOf('*');
+ if (nextSegment == -1)
{
return null;
}
- var image = new ItemImageInfo();
+ ReadOnlySpan path = value[..nextSegment];
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf('*');
+ if (nextSegment == -1)
+ {
+ return null;
+ }
- image.Path = RestorePath(parts[0]);
+ ReadOnlySpan dateModified = value[..nextSegment];
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf('*');
+ if (nextSegment == -1)
+ {
+ nextSegment = value.Length;
+ }
- if (long.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks))
+ ReadOnlySpan imageType = value[..nextSegment];
+
+ var image = new ItemImageInfo
+ {
+ Path = RestorePath(path.ToString())
+ };
+
+ if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks))
{
image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
}
- if (Enum.TryParse(parts[2], true, out ImageType type))
+ if (Enum.TryParse(imageType.ToString(), true, out ImageType type))
{
image.Type = type;
}
- if (parts.Length >= 5)
+ // Optional parameters: width*height*blurhash
+ if (nextSegment + 1 < value.Length - 1)
{
- if (int.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
- && int.TryParse(parts[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf('*');
+ if (nextSegment == -1 || nextSegment == value.Length)
+ {
+ return image;
+ }
+
+ ReadOnlySpan widthSpan = value[..nextSegment];
+
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf('*');
+ if (nextSegment == -1)
+ {
+ nextSegment = value.Length;
+ }
+
+ ReadOnlySpan heightSpan = value[..nextSegment];
+
+ if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
+ && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
{
image.Width = width;
image.Height = height;
}
- if (parts.Length >= 6)
+ if (nextSegment < value.Length - 1)
{
- image.BlurHash = parts[5].Replace('/', '*').Replace('\\', '|');
+ value = value[(nextSegment + 1)..];
+ var length = value.Length;
+
+ Span blurHashSpan = stackalloc char[length];
+ for (int i = 0; i < length; i++)
+ {
+ var c = value[i];
+ blurHashSpan[i] = c switch
+ {
+ '/' => '*',
+ '\\' => '|',
+ _ => c
+ };
+ }
+
+ image.BlurHash = new string(blurHashSpan);
}
}
@@ -1241,12 +1301,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query)
+ private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query)
{
return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query));
}
- private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
+ private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
{
var typeString = reader.GetString(0);
@@ -1291,27 +1351,30 @@ namespace Emby.Server.Implementations.Data
if (queryHasStartDate)
{
- if (!reader.IsDBNull(index))
+ if (item is IHasStartDate hasStartDate && reader.TryReadDateTime(index, out var startDate))
{
- if (item is IHasStartDate hasStartDate)
- {
- hasStartDate.StartDate = reader[index].ReadDateTime();
- }
+ hasStartDate.StartDate = startDate;
}
index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var endDate))
{
- item.EndDate = reader[index].TryReadDateTime();
+ item.EndDate = endDate;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ var channelId = reader[index];
+ if (!channelId.IsDbNull())
{
- item.ChannelId = new Guid(reader.GetString(index));
+ if (!Utf8Parser.TryParse(channelId.ToBlob(), out Guid value, out _, standardFormat: 'N'))
+ {
+ var str = reader.GetString(index);
+ Logger.LogWarning("{ChannelId} isn't in the expected format", str);
+ value = new Guid(str);
+ }
+
+ item.ChannelId = value;
}
index++;
@@ -1320,33 +1383,25 @@ namespace Emby.Server.Implementations.Data
{
if (item is IHasProgramAttributes hasProgramAttributes)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isMovie))
{
- hasProgramAttributes.IsMovie = reader.GetBoolean(index);
+ hasProgramAttributes.IsMovie = isMovie;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isSeries))
{
- hasProgramAttributes.IsSeries = reader.GetBoolean(index);
+ hasProgramAttributes.IsSeries = isSeries;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var episodeTitle))
{
- hasProgramAttributes.EpisodeTitle = reader.GetString(index);
+ hasProgramAttributes.EpisodeTitle = episodeTitle;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isRepeat))
{
- hasProgramAttributes.IsRepeat = reader.GetBoolean(index);
+ hasProgramAttributes.IsRepeat = isRepeat;
}
-
- index++;
}
else
{
@@ -1354,242 +1409,190 @@ namespace Emby.Server.Implementations.Data
}
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetSingle(index++, out var communityRating))
{
- item.CommunityRating = reader.GetFloat(index);
+ item.CommunityRating = communityRating;
}
- index++;
-
if (HasField(query, ItemFields.CustomRating))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var customRating))
{
- item.CustomRating = reader.GetString(index);
+ item.CustomRating = customRating;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var indexNumber))
{
- item.IndexNumber = reader.GetInt32(index);
+ item.IndexNumber = indexNumber;
}
- index++;
-
if (HasField(query, ItemFields.Settings))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isLocked))
{
- item.IsLocked = reader.GetBoolean(index);
+ item.IsLocked = isLocked;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var preferredMetadataLanguage))
{
- item.PreferredMetadataLanguage = reader.GetString(index);
+ item.PreferredMetadataLanguage = preferredMetadataLanguage;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
{
- item.PreferredMetadataCountryCode = reader.GetString(index);
+ item.PreferredMetadataCountryCode = preferredMetadataCountryCode;
}
-
- index++;
}
if (HasField(query, ItemFields.Width))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var width))
{
- item.Width = reader.GetInt32(index);
+ item.Width = width;
}
-
- index++;
}
if (HasField(query, ItemFields.Height))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var height))
{
- item.Height = reader.GetInt32(index);
+ item.Height = height;
}
-
- index++;
}
if (HasField(query, ItemFields.DateLastRefreshed))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
{
- item.DateLastRefreshed = reader[index].ReadDateTime();
+ item.DateLastRefreshed = dateLastRefreshed;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var name))
{
- item.Name = reader.GetString(index);
+ item.Name = name;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var restorePath))
{
- item.Path = RestorePath(reader.GetString(index));
+ item.Path = RestorePath(restorePath);
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var premiereDate))
{
- item.PremiereDate = reader[index].TryReadDateTime();
+ item.PremiereDate = premiereDate;
}
- index++;
-
if (HasField(query, ItemFields.Overview))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var overview))
{
- item.Overview = reader.GetString(index);
+ item.Overview = overview;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var parentIndexNumber))
{
- item.ParentIndexNumber = reader.GetInt32(index);
+ item.ParentIndexNumber = parentIndexNumber;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var productionYear))
{
- item.ProductionYear = reader.GetInt32(index);
+ item.ProductionYear = productionYear;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var officialRating))
{
- item.OfficialRating = reader.GetString(index);
+ item.OfficialRating = officialRating;
}
- index++;
-
if (HasField(query, ItemFields.SortName))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var forcedSortName))
{
- item.ForcedSortName = reader.GetString(index);
+ item.ForcedSortName = forcedSortName;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt64(index++, out var runTimeTicks))
{
- item.RunTimeTicks = reader.GetInt64(index);
+ item.RunTimeTicks = runTimeTicks;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt64(index++, out var size))
{
- item.Size = reader.GetInt64(index);
+ item.Size = size;
}
- index++;
-
if (HasField(query, ItemFields.DateCreated))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var dateCreated))
{
- item.DateCreated = reader[index].ReadDateTime();
+ item.DateCreated = dateCreated;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var dateModified))
{
- item.DateModified = reader[index].ReadDateTime();
+ item.DateModified = dateModified;
}
- index++;
-
- item.Id = reader.GetGuid(index);
- index++;
+ item.Id = reader.GetGuid(index++);
if (HasField(query, ItemFields.Genres))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var genres))
{
- item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
+ item.Genres = genres.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetGuid(index++, out var parentId))
{
- item.ParentId = reader.GetGuid(index);
+ item.ParentId = parentId;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var audioString))
{
- if (Enum.TryParse(reader.GetString(index), true, out ProgramAudio audio))
+ // TODO Span overload coming in the future https://github.com/dotnet/runtime/issues/1916
+ if (Enum.TryParse(audioString, true, out ProgramAudio audio))
{
item.Audio = audio;
}
}
- index++;
-
// TODO: Even if not needed by apps, the server needs it internally
// But get this excluded from contexts where it is not needed
if (hasServiceName)
{
if (item is LiveTvChannel liveTvChannel)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var serviceName))
{
- liveTvChannel.ServiceName = reader.GetString(index);
+ liveTvChannel.ServiceName = serviceName;
}
}
index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isInMixedFolder))
{
- item.IsInMixedFolder = reader.GetBoolean(index);
+ item.IsInMixedFolder = isInMixedFolder;
}
- index++;
-
if (HasField(query, ItemFields.DateLastSaved))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var dateLastSaved))
{
- item.DateLastSaved = reader[index].ReadDateTime();
+ item.DateLastSaved = dateLastSaved;
}
-
- index++;
}
if (HasField(query, ItemFields.Settings))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var lockedFields))
{
IEnumerable GetLockedFields(string s)
{
@@ -1602,37 +1605,31 @@ namespace Emby.Server.Implementations.Data
}
}
- item.LockedFields = GetLockedFields(reader.GetString(index)).ToArray();
+ item.LockedFields = GetLockedFields(lockedFields).ToArray();
}
-
- index++;
}
if (HasField(query, ItemFields.Studios))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var studios))
{
- item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
+ item.Studios = studios.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
-
- index++;
}
if (HasField(query, ItemFields.Tags))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var tags))
{
- item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
+ item.Tags = tags.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
-
- index++;
}
if (hasTrailerTypes)
{
if (item is Trailer trailer)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var trailerTypes))
{
IEnumerable GetTrailerTypes(string s)
{
@@ -1645,7 +1642,7 @@ namespace Emby.Server.Implementations.Data
}
}
- trailer.TrailerTypes = GetTrailerTypes(reader.GetString(index)).ToArray();
+ trailer.TrailerTypes = GetTrailerTypes(trailerTypes).ToArray();
}
}
@@ -1654,19 +1651,17 @@ namespace Emby.Server.Implementations.Data
if (HasField(query, ItemFields.OriginalTitle))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var originalTitle))
{
- item.OriginalTitle = reader.GetString(index);
+ item.OriginalTitle = originalTitle;
}
-
- index++;
}
if (item is Video video)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var primaryVersionId))
{
- video.PrimaryVersionId = reader.GetString(index);
+ video.PrimaryVersionId = primaryVersionId;
}
}
@@ -1674,40 +1669,34 @@ namespace Emby.Server.Implementations.Data
if (HasField(query, ItemFields.DateLastMediaAdded))
{
- if (item is Folder folder && !reader.IsDBNull(index))
+ if (item is Folder folder && reader.TryReadDateTime(index, out var dateLastMediaAdded))
{
- folder.DateLastMediaAdded = reader[index].TryReadDateTime();
+ folder.DateLastMediaAdded = dateLastMediaAdded;
}
index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var album))
{
- item.Album = reader.GetString(index);
+ item.Album = album;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetSingle(index++, out var criticRating))
{
- item.CriticRating = reader.GetFloat(index);
+ item.CriticRating = criticRating;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isVirtualItem))
{
- item.IsVirtualItem = reader.GetBoolean(index);
+ item.IsVirtualItem = isVirtualItem;
}
- index++;
-
if (item is IHasSeries hasSeriesName)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var seriesName))
{
- hasSeriesName.SeriesName = reader.GetString(index);
+ hasSeriesName.SeriesName = seriesName;
}
}
@@ -1717,15 +1706,15 @@ namespace Emby.Server.Implementations.Data
{
if (item is Episode episode)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var seasonName))
{
- episode.SeasonName = reader.GetString(index);
+ episode.SeasonName = seasonName;
}
index++;
- if (!reader.IsDBNull(index))
+ if (reader.TryGetGuid(index, out var seasonId))
{
- episode.SeasonId = reader.GetGuid(index);
+ episode.SeasonId = seasonId;
}
}
else
@@ -1741,9 +1730,9 @@ namespace Emby.Server.Implementations.Data
{
if (hasSeries != null)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetGuid(index, out var seriesId))
{
- hasSeries.SeriesId = reader.GetGuid(index);
+ hasSeries.SeriesId = seriesId;
}
}
@@ -1752,56 +1741,48 @@ namespace Emby.Server.Implementations.Data
if (HasField(query, ItemFields.PresentationUniqueKey))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var presentationUniqueKey))
{
- item.PresentationUniqueKey = reader.GetString(index);
+ item.PresentationUniqueKey = presentationUniqueKey;
}
-
- index++;
}
if (HasField(query, ItemFields.InheritedParentalRatingValue))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var parentalRating))
{
- item.InheritedParentalRatingValue = reader.GetInt32(index);
+ item.InheritedParentalRatingValue = parentalRating;
}
-
- index++;
}
if (HasField(query, ItemFields.ExternalSeriesId))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var externalSeriesId))
{
- item.ExternalSeriesId = reader.GetString(index);
+ item.ExternalSeriesId = externalSeriesId;
}
-
- index++;
}
if (HasField(query, ItemFields.Taglines))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var tagLine))
{
- item.Tagline = reader.GetString(index);
+ item.Tagline = tagLine;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (item.ProviderIds.Count == 0 && reader.TryGetString(index, out var providerIds))
{
- DeserializeProviderIds(reader.GetString(index), item);
+ DeserializeProviderIds(providerIds, item);
}
index++;
if (query.DtoOptions.EnableImages)
{
- if (!reader.IsDBNull(index))
+ if (item.ImageInfos.Length == 0 && reader.TryGetString(index, out var imageInfos))
{
- DeserializeImages(reader.GetString(index), item);
+ item.ImageInfos = DeserializeImages(imageInfos);
}
index++;
@@ -1809,72 +1790,62 @@ namespace Emby.Server.Implementations.Data
if (HasField(query, ItemFields.ProductionLocations))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var productionLocations))
{
- item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray();
+ item.ProductionLocations = productionLocations.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
-
- index++;
}
if (HasField(query, ItemFields.ExtraIds))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var extraIds))
{
- item.ExtraIds = SplitToGuids(reader.GetString(index));
+ item.ExtraIds = SplitToGuids(extraIds);
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var totalBitrate))
{
- item.TotalBitrate = reader.GetInt32(index);
+ item.TotalBitrate = totalBitrate;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var extraTypeString))
{
- if (Enum.TryParse(reader.GetString(index), true, out ExtraType extraType))
+ if (Enum.TryParse(extraTypeString, true, out ExtraType extraType))
{
item.ExtraType = extraType;
}
}
- index++;
-
if (hasArtistFields)
{
- if (item is IHasArtist hasArtists && !reader.IsDBNull(index))
+ if (item is IHasArtist hasArtists && reader.TryGetString(index, out var artists))
{
- hasArtists.Artists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
+ hasArtists.Artists = artists.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
index++;
- if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index))
+ if (item is IHasAlbumArtist hasAlbumArtists && reader.TryGetString(index, out var albumArtists))
{
- hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
+ hasAlbumArtists.AlbumArtists = albumArtists.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var externalId))
{
- item.ExternalId = reader.GetString(index);
+ item.ExternalId = externalId;
}
- index++;
-
if (HasField(query, ItemFields.SeriesPresentationUniqueKey))
{
if (hasSeries != null)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var seriesPresentationUniqueKey))
{
- hasSeries.SeriesPresentationUniqueKey = reader.GetString(index);
+ hasSeries.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
}
}
@@ -1883,21 +1854,19 @@ namespace Emby.Server.Implementations.Data
if (enableProgramAttributes)
{
- if (item is LiveTvProgram program && !reader.IsDBNull(index))
+ if (item is LiveTvProgram program && reader.TryGetString(index, out var showId))
{
- program.ShowId = reader.GetString(index);
+ program.ShowId = showId;
}
index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetGuid(index, out var ownerId))
{
- item.OwnerId = reader.GetGuid(index);
+ item.OwnerId = ownerId;
}
- index++;
-
return item;
}
@@ -1977,21 +1946,21 @@ namespace Emby.Server.Implementations.Data
/// The reader.
/// The item.
/// ChapterInfo.
- private ChapterInfo GetChapter(IReadOnlyList reader, BaseItem item)
+ private ChapterInfo GetChapter(IReadOnlyList reader, BaseItem item)
{
var chapter = new ChapterInfo
{
StartPositionTicks = reader.GetInt64(0)
};
- if (!reader.IsDBNull(1))
+ if (reader.TryGetString(1, out var chapterName))
{
- chapter.Name = reader.GetString(1);
+ chapter.Name = chapterName;
}
- if (!reader.IsDBNull(2))
+ if (reader.TryGetString(2, out var imagePath))
{
- chapter.ImagePath = reader.GetString(2);
+ chapter.ImagePath = imagePath;
if (!string.IsNullOrEmpty(chapter.ImagePath))
{
@@ -2006,9 +1975,9 @@ namespace Emby.Server.Implementations.Data
}
}
- if (!reader.IsDBNull(3))
+ if (reader.TryReadDateTime(3, out var imageDateModified))
{
- chapter.ImageDateModified = reader[3].ReadDateTime();
+ chapter.ImageDateModified = imageDateModified;
}
return chapter;
@@ -2116,30 +2085,7 @@ namespace Emby.Server.Implementations.Data
|| query.IsLiked.HasValue;
}
- private readonly ItemFields[] _allFields = Enum.GetNames(typeof(ItemFields))
- .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
- .ToArray();
-
- private string[] GetColumnNamesFromField(ItemFields field)
- {
- switch (field)
- {
- case ItemFields.Settings:
- return new[] { "IsLocked", "PreferredMetadataCountryCode", "PreferredMetadataLanguage", "LockedFields" };
- case ItemFields.ServiceName:
- return new[] { "ExternalServiceId" };
- case ItemFields.SortName:
- return new[] { "ForcedSortName" };
- case ItemFields.Taglines:
- return new[] { "Tagline" };
- case ItemFields.Tags:
- return new[] { "Tags" };
- case ItemFields.IsHD:
- return Array.Empty();
- default:
- return new[] { field.ToString() };
- }
- }
+ private readonly ItemFields[] _allFields = Enum.GetValues();
private bool HasField(InternalItemsQuery query, ItemFields name)
{
@@ -2321,77 +2267,98 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x));
}
- private List GetFinalColumnsToSelect(InternalItemsQuery query, IEnumerable startColumns)
+ private void SetFinalColumnsToSelect(InternalItemsQuery query, List columns)
{
- var list = startColumns.ToList();
-
foreach (var field in _allFields)
{
if (!HasField(query, field))
{
- foreach (var fieldToRemove in GetColumnNamesFromField(field))
+ switch (field)
{
- list.Remove(fieldToRemove);
+ case ItemFields.Settings:
+ columns.Remove("IsLocked");
+ columns.Remove("PreferredMetadataCountryCode");
+ columns.Remove("PreferredMetadataLanguage");
+ columns.Remove("LockedFields");
+ break;
+ case ItemFields.ServiceName:
+ columns.Remove("ExternalServiceId");
+ break;
+ case ItemFields.SortName:
+ columns.Remove("ForcedSortName");
+ break;
+ case ItemFields.Taglines:
+ columns.Remove("Tagline");
+ break;
+ case ItemFields.Tags:
+ columns.Remove("Tags");
+ break;
+ case ItemFields.IsHD:
+ // do nothing
+ break;
+ default:
+ columns.Remove(field.ToString());
+ break;
}
}
}
if (!HasProgramAttributes(query))
{
- list.Remove("IsMovie");
- list.Remove("IsSeries");
- list.Remove("EpisodeTitle");
- list.Remove("IsRepeat");
- list.Remove("ShowId");
+ columns.Remove("IsMovie");
+ columns.Remove("IsSeries");
+ columns.Remove("EpisodeTitle");
+ columns.Remove("IsRepeat");
+ columns.Remove("ShowId");
}
if (!HasEpisodeAttributes(query))
{
- list.Remove("SeasonName");
- list.Remove("SeasonId");
+ columns.Remove("SeasonName");
+ columns.Remove("SeasonId");
}
if (!HasStartDate(query))
{
- list.Remove("StartDate");
+ columns.Remove("StartDate");
}
if (!HasTrailerTypes(query))
{
- list.Remove("TrailerTypes");
+ columns.Remove("TrailerTypes");
}
if (!HasArtistFields(query))
{
- list.Remove("AlbumArtists");
- list.Remove("Artists");
+ columns.Remove("AlbumArtists");
+ columns.Remove("Artists");
}
if (!HasSeriesFields(query))
{
- list.Remove("SeriesId");
+ columns.Remove("SeriesId");
}
if (!HasEpisodeAttributes(query))
{
- list.Remove("SeasonName");
- list.Remove("SeasonId");
+ columns.Remove("SeasonName");
+ columns.Remove("SeasonId");
}
if (!query.DtoOptions.EnableImages)
{
- list.Remove("Images");
+ columns.Remove("Images");
}
if (EnableJoinUserData(query))
{
- list.Add("UserDatas.UserId");
- list.Add("UserDatas.lastPlayedDate");
- list.Add("UserDatas.playbackPositionTicks");
- list.Add("UserDatas.playcount");
- list.Add("UserDatas.isFavorite");
- list.Add("UserDatas.played");
- list.Add("UserDatas.rating");
+ columns.Add("UserDatas.UserId");
+ columns.Add("UserDatas.lastPlayedDate");
+ columns.Add("UserDatas.playbackPositionTicks");
+ columns.Add("UserDatas.playcount");
+ columns.Add("UserDatas.isFavorite");
+ columns.Add("UserDatas.played");
+ columns.Add("UserDatas.rating");
}
if (query.SimilarTo != null)
@@ -2439,7 +2406,7 @@ namespace Emby.Server.Implementations.Data
builder.Append(") as SimilarityScore");
- list.Add(builder.ToString());
+ columns.Add(builder.ToString());
var oldLen = query.ExcludeItemIds.Length;
var newLen = oldLen + item.ExtraIds.Length + 1;
@@ -2466,10 +2433,8 @@ namespace Emby.Server.Implementations.Data
builder.Append(") as SearchScore");
- list.Add(builder.ToString());
+ columns.Add(builder.ToString());
}
-
- return list;
}
private void BindSearchParams(InternalItemsQuery query, IStatement statement)
@@ -2535,31 +2500,25 @@ namespace Emby.Server.Implementations.Data
private string GetGroupBy(InternalItemsQuery query)
{
- var groups = new List();
-
- if (EnableGroupByPresentationUniqueKey(query))
+ var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query);
+ if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey)
{
- groups.Add("PresentationUniqueKey");
+ return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey";
+ }
+
+ if (enableGroupByPresentationUniqueKey)
+ {
+ return " Group by PresentationUniqueKey";
}
if (query.GroupBySeriesPresentationUniqueKey)
{
- groups.Add("SeriesPresentationUniqueKey");
- }
-
- if (groups.Count > 0)
- {
- return " Group by " + string.Join(",", groups);
+ return " Group by SeriesPresentationUniqueKey";
}
return string.Empty;
}
- private string GetFromText(string alias = "A")
- {
- return " from TypedBaseItems " + alias;
- }
-
public int GetCount(InternalItemsQuery query)
{
if (query == null)
@@ -2577,17 +2536,21 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = new List { "count(distinct PresentationUniqueKey)" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 256)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
+ var commandText = commandTextBuilder.ToString();
int count;
using (var connection = GetConnection(true))
{
@@ -2629,20 +2592,23 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = _retriveItemColumns.ToList();
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 1024)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
- commandText += GetGroupBy(query)
- + GetOrderByText(query);
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -2650,15 +2616,18 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
+ var commandText = commandTextBuilder.ToString();
var items = new List();
using (var connection = GetConnection(true))
{
@@ -2721,87 +2690,22 @@ namespace Emby.Server.Implementations.Data
private string FixUnicodeChars(string buffer)
{
- if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2013', '-'); // en dash
- }
-
- if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2014', '-'); // em dash
- }
-
- if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2015', '-'); // horizontal bar
- }
-
- if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2017', '_'); // double low line
- }
-
- if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
- }
-
- if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
- }
-
- if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
- }
-
- if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
- }
-
- if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
- }
-
- if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
- }
-
- if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
- }
-
- if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
- }
-
- if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2032', '\''); // prime
- }
-
- if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2033', '\"'); // double prime
- }
-
- if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u0060', '\''); // grave accent
- }
-
- if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u00B4', '\''); // acute accent
- }
-
- return buffer;
+ buffer = buffer.Replace('\u2013', '-'); // en dash
+ buffer = buffer.Replace('\u2014', '-'); // em dash
+ buffer = buffer.Replace('\u2015', '-'); // horizontal bar
+ buffer = buffer.Replace('\u2017', '_'); // double low line
+ buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
+ buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
+ buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
+ buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
+ buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
+ buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
+ buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
+ buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
+ buffer = buffer.Replace('\u2032', '\''); // prime
+ buffer = buffer.Replace('\u2033', '\"'); // double prime
+ buffer = buffer.Replace('\u0060', '\''); // grave accent
+ return buffer.Replace('\u00B4', '\''); // acute accent
}
private void AddItem(List items, BaseItem newItem)
@@ -2879,20 +2783,27 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = _retriveItemColumns.ToList();
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 512)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
var whereText = whereClauses.Count == 0 ?
string.Empty :
- " where " + string.Join(" AND ", whereClauses);
+ string.Join(" AND ", whereClauses);
- commandText += whereText
- + GetGroupBy(query)
- + GetOrderByText(query);
+ if (!string.IsNullOrEmpty(whereText))
+ {
+ commandTextBuilder.Append(" where ")
+ .Append(whereText);
+ }
+
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -2900,43 +2811,58 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
- var statementTexts = new List();
+ var itemQuery = string.Empty;
+ var totalRecordCountQuery = string.Empty;
if (!isReturningZeroItems)
{
- statementTexts.Add(commandText);
+ itemQuery = commandTextBuilder.ToString();
}
if (query.EnableTotalRecordCount)
{
- commandText = string.Empty;
+ commandTextBuilder.Clear();
+ commandTextBuilder.Append(" select ");
+
+ List columnsToSelect;
if (EnableGroupByPresentationUniqueKey(query))
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List { "count (distinct PresentationUniqueKey)" };
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List { "count (distinct SeriesPresentationUniqueKey)" };
}
else
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+ columnsToSelect = new List { "count (guid)" };
}
- commandText += GetJoinUserDataText(query)
- + whereText;
- statementTexts.Add(commandText);
+ SetFinalColumnsToSelect(query, columnsToSelect);
+
+ commandTextBuilder.AppendJoin(',', columnsToSelect)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
+ if (!string.IsNullOrEmpty(whereText))
+ {
+ commandTextBuilder.Append(" where ")
+ .Append(whereText);
+ }
+
+ totalRecordCountQuery = commandTextBuilder.ToString();
}
var list = new List();
@@ -2946,11 +2872,12 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts);
+ var itemQueryStatement = PrepareStatement(db, itemQuery);
+ var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery);
if (!isReturningZeroItems)
{
- using (var statement = statements[0])
+ using (var statement = itemQueryStatement)
{
if (EnableJoinUserData(query))
{
@@ -2980,11 +2907,14 @@ namespace Emby.Server.Implementations.Data
}
}
}
+
+ LogQueryTime("GetItems.ItemQuery", itemQuery, now);
}
+ now = DateTime.UtcNow;
if (query.EnableTotalRecordCount)
{
- using (var statement = statements[statements.Length - 1])
+ using (var statement = totalRecordCountQueryStatement)
{
if (EnableJoinUserData(query))
{
@@ -2999,11 +2929,12 @@ namespace Emby.Server.Implementations.Data
result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
}
+
+ LogQueryTime("GetItems.TotalRecordCount", totalRecordCountQuery, now);
}
}, ReadTransactionMode);
}
- LogQueryTime("GetItems", commandText, now);
result.Items = list;
return result;
}
@@ -3039,7 +2970,7 @@ namespace Emby.Server.Implementations.Data
return string.Empty;
}
- return " ORDER BY " + string.Join(",", orderBy.Select(i =>
+ return " ORDER BY " + string.Join(',', orderBy.Select(i =>
{
var columnMap = MapOrderByField(i.Item1, query);
@@ -3136,19 +3067,22 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
- var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = new List { "guid" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 256)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
- commandText += GetGroupBy(query)
- + GetOrderByText(query);
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -3156,15 +3090,18 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
+ var commandText = commandTextBuilder.ToString();
var list = new List();
using (var connection = GetConnection(true))
{
@@ -3203,7 +3140,9 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
- var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
+ var columns = new List { "guid", "path" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandText = "select " + string.Join(',', columns) + FromText;
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
@@ -3245,12 +3184,8 @@ namespace Emby.Server.Implementations.Data
foreach (var row in statement.ExecuteQuery())
{
var id = row.GetGuid(0);
- string path = null;
- if (!row.IsDBNull(1))
- {
- path = row.GetString(1);
- }
+ row.TryGetString(1, out var path);
list.Add(new Tuple(id, path));
}
@@ -3283,9 +3218,11 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
+ var columns = new List { "guid" };
+ SetFinalColumnsToSelect(query, columns);
var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }))
- + GetFromText()
+ + string.Join(',', columns)
+ + FromText
+ GetJoinUserDataText(query);
var whereClauses = GetWhereClauses(query, null);
@@ -3325,19 +3262,23 @@ namespace Emby.Server.Implementations.Data
{
commandText = string.Empty;
+ List columnsToSelect;
if (EnableGroupByPresentationUniqueKey(query))
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List { "count (distinct PresentationUniqueKey)" };
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List { "count (distinct SeriesPresentationUniqueKey)" };
}
else
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+ columnsToSelect = new List { "count (guid)" };
}
+ SetFinalColumnsToSelect(query, columnsToSelect);
+ commandText += " select " + string.Join(',', columnsToSelect) + FromText;
+
commandText += GetJoinUserDataText(query)
+ whereText;
statementTexts.Add(commandText);
@@ -3584,11 +3525,11 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind("@IsFolder", query.IsFolder);
}
- var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray();
+ var includeTypes = query.IncludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray();
// Only specify excluded types if no included types are specified
if (includeTypes.Length == 0)
{
- var excludeTypes = query.ExcludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray();
+ var excludeTypes = query.ExcludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray();
if (excludeTypes.Length == 1)
{
whereClauses.Add("type<>@type");
@@ -3596,7 +3537,7 @@ namespace Emby.Server.Implementations.Data
}
else if (excludeTypes.Length > 1)
{
- var inClause = string.Join(",", excludeTypes.Select(i => "'" + i + "'"));
+ var inClause = string.Join(',', excludeTypes.Select(i => "'" + i + "'"));
whereClauses.Add($"type not in ({inClause})");
}
}
@@ -3607,7 +3548,7 @@ namespace Emby.Server.Implementations.Data
}
else if (includeTypes.Length > 1)
{
- var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'"));
+ var inClause = string.Join(',', includeTypes.Select(i => "'" + i + "'"));
whereClauses.Add($"type in ({inClause})");
}
@@ -3618,7 +3559,7 @@ namespace Emby.Server.Implementations.Data
}
else if (query.ChannelIds.Count > 1)
{
- var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+ var inClause = string.Join(',', query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
whereClauses.Add($"ChannelId in ({inClause})");
}
@@ -4351,7 +4292,7 @@ namespace Emby.Server.Implementations.Data
}
else if (query.Years.Length > 1)
{
- var val = string.Join(",", query.Years);
+ var val = string.Join(',', query.Years);
whereClauses.Add("ProductionYear in (" + val + ")");
}
@@ -4401,7 +4342,7 @@ namespace Emby.Server.Implementations.Data
}
else if (queryMediaTypes.Length > 1)
{
- var val = string.Join(",", queryMediaTypes.Select(i => "'" + i + "'"));
+ var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
whereClauses.Add("MediaType in (" + val + ")");
}
@@ -4444,7 +4385,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(string.Join(" AND ", excludeIds));
}
- if (query.ExcludeProviderIds.Count > 0)
+ if (query.ExcludeProviderIds != null && query.ExcludeProviderIds.Count > 0)
{
var excludeIds = new List();
@@ -4474,7 +4415,7 @@ namespace Emby.Server.Implementations.Data
}
}
- if (query.HasAnyProviderId.Count > 0)
+ if (query.HasAnyProviderId != null && query.HasAnyProviderId.Count > 0)
{
var hasProviderIds = new List();
@@ -4498,7 +4439,7 @@ namespace Emby.Server.Implementations.Data
var paramName = "@HasAnyProviderId" + index;
// this is a search for the placeholder
- hasProviderIds.Add("ProviderIds like " + paramName + "");
+ hasProviderIds.Add("ProviderIds like " + paramName);
// this replaces the placeholder with a value, here: %key=val%
if (statement != null)
@@ -4532,56 +4473,50 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb"));
}
- var includedItemByNameTypes = GetItemByNameTypesInQuery(query).SelectMany(MapIncludeItemTypes).ToList();
- var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
-
var queryTopParentIds = query.TopParentIds;
- if (queryTopParentIds.Length == 1)
+ if (queryTopParentIds.Length > 0)
{
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
- {
- whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
- if (statement != null)
- {
- statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
- }
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
- }
- else
- {
- whereClauses.Add("(TopParentId=@TopParentId)");
- }
+ var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
+ var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
- if (statement != null)
+ if (queryTopParentIds.Length == 1)
{
- statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
- }
- }
- else if (queryTopParentIds.Length > 1)
- {
- var val = string.Join(",", queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
-
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
- {
- whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
- if (statement != null)
+ if (enableItemsByName && includedItemByNameTypes.Count == 1)
{
- statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
+ statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
}
+ else if (enableItemsByName && includedItemByNameTypes.Count > 1)
+ {
+ var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
+ whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
+ }
+ else
+ {
+ whereClauses.Add("(TopParentId=@TopParentId)");
+ }
+
+ statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
}
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
+ else if (queryTopParentIds.Length > 1)
{
- var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
- }
- else
- {
- whereClauses.Add("TopParentId in (" + val + ")");
+ var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+
+ if (enableItemsByName && includedItemByNameTypes.Count == 1)
+ {
+ whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
+ statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ }
+ else if (enableItemsByName && includedItemByNameTypes.Count > 1)
+ {
+ var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
+ whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
+ }
+ else
+ {
+ whereClauses.Add("TopParentId in (" + val + ")");
+ }
}
}
@@ -4597,7 +4532,7 @@ namespace Emby.Server.Implementations.Data
if (query.AncestorIds.Length > 1)
{
- var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+ var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
}
@@ -4790,27 +4725,27 @@ namespace Emby.Server.Implementations.Data
if (IsTypeInQuery(nameof(Person), query))
{
- list.Add(nameof(Person));
+ list.Add(typeof(Person).FullName);
}
if (IsTypeInQuery(nameof(Genre), query))
{
- list.Add(nameof(Genre));
+ list.Add(typeof(Genre).FullName);
}
if (IsTypeInQuery(nameof(MusicGenre), query))
{
- list.Add(nameof(MusicGenre));
+ list.Add(typeof(MusicGenre).FullName);
}
if (IsTypeInQuery(nameof(MusicArtist), query))
{
- list.Add(nameof(MusicArtist));
+ list.Add(typeof(MusicArtist).FullName);
}
if (IsTypeInQuery(nameof(Studio), query))
{
- list.Add(nameof(Studio));
+ list.Add(typeof(Studio).FullName);
}
return list;
@@ -4863,17 +4798,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- var types = new[]
- {
- nameof(Episode),
- nameof(Video),
- nameof(Movie),
- nameof(MusicVideo),
- nameof(Series),
- nameof(Season)
- };
-
- if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ if (query.IncludeItemTypes.Contains(nameof(Episode), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Video), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Movie), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(MusicVideo), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Series), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Season), StringComparer.OrdinalIgnoreCase))
{
return true;
}
@@ -4915,15 +4845,10 @@ namespace Emby.Server.Implementations.Data
typeof(AggregateFolder)
};
- public void UpdateInheritedValues(CancellationToken cancellationToken)
- {
- UpdateInheritedTags(cancellationToken);
- }
-
- private void UpdateInheritedTags(CancellationToken cancellationToken)
+ public void UpdateInheritedValues()
{
string sql = string.Join(
- ";",
+ ';',
new string[]
{
"delete from itemvalues where type = 6",
@@ -4946,37 +4871,38 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
}
- private static Dictionary GetTypeMapDictionary()
+ private static Dictionary GetTypeMapDictionary()
{
- var dict = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var dict = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var t in _knownTypes)
{
- dict[t.Name] = new[] { t.FullName };
+ dict[t.Name] = t.FullName ;
}
- dict["Program"] = new[] { typeof(LiveTvProgram).FullName };
- dict["TvChannel"] = new[] { typeof(LiveTvChannel).FullName };
+ dict["Program"] = typeof(LiveTvProgram).FullName;
+ dict["TvChannel"] = typeof(LiveTvChannel).FullName;
return dict;
}
// Not crazy about having this all the way down here, but at least it's in one place
- private readonly Dictionary _types = GetTypeMapDictionary();
+ private readonly Dictionary _types = GetTypeMapDictionary();
- private string[] MapIncludeItemTypes(string value)
+ private string MapIncludeItemTypes(string value)
{
- if (_types.TryGetValue(value, out string[] result))
+ if (_types.TryGetValue(value, out string result))
{
return result;
}
if (IsValidType(value))
{
- return new[] { value };
+ return value;
}
- return Array.Empty();
+ Logger.LogWarning("Unknown item type: {ItemType}", value);
+ return null;
}
public void DeleteItem(Guid id)
@@ -5148,7 +5074,7 @@ AND Type = @InternalPersonType)");
}
else if (queryPersonTypes.Count > 1)
{
- var val = string.Join(",", queryPersonTypes.Select(i => "'" + i + "'"));
+ var val = string.Join(',', queryPersonTypes.Select(i => "'" + i + "'"));
whereClauses.Add("PersonType in (" + val + ")");
}
@@ -5162,7 +5088,7 @@ AND Type = @InternalPersonType)");
}
else if (queryExcludePersonTypes.Count > 1)
{
- var val = string.Join(",", queryExcludePersonTypes.Select(i => "'" + i + "'"));
+ var val = string.Join(',', queryExcludePersonTypes.Select(i => "'" + i + "'"));
whereClauses.Add("PersonType not in (" + val + ")");
}
@@ -5279,64 +5205,87 @@ AND Type = @InternalPersonType)");
public List GetStudioNames()
{
- return GetItemValueNames(new[] { 3 }, new List(), new List());
+ return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty());
}
public List GetAllArtistNames()
{
- return GetItemValueNames(new[] { 0, 1 }, new List(), new List());
+ return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty());
}
public List GetMusicGenreNames()
{
- return GetItemValueNames(new[] { 2 }, new List { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" }, new List());
+ return GetItemValueNames(
+ new[] { 2 },
+ new string[]
+ {
+ typeof(Audio).FullName,
+ typeof(MusicVideo).FullName,
+ typeof(MusicAlbum).FullName,
+ typeof(MusicArtist).FullName
+ },
+ Array.Empty());
}
public List GetGenreNames()
{
- return GetItemValueNames(new[] { 2 }, new List(), new List { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" });
+ return GetItemValueNames(
+ new[] { 2 },
+ Array.Empty(),
+ new string[]
+ {
+ typeof(Audio).FullName,
+ typeof(MusicVideo).FullName,
+ typeof(MusicAlbum).FullName,
+ typeof(MusicArtist).FullName
+ });
}
- private List GetItemValueNames(int[] itemValueTypes, List withItemTypes, List excludeItemTypes)
+ private List GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes)
{
CheckDisposed();
- withItemTypes = withItemTypes.SelectMany(MapIncludeItemTypes).ToList();
- excludeItemTypes = excludeItemTypes.SelectMany(MapIncludeItemTypes).ToList();
-
var now = DateTime.UtcNow;
- var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
- ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
-
- var commandText = "Select Value From ItemValues where " + typeClause;
+ var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
+ if (itemValueTypes.Length == 1)
+ {
+ stringBuilder.Append('=')
+ .Append(itemValueTypes[0]);
+ }
+ else
+ {
+ stringBuilder.Append(" in (")
+ .AppendJoin(',', itemValueTypes)
+ .Append(')');
+ }
if (withItemTypes.Count > 0)
{
- var typeString = string.Join(",", withItemTypes.Select(i => "'" + i + "'"));
- commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))";
+ stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (")
+ .AppendJoinInSingleQuotes(',', withItemTypes)
+ .Append("))");
}
if (excludeItemTypes.Count > 0)
{
- var typeString = string.Join(",", excludeItemTypes.Select(i => "'" + i + "'"));
- commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))";
+ stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (")
+ .AppendJoinInSingleQuotes(',', excludeItemTypes)
+ .Append("))");
}
- commandText += " Group By CleanValue";
+ stringBuilder.Append(" Group By CleanValue");
+ var commandText = stringBuilder.ToString();
var list = new List();
using (var connection = GetConnection(true))
+ using (var statement = PrepareStatement(connection, commandText))
{
- using (var statement = PrepareStatement(connection, commandText))
+ foreach (var row in statement.ExecuteQuery())
{
- foreach (var row in statement.ExecuteQuery())
+ if (row.TryGetString(0, out var result))
{
- if (!row.IsDBNull(0))
- {
- list.Add(row.GetString(0));
- }
+ list.Add(result);
}
}
}
@@ -5362,18 +5311,19 @@ AND Type = @InternalPersonType)");
var now = DateTime.UtcNow;
var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
- ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
+ ("Type=" + itemValueTypes[0]) :
+ ("Type in (" + string.Join(',', itemValueTypes) + ")");
InternalItemsQuery typeSubQuery = null;
- Dictionary itemCountColumns = null;
+ string itemCountColumns = null;
+ var stringBuilder = new StringBuilder(1024);
var typesToCount = query.IncludeItemTypes;
if (typesToCount.Length > 0)
{
- var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B");
+ stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B");
typeSubQuery = new InternalItemsQuery(query.User)
{
@@ -5389,20 +5339,22 @@ AND Type = @InternalPersonType)");
};
var whereClauses = GetWhereClauses(typeSubQuery, null);
- whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")");
+ stringBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses)
+ .Append(" AND ")
+ .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ")
+ .Append(typeClause)
+ .Append(")) as itemTypes");
- itemCountColumnQuery += " where " + string.Join(" AND ", whereClauses);
-
- itemCountColumns = new Dictionary()
- {
- { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
- };
+ itemCountColumns = stringBuilder.ToString();
+ stringBuilder.Clear();
}
List columns = _retriveItemColumns.ToList();
- if (itemCountColumns != null)
+ // Unfortunately we need to add it to columns to ensure the order of the columns in the select
+ if (!string.IsNullOrEmpty(itemCountColumns))
{
- columns.AddRange(itemCountColumns.Values);
+ columns.Add(itemCountColumns);
}
// do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo
@@ -5415,7 +5367,6 @@ AND Type = @InternalPersonType)");
ItemIds = query.ItemIds,
TopParentIds = query.TopParentIds,
ParentId = query.ParentId,
- IsPlayed = query.IsPlayed,
IsAiring = query.IsAiring,
IsMovie = query.IsMovie,
IsSports = query.IsSports,
@@ -5424,23 +5375,24 @@ AND Type = @InternalPersonType)");
IsSeries = query.IsSeries
};
- columns = GetFinalColumnsToSelect(query, columns);
-
- var commandText = "select "
- + string.Join(",", columns)
- + GetFromText()
- + GetJoinUserDataText(query);
+ SetFinalColumnsToSelect(query, columns);
var innerWhereClauses = GetWhereClauses(innerQuery, null);
- var innerWhereText = innerWhereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", innerWhereClauses);
+ stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ")
+ .Append(typeClause)
+ .Append(" AND ItemId in (select guid from TypedBaseItems");
+ if (innerWhereClauses.Count > 0)
+ {
+ stringBuilder.Append(" where ")
+ .AppendJoin(" AND ", innerWhereClauses);
+ }
- var whereText = " where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))";
+ stringBuilder.Append("))");
var outerQuery = new InternalItemsQuery(query.User)
{
+ IsPlayed = query.IsPlayed,
IsFavorite = query.IsFavorite,
IsFavoriteOrLiked = query.IsFavoriteOrLiked,
IsLiked = query.IsLiked,
@@ -5461,21 +5413,31 @@ AND Type = @InternalPersonType)");
};
var outerWhereClauses = GetWhereClauses(outerQuery, null);
-
if (outerWhereClauses.Count != 0)
{
- whereText += " AND " + string.Join(" AND ", outerWhereClauses);
+ stringBuilder.Append(" AND ")
+ .AppendJoin(" AND ", outerWhereClauses);
}
- commandText += whereText + " group by PresentationUniqueKey";
+ var whereText = stringBuilder.ToString();
+ stringBuilder.Clear();
- if (query.SimilarTo != null || !string.IsNullOrEmpty(query.SearchTerm))
+ stringBuilder.Append("select ")
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query))
+ .Append(whereText)
+ .Append(" group by PresentationUniqueKey");
+
+ if (query.OrderBy.Count != 0
+ || query.SimilarTo != null
+ || !string.IsNullOrEmpty(query.SearchTerm))
{
- commandText += GetOrderByText(query);
+ stringBuilder.Append(GetOrderByText(query));
}
else
{
- commandText += " order by SortName";
+ stringBuilder.Append(" order by SortName");
}
if (query.Limit.HasValue || query.StartIndex.HasValue)
@@ -5484,32 +5446,39 @@ AND Type = @InternalPersonType)");
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ stringBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ stringBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
- var statementTexts = new List();
+ string commandText = string.Empty;
+
if (!isReturningZeroItems)
{
- statementTexts.Add(commandText);
+ commandText = stringBuilder.ToString();
}
+ string countText = string.Empty;
if (query.EnableTotalRecordCount)
{
- var countText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query)
- + whereText;
+ stringBuilder.Clear();
+ var columnsToSelect = new List { "count (distinct PresentationUniqueKey)" };
+ SetFinalColumnsToSelect(query, columnsToSelect);
+ stringBuilder.Append("select ")
+ .AppendJoin(',', columnsToSelect)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query))
+ .Append(whereText);
- statementTexts.Add(countText);
+ countText = stringBuilder.ToString();
}
var list = new List<(BaseItem, ItemCounts)>();
@@ -5519,11 +5488,9 @@ AND Type = @InternalPersonType)");
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts);
-
if (!isReturningZeroItems)
{
- using (var statement = statements[0])
+ using (var statement = PrepareStatement(db, commandText))
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
@@ -5564,13 +5531,7 @@ AND Type = @InternalPersonType)");
if (query.EnableTotalRecordCount)
{
- commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query)
- + whereText;
-
- using (var statement = statements[statements.Length - 1])
+ using (var statement = PrepareStatement(db, countText))
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
@@ -5607,7 +5568,7 @@ AND Type = @InternalPersonType)");
return result;
}
- private ItemCounts GetItemCounts(IReadOnlyList reader, int countStartColumn, string[] typesToCount)
+ private static ItemCounts GetItemCounts(IReadOnlyList reader, int countStartColumn, string[] typesToCount)
{
var counts = new ItemCounts();
@@ -5616,51 +5577,43 @@ AND Type = @InternalPersonType)");
return counts;
}
- var typeString = reader.IsDBNull(countStartColumn) ? null : reader.GetString(countStartColumn);
-
- if (string.IsNullOrWhiteSpace(typeString))
+ if (!reader.TryGetString(countStartColumn, out var typeString))
{
return counts;
}
- var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries)
- .ToLookup(x => x);
-
- foreach (var type in allTypes)
+ foreach (var typeName in typeString.AsSpan().Split('|'))
{
- var value = type.Count();
- var typeName = type.Key;
-
- if (string.Equals(typeName, typeof(Series).FullName, StringComparison.OrdinalIgnoreCase))
+ if (typeName.Equals(typeof(Series).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.SeriesCount = value;
+ counts.SeriesCount++;
}
- else if (string.Equals(typeName, typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.EpisodeCount = value;
+ counts.EpisodeCount++;
}
- else if (string.Equals(typeName, typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.MovieCount = value;
+ counts.MovieCount++;
}
- else if (string.Equals(typeName, typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.AlbumCount = value;
+ counts.AlbumCount++;
}
- else if (string.Equals(typeName, typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.ArtistCount = value;
+ counts.ArtistCount++;
}
- else if (string.Equals(typeName, typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.SongCount = value;
+ counts.SongCount++;
}
- else if (string.Equals(typeName, typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.TrailerCount = value;
+ counts.TrailerCount++;
}
- counts.ItemCount += value;
+ counts.ItemCount++;
}
return counts;
@@ -5809,7 +5762,10 @@ AND Type = @InternalPersonType)");
var endIndex = Math.Min(people.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
{
- insertText.AppendFormat("(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),", i.ToString(CultureInfo.InvariantCulture));
+ insertText.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),",
+ i.ToString(CultureInfo.InvariantCulture));
}
// Remove last comma
@@ -5843,7 +5799,7 @@ AND Type = @InternalPersonType)");
}
}
- private PersonInfo GetPerson(IReadOnlyList reader)
+ private PersonInfo GetPerson(IReadOnlyList reader)
{
var item = new PersonInfo
{
@@ -5851,19 +5807,19 @@ AND Type = @InternalPersonType)");
Name = reader.GetString(1)
};
- if (!reader.IsDBNull(2))
+ if (reader.TryGetString(2, out var role))
{
- item.Role = reader.GetString(2);
+ item.Role = role;
}
- if (!reader.IsDBNull(3))
+ if (reader.TryGetString(3, out var type))
{
- item.Type = reader.GetString(3);
+ item.Type = type;
}
- if (!reader.IsDBNull(4))
+ if (reader.TryGetInt32(4, out var sortOrder))
{
- item.SortOrder = reader.GetInt32(4);
+ item.SortOrder = sortOrder;
}
return item;
@@ -6050,7 +6006,7 @@ AND Type = @InternalPersonType)");
///
/// The reader.
/// ChapterInfo.
- private MediaStream GetMediaStream(IReadOnlyList reader)
+ private MediaStream GetMediaStream(IReadOnlyList reader)
{
var item = new MediaStream
{
@@ -6059,157 +6015,157 @@ AND Type = @InternalPersonType)");
item.Type = Enum.Parse(reader[2].ToString(), true);
- if (reader[3].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(3, out var codec))
{
- item.Codec = reader[3].ToString();
+ item.Codec = codec;
}
- if (reader[4].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(4, out var language))
{
- item.Language = reader[4].ToString();
+ item.Language = language;
}
- if (reader[5].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(5, out var channelLayout))
{
- item.ChannelLayout = reader[5].ToString();
+ item.ChannelLayout = channelLayout;
}
- if (reader[6].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(6, out var profile))
{
- item.Profile = reader[6].ToString();
+ item.Profile = profile;
}
- if (reader[7].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(7, out var aspectRatio))
{
- item.AspectRatio = reader[7].ToString();
+ item.AspectRatio = aspectRatio;
}
- if (reader[8].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(8, out var path))
{
- item.Path = RestorePath(reader[8].ToString());
+ item.Path = RestorePath(path);
}
item.IsInterlaced = reader.GetBoolean(9);
- if (reader[10].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(10, out var bitrate))
{
- item.BitRate = reader.GetInt32(10);
+ item.BitRate = bitrate;
}
- if (reader[11].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(11, out var channels))
{
- item.Channels = reader.GetInt32(11);
+ item.Channels = channels;
}
- if (reader[12].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(12, out var sampleRate))
{
- item.SampleRate = reader.GetInt32(12);
+ item.SampleRate = sampleRate;
}
item.IsDefault = reader.GetBoolean(13);
item.IsForced = reader.GetBoolean(14);
item.IsExternal = reader.GetBoolean(15);
- if (reader[16].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(16, out var width))
{
- item.Width = reader.GetInt32(16);
+ item.Width = width;
}
- if (reader[17].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(17, out var height))
{
- item.Height = reader.GetInt32(17);
+ item.Height = height;
}
- if (reader[18].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetSingle(18, out var averageFrameRate))
{
- item.AverageFrameRate = reader.GetFloat(18);
+ item.AverageFrameRate = averageFrameRate;
}
- if (reader[19].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetSingle(19, out var realFrameRate))
{
- item.RealFrameRate = reader.GetFloat(19);
+ item.RealFrameRate = realFrameRate;
}
- if (reader[20].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetSingle(20, out var level))
{
- item.Level = reader.GetFloat(20);
+ item.Level = level;
}
- if (reader[21].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(21, out var pixelFormat))
{
- item.PixelFormat = reader[21].ToString();
+ item.PixelFormat = pixelFormat;
}
- if (reader[22].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(22, out var bitDepth))
{
- item.BitDepth = reader.GetInt32(22);
+ item.BitDepth = bitDepth;
}
- if (reader[23].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetBoolean(23, out var isAnamorphic))
{
- item.IsAnamorphic = reader.GetBoolean(23);
+ item.IsAnamorphic = isAnamorphic;
}
- if (reader[24].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(24, out var refFrames))
{
- item.RefFrames = reader.GetInt32(24);
+ item.RefFrames = refFrames;
}
- if (reader[25].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(25, out var codecTag))
{
- item.CodecTag = reader.GetString(25);
+ item.CodecTag = codecTag;
}
- if (reader[26].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(26, out var comment))
{
- item.Comment = reader.GetString(26);
+ item.Comment = comment;
}
- if (reader[27].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(27, out var nalLengthSize))
{
- item.NalLengthSize = reader.GetString(27);
+ item.NalLengthSize = nalLengthSize;
}
- if (reader[28].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetBoolean(28, out var isAVC))
{
- item.IsAVC = reader[28].ToBool();
+ item.IsAVC = isAVC;
}
- if (reader[29].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(29, out var title))
{
- item.Title = reader[29].ToString();
+ item.Title = title;
}
- if (reader[30].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(30, out var timeBase))
{
- item.TimeBase = reader[30].ToString();
+ item.TimeBase = timeBase;
}
- if (reader[31].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(31, out var codecTimeBase))
{
- item.CodecTimeBase = reader[31].ToString();
+ item.CodecTimeBase = codecTimeBase;
}
- if (reader[32].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(32, out var colorPrimaries))
{
- item.ColorPrimaries = reader[32].ToString();
+ item.ColorPrimaries = colorPrimaries;
}
- if (reader[33].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(33, out var colorSpace))
{
- item.ColorSpace = reader[33].ToString();
+ item.ColorSpace = colorSpace;
}
- if (reader[34].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(34, out var colorTransfer))
{
- item.ColorTransfer = reader[34].ToString();
+ item.ColorTransfer = colorTransfer;
}
if (item.Type == MediaStreamType.Subtitle)
{
- item.localizedUndefined = _localization.GetLocalizedString("Undefined");
- item.localizedDefault = _localization.GetLocalizedString("Default");
- item.localizedForced = _localization.GetLocalizedString("Forced");
+ item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
+ item.LocalizedDefault = _localization.GetLocalizedString("Default");
+ item.LocalizedForced = _localization.GetLocalizedString("Forced");
}
return item;
@@ -6261,7 +6217,7 @@ AND Type = @InternalPersonType)");
CheckDisposed();
if (id == Guid.Empty)
{
- throw new ArgumentException(nameof(id));
+ throw new ArgumentException("Guid can't be empty.", nameof(id));
}
if (attachments == null)
@@ -6351,36 +6307,36 @@ AND Type = @InternalPersonType)");
///
/// The reader.
/// MediaAttachment.
- private MediaAttachment GetMediaAttachment(IReadOnlyList reader)
+ private MediaAttachment GetMediaAttachment(IReadOnlyList reader)
{
var item = new MediaAttachment
{
Index = reader[1].ToInt()
};
- if (reader[2].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(2, out var codec))
{
- item.Codec = reader[2].ToString();
+ item.Codec = codec;
}
- if (reader[2].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(3, out var codecTag))
{
- item.CodecTag = reader[3].ToString();
+ item.CodecTag = codecTag;
}
- if (reader[4].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(4, out var comment))
{
- item.Comment = reader[4].ToString();
+ item.Comment = comment;
}
- if (reader[6].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(5, out var fileName))
{
- item.FileName = reader[5].ToString();
+ item.FileName = fileName;
}
- if (reader[6].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(6, out var mimeType))
{
- item.MimeType = reader[6].ToString();
+ item.MimeType = mimeType;
}
return item;
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 2c4e8e0fc..ef9af1dcd 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- db.ExecuteAll(string.Join(";", new[] {
+ db.ExecuteAll(string.Join(';', new[] {
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
@@ -348,16 +350,16 @@ namespace Emby.Server.Implementations.Data
/// Read a row from the specified reader into the provided userData object.
///
///
- private UserItemData ReadRow(IReadOnlyList reader)
+ private UserItemData ReadRow(IReadOnlyList 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 +367,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;
diff --git a/Emby.Server.Implementations/Data/TypeMapper.cs b/Emby.Server.Implementations/Data/TypeMapper.cs
index 7044b1d19..064664e1f 100644
--- a/Emby.Server.Implementations/Data/TypeMapper.cs
+++ b/Emby.Server.Implementations/Data/TypeMapper.cs
@@ -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.
///
- private readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary();
+ private readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary();
///
/// Gets the type.
@@ -21,26 +21,16 @@ namespace Emby.Server.Implementations.Data
/// Name of the type.
/// Type.
/// typeName is null.
- public Type GetType(string typeName)
+ public Type? GetType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
{
throw new ArgumentNullException(nameof(typeName));
}
- return _typeMap.GetOrAdd(typeName, LookupType);
- }
-
- ///
- /// Lookups the type.
- ///
- /// Name of the type.
- /// Type.
- 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));
}
}
}
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index fa6ac95fd..3d15b3e76 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index da5047d24..2637addce 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 54b18a8c8..7411239a1 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -665,10 +667,7 @@ namespace Emby.Server.Implementations.Dto
var tag = GetImageCacheTag(item, image);
if (!string.IsNullOrEmpty(image.BlurHash))
{
- if (dto.ImageBlurHashes == null)
- {
- dto.ImageBlurHashes = new Dictionary>();
- }
+ dto.ImageBlurHashes ??= new Dictionary>();
if (!dto.ImageBlurHashes.ContainsKey(image.Type))
{
@@ -702,10 +701,7 @@ namespace Emby.Server.Implementations.Dto
if (hashes.Count > 0)
{
- if (dto.ImageBlurHashes == null)
- {
- dto.ImageBlurHashes = new Dictionary>();
- }
+ dto.ImageBlurHashes ??= new Dictionary>();
dto.ImageBlurHashes[imageType] = hashes;
}
@@ -898,10 +894,7 @@ namespace Emby.Server.Implementations.Dto
dto.Taglines = new string[] { item.Tagline };
}
- if (dto.Taglines == null)
- {
- dto.Taglines = Array.Empty();
- }
+ dto.Taglines ??= Array.Empty();
}
dto.Type = item.GetBaseItemKind();
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 08047ba47..9c90de1ed 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -9,6 +9,7 @@
+
@@ -27,10 +28,11 @@
+
-
-
-
+
+
+
@@ -43,22 +45,20 @@
false
true
true
+ enable
AD0001
+ AllEnabledByDefault
+ ../jellyfin.ruleset
-
-
- ../jellyfin.ruleset
-
-
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 14201ead2..0a4efd73c 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -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 e)
- {
- NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
}
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index ae1b51b4c..5bb4100ba 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
index 824bb85f4..e0ca02d98 100644
--- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 9486874d5..2e72b18f5 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -1,3 +1,4 @@
+using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
@@ -29,7 +30,7 @@ namespace Emby.Server.Implementations.EntryPoints
///
/// The UDP server.
///
- private UdpServer _udpServer;
+ private UdpServer? _udpServer;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private bool _disposed = false;
@@ -49,10 +50,12 @@ namespace Emby.Server.Implementations.EntryPoints
///
public Task RunAsync()
{
+ CheckDisposed();
+
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)
{
@@ -62,6 +65,14 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
+ private void CheckDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(this.GetType().Name);
+ }
+ }
+
///
public void Dispose()
{
@@ -71,9 +82,8 @@ namespace Emby.Server.Implementations.EntryPoints
}
_cancellationTokenSource.Cancel();
- _udpServer.Dispose();
_cancellationTokenSource.Dispose();
- _cancellationTokenSource = null;
+ _udpServer?.Dispose();
_udpServer = null;
_disposed = true;
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index 1989e9ed2..d3bcd5e13 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly Dictionary> _changedItems = new Dictionary>();
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 keys))
+ if (!_changedItems.TryGetValue(e.UserId, out List? keys))
{
keys = new List();
_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)
{
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 4a0fc8239..9afabf527 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -1,6 +1,5 @@
#pragma warning disable CS1591
-using System;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index 024404ceb..c87f7dbbd 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -2,8 +2,8 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Net;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
{
- return (AuthorizationInfo)cached;
+ return (AuthorizationInfo)cached!; // Cache should never contain null
}
return GetAuthorization(requestContext);
@@ -55,15 +55,15 @@ namespace Emby.Server.Implementations.HttpServer.Security
}
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
- in Dictionary auth,
+ in Dictionary? auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
{
- string deviceId = null;
- string device = null;
- string client = null;
- string version = null;
- string token = null;
+ string? deviceId = null;
+ string? device = null;
+ string? client = null;
+ string? version = null;
+ string? token = null;
if (auth != null)
{
@@ -206,7 +206,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
///
/// The HTTP req.
/// Dictionary{System.StringSystem.String}.
- private Dictionary GetAuthorizationDictionary(HttpContext httpReq)
+ private Dictionary? GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
@@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
- return GetAuthorization(auth);
+ return GetAuthorization(auth.Count > 0 ? auth[0] : null);
}
///
@@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
///
/// The HTTP req.
/// Dictionary{System.StringSystem.String}.
- private Dictionary GetAuthorizationDictionary(HttpRequest httpReq)
+ private Dictionary? GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
@@ -232,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
auth = httpReq.Headers[HeaderNames.Authorization];
}
- return GetAuthorization(auth);
+ return GetAuthorization(auth.Count > 0 ? auth[0] : null);
}
///
@@ -240,43 +240,43 @@ namespace Emby.Server.Implementations.HttpServer.Security
///
/// The authorization header.
/// Dictionary{System.StringSystem.String}.
- private Dictionary GetAuthorization(string authorizationHeader)
+ private Dictionary? GetAuthorization(ReadOnlySpan authorizationHeader)
{
if (authorizationHeader == null)
{
return null;
}
- var parts = authorizationHeader.Split(' ', 2);
+ var firstSpace = authorizationHeader.IndexOf(' ');
- // There should be at least to parts
- if (parts.Length != 2)
+ // There should be at least two parts
+ if (firstSpace == -1)
{
return null;
}
- var acceptedNames = new[] { "MediaBrowser", "Emby" };
+ var name = authorizationHeader[..firstSpace];
- // It has to be a digest request
- if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
+ if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
+ && !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
{
return null;
}
- // Remove uptil the first space
- authorizationHeader = parts[1];
- parts = authorizationHeader.Split(',');
+ authorizationHeader = authorizationHeader[(firstSpace + 1)..];
var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
- foreach (var item in parts)
+ foreach (var item in authorizationHeader.Split(','))
{
- var param = item.Trim().Split('=', 2);
+ var trimmedItem = item.Trim();
+ var firstEqualsSign = trimmedItem.IndexOf('=');
- if (param.Length == 2)
+ if (firstEqualsSign > 0)
{
- var value = NormalizeValue(param[1].Trim('"'));
- result[param[0]] = value;
+ var key = trimmedItem[..firstEqualsSign].ToString();
+ var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString());
+ result[key] = value;
}
}
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 040b6b9e4..c375f36ce 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User;
- return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user);
+ return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
}
public SessionInfo GetSession(object requestContext)
@@ -36,14 +36,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetSession((HttpContext)requestContext);
}
- public User GetUser(HttpContext requestContext)
+ public User? GetUser(HttpContext requestContext)
{
var session = GetSession(requestContext);
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
}
- public User GetUser(object requestContext)
+ public User? GetUser(object requestContext)
{
return GetUser(((HttpRequest)requestContext).HttpContext);
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7e0c2c1da..8f7d60669 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Buffers;
using System.IO.Pipelines;
@@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.HttpServer
RemoteEndPoint = remoteEndPoint;
QueryString = query;
- _jsonOptions = JsonDefaults.GetOptions();
+ _jsonOptions = JsonDefaults.Options;
LastActivityDate = DateTime.Now;
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index d6cf6233e..861c0a95e 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -14,15 +16,18 @@ namespace Emby.Server.Implementations.HttpServer
public class WebSocketManager : IWebSocketManager
{
private readonly IWebSocketListener[] _webSocketListeners;
+ private readonly IAuthService _authService;
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
public WebSocketManager(
+ IAuthService authService,
IEnumerable webSocketListeners,
ILogger logger,
ILoggerFactory loggerFactory)
{
_webSocketListeners = webSocketListeners.ToArray();
+ _authService = authService;
_logger = logger;
_loggerFactory = loggerFactory;
}
@@ -30,6 +35,7 @@ namespace Emby.Server.Implementations.HttpServer
///
public async Task WebSocketRequestHandler(HttpContext context)
{
+ _ = _authService.Authenticate(context.Request);
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index 7435e9d0b..47a83d77c 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index 3353fae9d..aa80bccd7 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index c0e757543..64d802457 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -2,12 +2,12 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Text;
+using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.IO
private readonly List _shortcutHandlers = new List();
private readonly string _tempPath;
- private readonly bool _isEnvironmentCaseInsensitive;
+ private static readonly bool _isEnvironmentCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public ManagedFileSystem(
ILogger logger,
@@ -32,8 +32,6 @@ namespace Emby.Server.Implementations.IO
{
Logger = logger;
_tempPath = applicationPaths.TempDirectory;
-
- _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
}
public virtual void AddShortcutHandler(IShortcutHandler handler)
@@ -64,7 +62,7 @@ namespace Emby.Server.Implementations.IO
/// The filename.
/// System.String.
/// filename
- public virtual string ResolveShortcut(string filename)
+ public virtual string? ResolveShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
{
@@ -72,7 +70,7 @@ namespace Emby.Server.Implementations.IO
}
var extension = Path.GetExtension(filename);
- var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
+ var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
return handler?.Resolve(filename);
}
@@ -246,16 +244,23 @@ namespace Emby.Server.Implementations.IO
{
result.Length = fileInfo.Length;
- // Issue #2354 get the size of files behind symbolic links
- if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
+ // Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
+ if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
{
- using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+ try
{
- result.Length = thisFileStream.Length;
+ using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+ {
+ result.Length = thisFileStream.Length;
+ }
+ }
+ catch (FileNotFoundException ex)
+ {
+ // Dangling symlinks cannot be detected before opening the file unfortunately...
+ Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
+ result.Exists = false;
}
}
-
- result.DirectoryName = fileInfo.DirectoryName;
}
result.CreationTimeUtc = GetCreationTimeUtc(info);
@@ -294,16 +299,37 @@ namespace Emby.Server.Implementations.IO
/// The filename.
/// System.String.
/// The filename is null.
- public virtual string GetValidFilename(string filename)
+ public string GetValidFilename(string filename)
{
- var builder = new StringBuilder(filename);
-
- foreach (var c in Path.GetInvalidFileNameChars())
+ var invalid = Path.GetInvalidFileNameChars();
+ var first = filename.IndexOfAny(invalid);
+ if (first == -1)
{
- builder = builder.Replace(c, ' ');
+ // Fast path for clean strings
+ return filename;
}
- return builder.ToString();
+ return string.Create(
+ filename.Length,
+ (filename, invalid, first),
+ (chars, state) =>
+ {
+ state.filename.AsSpan().CopyTo(chars);
+
+ chars[state.first++] = ' ';
+
+ var len = chars.Length;
+ foreach (var c in state.invalid)
+ {
+ for (int i = state.first; i < len; i++)
+ {
+ if (chars[i] == c)
+ {
+ chars[i] = ' ';
+ }
+ }
+ }
+ });
}
///
@@ -487,26 +513,9 @@ namespace Emby.Server.Implementations.IO
throw new ArgumentNullException(nameof(path));
}
- var separatorChar = Path.DirectorySeparatorChar;
-
- return path.IndexOf(parentPath.TrimEnd(separatorChar) + separatorChar, StringComparison.OrdinalIgnoreCase) != -1;
- }
-
- public virtual bool IsRootPath(string path)
- {
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- var parent = Path.GetDirectoryName(path);
-
- if (!string.IsNullOrEmpty(parent))
- {
- return false;
- }
-
- return true;
+ return path.Contains(
+ Path.TrimEndingDirectorySeparator(parentPath) + Path.DirectorySeparatorChar,
+ _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
public virtual string NormalizePath(string path)
@@ -521,7 +530,7 @@ namespace Emby.Server.Implementations.IO
return path;
}
- return path.TrimEnd(Path.DirectorySeparatorChar);
+ return Path.TrimEndingDirectorySeparator(path);
}
public virtual bool AreEqual(string path1, string path2)
@@ -536,7 +545,10 @@ namespace Emby.Server.Implementations.IO
return false;
}
- return string.Equals(NormalizePath(path1), NormalizePath(path2), StringComparison.OrdinalIgnoreCase);
+ return string.Equals(
+ NormalizePath(path1),
+ NormalizePath(path2),
+ _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
public virtual string GetFileNameWithoutExtension(FileSystemMetadata info)
@@ -590,7 +602,7 @@ namespace Emby.Server.Implementations.IO
return GetFiles(path, null, false, recursive);
}
- public virtual IEnumerable GetFiles(string path, IReadOnlyList extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+ public virtual IEnumerable GetFiles(string path, IReadOnlyList? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -607,13 +619,13 @@ namespace Emby.Server.Implementations.IO
{
files = files.Where(i =>
{
- var ext = i.Extension;
- if (ext == null)
+ var ext = i.Extension.AsSpan();
+ if (ext.IsEmpty)
{
return false;
}
- return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
+ return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
});
}
@@ -625,8 +637,7 @@ namespace Emby.Server.Implementations.IO
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);
- return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
- .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
+ return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", enumerationOptions));
}
private IEnumerable ToMetadata(IEnumerable infos)
@@ -644,7 +655,7 @@ namespace Emby.Server.Implementations.IO
return GetFilePaths(path, null, false, recursive);
}
- public virtual IEnumerable GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+ public virtual IEnumerable GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -661,13 +672,13 @@ namespace Emby.Server.Implementations.IO
{
files = files.Where(i =>
{
- var ext = Path.GetExtension(i);
- if (ext == null)
+ var ext = Path.GetExtension(i.AsSpan());
+ if (ext.IsEmpty)
{
return false;
}
- return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
+ return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
});
}
@@ -689,20 +700,5 @@ namespace Emby.Server.Implementations.IO
AttributesToSkip = 0
};
}
-
- private static void RunProcess(string path, string args, string workingDirectory)
- {
- using (var process = Process.Start(new ProcessStartInfo
- {
- Arguments = args,
- FileName = path,
- CreateNoWindow = true,
- WorkingDirectory = workingDirectory,
- WindowStyle = ProcessWindowStyle.Normal
- }))
- {
- process.WaitForExit();
- }
- }
}
}
diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
index e6696b8c4..76c58d5dc 100644
--- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
+++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.IO
public string Extension => ".mblink";
- public string Resolve(string shortcutPath)
+ public string? Resolve(string shortcutPath)
{
if (string.IsNullOrEmpty(shortcutPath))
{
diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs
index c16ebd61b..e4f5f4cf0 100644
--- a/Emby.Server.Implementations/IO/StreamHelper.cs
+++ b/Emby.Server.Implementations/IO/StreamHelper.cs
@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.IO
{
public class StreamHelper : IStreamHelper
{
- public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
+ public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool.Shared.Rent(bufferSize);
try
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index 4bef59543..a430b9e72 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -1,7 +1,5 @@
#pragma warning disable CS1591
-using System;
-
namespace Emby.Server.Implementations
{
public interface IStartupOptions
@@ -9,7 +7,7 @@ namespace Emby.Server.Implementations
///
/// Gets the value of the --ffmpeg command line option.
///
- string FFmpegPath { get; }
+ string? FFmpegPath { get; }
///
/// Gets the value of the --service command line option.
@@ -19,21 +17,21 @@ namespace Emby.Server.Implementations
///
/// Gets the value of the --package-name command line option.
///
- string PackageName { get; }
+ string? PackageName { get; }
///
/// Gets the value of the --restartpath command line option.
///
- string RestartPath { get; }
+ string? RestartPath { get; }
///
/// Gets the value of the --restartargs command line option.
///
- string RestartArgs { get; }
+ string? RestartArgs { get; }
///
/// Gets the value of the --published-server-url command line option.
///
- Uri PublishedServerUrl { get; }
+ string? PublishedServerUrl { get; }
}
}
diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
index afa4ec7b1..e96b64595 100644
--- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
@@ -2,20 +2,12 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using Emby.Server.Implementations.Images;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 5f7e51858..833fb0b7a 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -191,7 +193,7 @@ namespace Emby.Server.Implementations.Images
InputPaths = GetStripCollageImagePaths(primaryItem, items).ToArray()
};
- if (options.InputPaths.Length == 0)
+ if (options.InputPaths.Count == 0)
{
return null;
}
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index 161b4c452..ff5f26ce0 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
index 462eb03a8..900b3fd9c 100644
--- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
@@ -1,10 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using Emby.Server.Implementations.Images;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index 0224ab32a..859017f86 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 381788231..6da431c68 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
index 0ce1b91e8..b8f0f0d65 100644
--- a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -29,9 +31,7 @@ namespace Emby.Server.Implementations.Images
{
var subItem = i.Item2;
- var episode = subItem as Episode;
-
- if (episode != null)
+ if (subItem is Episode episode)
{
var series = episode.Series;
if (series != null && series.HasImage(ImageType.Primary))
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index 3380e29d4..c7d113963 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Don't resolve these into audio files
- if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
+ if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename))
{
return true;
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index 236453e80..6c65b5899 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index e30a67593..5384c04b3 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Linq;
using DotNet.Globbing;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index db27862ce..028673529 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -48,6 +50,7 @@ using MediaBrowser.Providers.MediaInfo;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
using VideoResolver = Emby.Naming.Video.VideoResolver;
@@ -175,10 +178,7 @@ namespace Emby.Server.Implementations.Library
{
lock (_rootFolderSyncLock)
{
- if (_rootFolder == null)
- {
- _rootFolder = CreateRootFolder();
- }
+ _rootFolder ??= CreateRootFolder();
}
}
@@ -196,33 +196,33 @@ namespace Emby.Server.Implementations.Library
/// Gets or sets the postscan tasks.
///
/// The postscan tasks.
- private ILibraryPostScanTask[] PostscanTasks { get; set; }
+ private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty();
///
/// Gets or sets the intro providers.
///
/// The intro providers.
- private IIntroProvider[] IntroProviders { get; set; }
+ private IIntroProvider[] IntroProviders { get; set; } = Array.Empty();
///
/// Gets or sets the list of entity resolution ignore rules.
///
/// The entity resolution ignore rules.
- private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty();
///
/// Gets or sets the list of currently registered entity resolvers.
///
/// The entity resolvers enumerable.
- private IItemResolver[] EntityResolvers { get; set; }
+ private IItemResolver[] EntityResolvers { get; set; } = Array.Empty();
- private IMultiItemResolver[] MultiItemResolvers { get; set; }
+ private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty();
///
/// Gets or sets the comparers.
///
/// The comparers.
- private IBaseItemComparer[] Comparers { get; set; }
+ private IBaseItemComparer[] Comparers { get; set; } = Array.Empty();
public bool IsScanRunning { get; private set; }
@@ -558,7 +558,6 @@ namespace Emby.Server.Implementations.Library
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
{
Parent = parent,
- Path = fullPath,
FileInfo = fileInfo,
CollectionType = collectionType,
LibraryOptions = libraryOptions
@@ -684,7 +683,7 @@ namespace Emby.Server.Implementations.Library
foreach (var item in items)
{
- ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService);
+ ResolverHelper.SetInitialItemValues(item, parent, this, directoryService);
}
items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions));
@@ -697,25 +696,32 @@ namespace Emby.Server.Implementations.Library
}
private IEnumerable ResolveFileList(
- IEnumerable fileList,
+ IReadOnlyList fileList,
IDirectoryService directoryService,
Folder parent,
string collectionType,
IItemResolver[] resolvers,
LibraryOptions libraryOptions)
{
- return fileList.Select(f =>
+ // Given that fileList is a list we can save enumerator allocations by indexing
+ for (var i = 0; i < fileList.Count; i++)
{
+ var file = fileList[i];
+ BaseItem result = null;
try
{
- return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions);
+ result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error resolving path {path}", f.FullName);
- return null;
+ _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
}
- }).Where(i => i != null);
+
+ if (result != null)
+ {
+ yield return result;
+ }
+ }
}
///
@@ -1066,17 +1072,17 @@ namespace Emby.Server.Implementations.Library
// Start by just validating the children of the root, but go no further
await RootFolder.ValidateChildren(
new SimpleProgress(),
- cancellationToken,
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
- recursive: false).ConfigureAwait(false);
+ recursive: false,
+ cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().ValidateChildren(
new SimpleProgress(),
- cancellationToken,
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
- recursive: false).ConfigureAwait(false);
+ recursive: false,
+ cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
foreach (var folder in GetUserRootFolder().Children.OfType())
@@ -1096,7 +1102,7 @@ namespace Emby.Server.Implementations.Library
innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
// Validate the entire media library
- await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);
+ await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
progress.Report(96);
@@ -1163,7 +1169,7 @@ namespace Emby.Server.Implementations.Library
progress.Report(percent * 100);
}
- _itemRepository.UpdateInheritedValues(cancellationToken);
+ _itemRepository.UpdateInheritedValues();
progress.Report(100);
}
@@ -1240,11 +1246,20 @@ namespace Emby.Server.Implementations.Library
return info;
}
- private string GetCollectionType(string path)
+ private CollectionTypeOptions? GetCollectionType(string path)
{
- return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false)
- .Select(Path.GetFileNameWithoutExtension)
- .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
+ foreach (var file in files)
+ {
+ // TODO: @bond use a ReadOnlySpan here when Enum.TryParse supports it
+ // https://github.com/dotnet/runtime/issues/20008
+ if (Enum.TryParse(Path.GetFileNameWithoutExtension(file), true, out var res))
+ {
+ return res;
+ }
+ }
+
+ return null;
}
///
@@ -1905,12 +1920,17 @@ namespace Emby.Server.Implementations.Library
}
catch (ArgumentException)
{
- _logger.LogWarning("Cannot get image index for {0}", img.Path);
+ _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
continue;
}
- catch (InvalidOperationException)
+ catch (Exception ex) when (ex is InvalidOperationException || ex is IOException)
{
- _logger.LogWarning("Cannot fetch image from {0}", img.Path);
+ _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
+ continue;
+ }
+ catch (HttpRequestException ex)
+ {
+ _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", img.Path, ex.StatusCode);
continue;
}
}
@@ -1923,7 +1943,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path);
+ _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
image.Width = 0;
image.Height = 0;
continue;
@@ -1935,7 +1955,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path);
+ _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
image.BlurHash = string.Empty;
}
@@ -1945,7 +1965,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Cannot update DateModified for {0}", image.Path);
+ _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
}
}
@@ -2063,7 +2083,7 @@ namespace Emby.Server.Implementations.Library
return new List();
}
- return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType().ToList());
+ return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType());
}
public List GetCollectionFolders(BaseItem item, List allUserRootChildren)
@@ -2088,10 +2108,10 @@ namespace Emby.Server.Implementations.Library
return GetCollectionFoldersInternal(item, allUserRootChildren);
}
- private static List GetCollectionFoldersInternal(BaseItem item, List allUserRootChildren)
+ private static List GetCollectionFoldersInternal(BaseItem item, IEnumerable allUserRootChildren)
{
return allUserRootChildren
- .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase))
+ .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path.AsSpan(), StringComparison.OrdinalIgnoreCase))
.ToList();
}
@@ -2099,9 +2119,9 @@ namespace Emby.Server.Implementations.Library
{
if (!(item is CollectionFolder collectionFolder))
{
+ // List.Find is more performant than FirstOrDefault due to enumerator allocation
collectionFolder = GetCollectionFolders(item)
- .OfType()
- .FirstOrDefault();
+ .Find(folder => folder is CollectionFolder) as CollectionFolder;
}
return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
@@ -2487,8 +2507,7 @@ namespace Emby.Server.Implementations.Library
///
public bool IsVideoFile(string path)
{
- var resolver = new VideoResolver(GetNamingOptions());
- return resolver.IsVideoFile(path);
+ return VideoResolver.IsVideoFile(path, GetNamingOptions());
}
///
@@ -2503,7 +2522,7 @@ namespace Emby.Server.Implementations.Library
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
{
var series = episode.Series;
- bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase);
+ bool? isAbsoluteNaming = series != null && string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase);
if (!isAbsoluteNaming.Value)
{
// In other words, no filter applied
@@ -2515,9 +2534,23 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
// TODO nullable - what are we trying to do there with empty episodeInfo?
- var episodeInfo = episode.IsFileProtocol
- ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path)
- : new Naming.TV.EpisodeInfo(episode.Path);
+ EpisodeInfo episodeInfo = null;
+ if (episode.IsFileProtocol)
+ {
+ episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
+ // Resolve from parent folder if it's not the Season folder
+ if (episodeInfo == null && episode.Parent.GetType() == typeof(Folder))
+ {
+ episodeInfo = resolver.Resolve(episode.Parent.Path, true, null, null, isAbsoluteNaming);
+ if (episodeInfo != null)
+ {
+ // add the container
+ episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
+ }
+ }
+ }
+
+ episodeInfo ??= new EpisodeInfo(episode.Path);
try
{
@@ -2652,6 +2685,7 @@ namespace Emby.Server.Implementations.Library
return changed;
}
+ ///
public NamingOptions GetNamingOptions()
{
if (_namingOptions == null)
@@ -2665,13 +2699,12 @@ namespace Emby.Server.Implementations.Library
public ItemLookupInfo ParseName(string name)
{
- var resolver = new VideoResolver(GetNamingOptions());
-
- var result = resolver.CleanDateTime(name);
+ var namingOptions = GetNamingOptions();
+ var result = VideoResolver.CleanDateTime(name, namingOptions);
return new ItemLookupInfo
{
- Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name,
+ Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name,
Year = result.Year
};
}
@@ -2685,9 +2718,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
- var videoListResolver = new VideoListResolver(namingOptions);
-
- var videos = videoListResolver.Resolve(fileSystemChildren);
+ var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
@@ -2731,9 +2762,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
- var videoListResolver = new VideoListResolver(namingOptions);
-
- var videos = videoListResolver.Resolve(fileSystemChildren);
+ var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
@@ -2767,6 +2796,7 @@ namespace Emby.Server.Implementations.Library
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
{
+ string newPath;
if (ownerItem != null)
{
var libraryOptions = GetLibraryOptions(ownerItem);
@@ -2774,15 +2804,9 @@ namespace Emby.Server.Implementations.Library
{
foreach (var pathInfo in libraryOptions.PathInfos)
{
- if (string.IsNullOrWhiteSpace(pathInfo.Path) || string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
+ if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath))
{
- continue;
- }
-
- var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
- if (substitutionResult.Item2)
- {
- return substitutionResult.Item1;
+ return newPath;
}
}
}
@@ -2791,24 +2815,16 @@ namespace Emby.Server.Implementations.Library
var metadataPath = _configurationManager.Configuration.MetadataPath;
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
- if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
+ if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath))
{
- var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
- if (metadataSubstitutionResult.Item2)
- {
- return metadataSubstitutionResult.Item1;
- }
+ return newPath;
}
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
{
- if (!string.IsNullOrWhiteSpace(map.From))
+ if (path.TryReplaceSubPath(map.From, map.To, out newPath))
{
- var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
- if (substitutionResult.Item2)
- {
- return substitutionResult.Item1;
- }
+ return newPath;
}
}
@@ -2817,47 +2833,12 @@ namespace Emby.Server.Implementations.Library
public string SubstitutePath(string path, string from, string to)
{
- return SubstitutePathInternal(path, from, to).Item1;
- }
-
- private Tuple SubstitutePathInternal(string path, string from, string to)
- {
- if (string.IsNullOrWhiteSpace(path))
+ if (path.TryReplaceSubPath(from, to, out var newPath))
{
- throw new ArgumentNullException(nameof(path));
+ return newPath;
}
- if (string.IsNullOrWhiteSpace(from))
- {
- throw new ArgumentNullException(nameof(from));
- }
-
- if (string.IsNullOrWhiteSpace(to))
- {
- throw new ArgumentNullException(nameof(to));
- }
-
- from = from.Trim();
- to = to.Trim();
-
- var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
- var changed = false;
-
- if (!string.Equals(newPath, path, StringComparison.Ordinal))
- {
- if (to.IndexOf('/', StringComparison.Ordinal) != -1)
- {
- newPath = newPath.Replace('\\', '/');
- }
- else
- {
- newPath = newPath.Replace('/', '\\');
- }
-
- changed = true;
- }
-
- return new Tuple(newPath, changed);
+ return path;
}
private void SetExtraTypeFromFilename(Video item)
@@ -2914,6 +2895,12 @@ namespace Emby.Server.Implementations.Library
}
public void UpdatePeople(BaseItem item, List people)
+ {
+ UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task UpdatePeopleAsync(BaseItem item, List people, CancellationToken cancellationToken)
{
if (!item.SupportsPeople)
{
@@ -2921,6 +2908,8 @@ namespace Emby.Server.Implementations.Library
}
_itemRepository.UpdatePeople(item.Id, people);
+
+ await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
}
public async Task ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
@@ -2956,7 +2945,7 @@ namespace Emby.Server.Implementations.Library
throw new InvalidOperationException();
}
- public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
+ public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
{
if (string.IsNullOrWhiteSpace(name))
{
@@ -2990,9 +2979,9 @@ namespace Emby.Server.Implementations.Library
{
Directory.CreateDirectory(virtualFolderPath);
- if (!string.IsNullOrEmpty(collectionType))
+ if (collectionType != null)
{
- var path = Path.Combine(virtualFolderPath, collectionType + ".collection");
+ var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
File.WriteAllBytes(path, Array.Empty());
}
@@ -3024,6 +3013,58 @@ namespace Emby.Server.Implementations.Library
}
}
+ private async Task SavePeopleMetadataAsync(IEnumerable people, CancellationToken cancellationToken)
+ {
+ var personsToSave = new List();
+
+ foreach (var person in people)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var itemUpdateType = ItemUpdateType.MetadataDownload;
+ var saveEntity = false;
+ var personEntity = GetPerson(person.Name);
+
+ // if PresentationUniqueKey is empty it's likely a new item.
+ if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
+ {
+ personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
+ saveEntity = true;
+ }
+
+ foreach (var id in person.ProviderIds)
+ {
+ if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase))
+ {
+ personEntity.SetProviderId(id.Key, id.Value);
+ saveEntity = true;
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
+ {
+ personEntity.SetImage(
+ new ItemImageInfo
+ {
+ Path = person.ImageUrl,
+ Type = ImageType.Primary
+ },
+ 0);
+
+ saveEntity = true;
+ itemUpdateType = ItemUpdateType.ImageUpdate;
+ }
+
+ if (saveEntity)
+ {
+ personsToSave.Add(personEntity);
+ await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
+ }
+ }
+
+ CreateItems(personsToSave, null, CancellationToken.None);
+ }
+
private void StartScanInBackground()
{
Task.Run(() =>
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 2070df31e..4ef7923db 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -25,7 +27,7 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths)
{
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index c63eb7017..b812b6b61 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -46,7 +48,7 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary _openStreams = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private IMediaSourceProvider[] _providers;
@@ -199,10 +201,15 @@ namespace Emby.Server.Implementations.Library
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
+ else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ {
+ source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
+ source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
+ }
}
}
- return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList();
+ return SortMediaSources(list);
}
public MediaProtocol GetPathProtocol(string path)
@@ -345,7 +352,7 @@ namespace Emby.Server.Implementations.Library
private string[] NormalizeLanguage(string language)
{
- if (language == null)
+ if (string.IsNullOrEmpty(language))
{
return Array.Empty();
}
@@ -374,8 +381,7 @@ namespace Emby.Server.Implementations.Library
}
}
- var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
- ? Array.Empty() : NormalizeLanguage(user.SubtitleLanguagePreference);
+ var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
var audioLangage = defaultAudioIndex == null
@@ -404,9 +410,7 @@ namespace Emby.Server.Implementations.Library
}
}
- var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
- ? Array.Empty()
- : NormalizeLanguage(user.AudioLanguagePreference);
+ var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
}
@@ -436,7 +440,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private static IEnumerable SortMediaSources(IEnumerable sources)
+ private static List SortMediaSources(IEnumerable sources)
{
return sources.OrderBy(i =>
{
@@ -451,8 +455,9 @@ namespace Emby.Server.Implementations.Library
{
var stream = i.VideoStream;
- return stream == null || stream.Width == null ? 0 : stream.Width.Value;
+ return stream?.Width ?? 0;
})
+ .Where(i => i.Type != MediaSourceType.Placeholder)
.ToList();
}
@@ -584,18 +589,9 @@ namespace Emby.Server.Implementations.Library
public Task GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
- var info = _openStreams.Values.FirstOrDefault(i =>
- {
- var liveStream = i as ILiveStream;
- if (liveStream != null)
- {
- return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
- }
+ var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
- return false;
- });
-
- return Task.FromResult(info as IDirectStreamProvider);
+ return Task.FromResult(info.Value as IDirectStreamProvider);
}
public async Task OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 28fa06239..b833122ea 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 658c53f28..06300adeb 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -100,8 +102,7 @@ namespace Emby.Server.Implementations.Library
public List GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions)
{
- var genre = item as MusicGenre;
- if (genre != null)
+ if (item is MusicGenre genre)
{
return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
}
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 06ff3e611..86b8039fa 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -1,7 +1,6 @@
-#nullable enable
-
using System;
-using System.Text.RegularExpressions;
+using System.Diagnostics.CodeAnalysis;
+using MediaBrowser.Common.Providers;
namespace Emby.Server.Implementations.Library
{
@@ -41,11 +40,78 @@ namespace Emby.Server.Implementations.Library
// for imdbid we also accept pattern matching
if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase))
{
- var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase);
- return m.Success ? m.Value : null;
+ var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId);
+ return match ? imdbId.ToString() : null;
}
return null;
}
+
+ ///
+ /// Replaces a sub path with another sub path and normalizes the final path.
+ ///
+ /// The original path.
+ /// The original sub path.
+ /// The new sub path.
+ /// The result of the sub path replacement
+ /// The path after replacing the sub path.
+ /// , or is empty.
+ public static bool TryReplaceSubPath(
+ [NotNullWhen(true)] this string? path,
+ [NotNullWhen(true)] string? subPath,
+ [NotNullWhen(true)] string? newSubPath,
+ [NotNullWhen(true)] out string? newPath)
+ {
+ newPath = null;
+
+ if (string.IsNullOrEmpty(path)
+ || string.IsNullOrEmpty(subPath)
+ || string.IsNullOrEmpty(newSubPath)
+ || subPath.Length > path.Length)
+ {
+ return false;
+ }
+
+ char oldDirectorySeparatorChar;
+ char newDirectorySeparatorChar;
+ // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
+ // The reasoning behind this is that a forward slash likely means it's a Linux path and
+ // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
+ if (newSubPath.Contains('/', StringComparison.Ordinal))
+ {
+ oldDirectorySeparatorChar = '\\';
+ newDirectorySeparatorChar = '/';
+ }
+ else
+ {
+ oldDirectorySeparatorChar = '/';
+ newDirectorySeparatorChar = '\\';
+ }
+
+ path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
+ subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
+
+ // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
+ // when the sub path matches a similar but in-complete subpath
+ var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar;
+ if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (path.Length > subPath.Length
+ && !oldSubPathEndsWithSeparator
+ && path[subPath.Length] != newDirectorySeparatorChar)
+ {
+ return false;
+ }
+
+ var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar);
+ // Ensure that the path with the old subpath removed starts with a leading dir separator
+ int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length;
+ newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx));
+
+ return true;
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index 4e4cac75b..ac75e5d3a 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -18,11 +18,10 @@ namespace Emby.Server.Implementations.Library
///
/// The item.
/// The parent.
- /// The file system.
/// The library manager.
/// The directory service.
- /// Item must have a path
- public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService)
+ /// Item must have a path.
+ public static void SetInitialItemValues(BaseItem item, Folder? parent, ILibraryManager libraryManager, IDirectoryService directoryService)
{
// This version of the below method has no ItemResolveArgs, so we have to require the path already being set
if (string.IsNullOrEmpty(item.Path))
@@ -43,9 +42,14 @@ namespace Emby.Server.Implementations.Library
// Make sure DateCreated and DateModified have values
var fileInfo = directoryService.GetFile(item.Path);
- SetDateCreated(item, fileSystem, fileInfo);
+ if (fileInfo == null)
+ {
+ throw new FileNotFoundException("Can't find item path.", item.Path);
+ }
- EnsureName(item, item.Path, fileInfo);
+ SetDateCreated(item, fileInfo);
+
+ EnsureName(item, fileInfo);
}
///
@@ -72,9 +76,9 @@ namespace Emby.Server.Implementations.Library
item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
// Make sure the item has a name
- EnsureName(item, item.Path, args.FileInfo);
+ EnsureName(item, args.FileInfo);
- item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
+ item.IsLocked = item.Path.Contains("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) ||
item.GetParents().Any(i => i.IsLocked);
// Make sure DateCreated and DateModified have values
@@ -84,28 +88,15 @@ namespace Emby.Server.Implementations.Library
///
/// Ensures the name.
///
- private static void EnsureName(BaseItem item, string fullPath, FileSystemMetadata fileInfo)
+ private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo)
{
// If the subclass didn't supply a name, add it here
- if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(fullPath))
+ if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path))
{
- var fileName = fileInfo == null ? Path.GetFileName(fullPath) : fileInfo.Name;
-
- item.Name = GetDisplayName(fileName, fileInfo != null && fileInfo.IsDirectory);
+ item.Name = fileInfo.IsDirectory ? fileInfo.Name : Path.GetFileNameWithoutExtension(fileInfo.Name);
}
}
- ///
- /// Gets the display name.
- ///
- /// The path.
- /// if set to true [is directory].
- /// System.String.
- private static string GetDisplayName(string path, bool isDirectory)
- {
- return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path);
- }
-
///
/// Ensures DateCreated and DateModified have values.
///
@@ -114,21 +105,6 @@ namespace Emby.Server.Implementations.Library
/// The args.
private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args)
{
- if (fileSystem == null)
- {
- throw new ArgumentNullException(nameof(fileSystem));
- }
-
- if (item == null)
- {
- throw new ArgumentNullException(nameof(item));
- }
-
- if (args == null)
- {
- throw new ArgumentNullException(nameof(args));
- }
-
// See if a different path came out of the resolver than what went in
if (!fileSystem.AreEqual(args.Path, item.Path))
{
@@ -136,7 +112,7 @@ namespace Emby.Server.Implementations.Library
if (childData != null)
{
- SetDateCreated(item, fileSystem, childData);
+ SetDateCreated(item, childData);
}
else
{
@@ -144,17 +120,17 @@ namespace Emby.Server.Implementations.Library
if (fileData.Exists)
{
- SetDateCreated(item, fileSystem, fileData);
+ SetDateCreated(item, fileData);
}
}
}
else
{
- SetDateCreated(item, fileSystem, args.FileInfo);
+ SetDateCreated(item, args.FileInfo);
}
}
- private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info)
+ private static void SetDateCreated(BaseItem item, FileSystemMetadata? info)
{
var config = BaseItem.ConfigurationManager.GetMetadataConfiguration();
@@ -163,7 +139,7 @@ namespace Emby.Server.Implementations.Library
// directoryService.getFile may return null
if (info != null)
{
- var dateCreated = fileSystem.GetCreationTimeUtc(info);
+ var dateCreated = info.CreationTimeUtc;
if (dateCreated.Equals(DateTime.MinValue))
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 90b6a8a7d..e893d6335 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -201,6 +203,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue;
}
+ if (resolvedItem.Files.Count == 0)
+ {
+ continue;
+ }
+
var firstMedia = resolvedItem.Files[0];
var libraryItem = new T
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index bf32381eb..8e1eccb10 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 60f82806f..3d2ae95d2 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Linq;
using System.Threading.Tasks;
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 2f5e46038..cdb492022 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -30,7 +32,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
///
/// The args.
/// `0.
- protected override T Resolve(ItemResolveArgs args)
+ public override T Resolve(ItemResolveArgs args)
{
return ResolveVideo(args, false);
}
@@ -42,14 +44,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// The args.
/// if set to true [parse name].
/// ``0.
- protected TVideoType ResolveVideo(ItemResolveArgs args, bool parseName)
+ protected virtual TVideoType ResolveVideo(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new()
{
- var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+ var namingOptions = LibraryManager.GetNamingOptions();
// If the path is a file check for a matching extensions
- var parser = new VideoResolver(namingOptions);
-
if (args.IsDirectory)
{
TVideoType video = null;
@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
{
- videoInfo = parser.ResolveDirectory(args.Path);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
{
- videoInfo = parser.ResolveDirectory(args.Path);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
else if (IsDvdFile(filename))
{
- videoInfo = parser.ResolveDirectory(args.Path);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
else
{
- var videoInfo = parser.Resolve(args.Path, false, false);
+ var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
if (videoInfo == null)
{
@@ -165,13 +165,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected void SetVideoType(Video video, VideoFileInfo videoInfo)
{
- var extension = Path.GetExtension(video.Path);
- video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ?
- VideoType.Iso :
- VideoType.VideoFile;
+ var extension = Path.GetExtension(video.Path.AsSpan());
+ video.VideoType = extension.Equals(".iso", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".img", StringComparison.OrdinalIgnoreCase)
+ ? VideoType.Iso
+ : VideoType.VideoFile;
- video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+ video.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
video.IsPlaceHolder = videoInfo.IsStub;
if (videoInfo.IsStub)
@@ -193,11 +193,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (video.VideoType == VideoType.Iso)
{
- if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1)
+ if (video.Path.Contains("dvd", StringComparison.OrdinalIgnoreCase))
{
video.IsoType = IsoType.Dvd;
}
- else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1)
+ else if (video.Path.Contains("bluray", StringComparison.OrdinalIgnoreCase))
{
video.IsoType = IsoType.BluRay;
}
@@ -250,10 +250,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected void Set3DFormat(Video video)
{
- var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
-
- var resolver = new Format3DParser(namingOptions);
- var result = resolver.Parse(video.Path);
+ var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
Set3DFormat(video, result.Is3D, result.Format3D);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 86242d137..68076730b 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -13,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
- protected override Book Resolve(ItemResolveArgs args)
+ public override Book Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
index 7dbce7a6e..7aaee017d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
index 9ca76095b..fa45ccf84 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
@@ -11,6 +13,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
public abstract class ItemResolver : IItemResolver
where T : BaseItem, new()
{
+ ///
+ /// Gets the priority.
+ ///
+ /// The priority.
+ public virtual ResolverPriority Priority => ResolverPriority.First;
+
///
/// Resolves the specified args.
///
@@ -21,12 +29,6 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
- ///
- /// Gets the priority.
- ///
- /// The priority.
- public virtual ResolverPriority Priority => ResolverPriority.First;
-
///
/// Sets initial values on the newly resolved item.
///
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index 295e9e120..69d71d0d9 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.IO;
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 8ef7172de..97f96f746 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -1,9 +1,12 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -69,159 +72,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result;
}
- private MultiItemResolverResult ResolveMultipleInternal(
- Folder parent,
- List files,
- string collectionType,
- IDirectoryService directoryService)
- {
- if (IsInvalid(parent, collectionType))
- {
- return null;
- }
-
- if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
- {
- return ResolveVideos(parent, files, directoryService, true, collectionType, false);
- }
-
- if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
- {
- return ResolveVideos
public class SeriesResolver : FolderResolver
{
- private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
///
/// Initializes a new instance of the class.
///
- /// The file system.
/// The logger.
/// The library manager.
- public SeriesResolver(IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager)
+ public SeriesResolver(ILogger logger, ILibraryManager libraryManager)
{
- _fileSystem = fileSystem;
_logger = logger;
_libraryManager = libraryManager;
}
@@ -59,15 +58,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
- // if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
- //{
- // return new Series
- // {
- // Path = args.Path,
- // Name = Path.GetFileName(args.Path)
- // };
- //}
-
var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path);
if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
@@ -100,7 +90,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
- if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, false))
+ if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false))
{
return new Series
{
@@ -117,8 +107,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
public static bool IsSeriesFolder(
string path,
IEnumerable fileSystemChildren,
- IDirectoryService directoryService,
- IFileSystem fileSystem,
ILogger logger,
ILibraryManager libraryManager,
bool isTvContentType)
@@ -127,7 +115,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (child.IsDirectory)
{
- if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager))
+ if (IsSeasonFolder(child.FullName, isTvContentType))
{
logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true;
@@ -160,32 +148,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return false;
}
- ///
- /// Determines whether [is place holder] [the specified path].
- ///
- /// The path.
- /// true if [is place holder] [the specified path]; otherwise, false.
- /// path
- private static bool IsVideoPlaceHolder(string path)
- {
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- var extension = Path.GetExtension(path);
-
- return string.Equals(extension, ".disc", StringComparison.OrdinalIgnoreCase);
- }
-
///
/// Determines whether [is season folder] [the specified path].
///
/// The path.
/// if set to true [is tv content type].
- /// The library manager.
/// true if [is season folder] [the specified path]; otherwise, false.
- private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager)
+ private static bool IsSeasonFolder(string path, bool isTvContentType)
{
var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
index 62268fce9..9599faea4 100644
--- a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 94602582b..26e615fa0 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -12,7 +14,6 @@ using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;
-using Microsoft.Extensions.Logging;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index d16275b19..8aa605a90 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -13,8 +15,8 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using Book = MediaBrowser.Controller.Entities.Book;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
+using Book = MediaBrowser.Controller.Entities.Book;
namespace Emby.Server.Implementations.Library
{
@@ -220,7 +222,7 @@ namespace Emby.Server.Implementations.Library
var hasRuntime = runtimeTicks > 0;
// If a position has been reported, and if we know the duration
- if (positionTicks > 0 && hasRuntime && !(item is AudioBook))
+ if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
{
var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
@@ -239,7 +241,7 @@ namespace Emby.Server.Implementations.Library
{
// Enforce MinResumeDuration
var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
- if (durationSeconds < _config.Configuration.MinResumeDurationSeconds && !(item is Book))
+ if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
{
positionTicks = 0;
data.Played = playedToCompletion = true;
@@ -248,15 +250,15 @@ namespace Emby.Server.Implementations.Library
}
else if (positionTicks > 0 && hasRuntime && item is AudioBook)
{
- var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes;
- var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
+ var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
+ var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
- if (minIn > _config.Configuration.MinAudiobookResume)
+ if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
{
// ignore progress during the beginning
positionTicks = 0;
}
- else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
+ else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
{
// mark as completed close to the end
positionTicks = 0;
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index b6b7ea949..e2da672a3 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -1,8 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index 341194f23..3fcadf5b1 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -45,7 +47,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
- using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
{
onStarted();
@@ -70,7 +73,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
- await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
onStarted();
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 2c0de661d..797063120 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -17,7 +19,6 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -802,22 +803,22 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public ActiveRecordingInfo GetActiveRecordingInfo(string path)
{
- if (string.IsNullOrWhiteSpace(path))
+ if (string.IsNullOrWhiteSpace(path) || _activeRecordings.IsEmpty)
{
return null;
}
- foreach (var recording in _activeRecordings.Values)
+ foreach (var (_, recordingInfo) in _activeRecordings)
{
- if (string.Equals(recording.Path, path, StringComparison.Ordinal) && !recording.CancellationTokenSource.IsCancellationRequested)
+ if (string.Equals(recordingInfo.Path, path, StringComparison.Ordinal) && !recordingInfo.CancellationTokenSource.IsCancellationRequested)
{
- var timer = recording.Timer;
+ var timer = recordingInfo.Timer;
if (timer.Status != RecordingStatus.InProgress)
{
return null;
}
- return recording;
+ return recordingInfo;
}
}
@@ -1622,9 +1623,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
return _activeRecordings
- .Values
- .ToList()
- .Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
+ .Any(i => string.Equals(i.Value.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Value.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
}
private IRecorder GetRecorder(MediaSourceInfo mediaSource)
@@ -1856,7 +1855,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
@@ -1920,7 +1920,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
@@ -2238,14 +2239,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var enabledTimersForSeries = new List();
foreach (var timer in allTimers)
{
- var existingTimer = _timerProvider.GetTimer(timer.Id);
-
- if (existingTimer == null)
- {
- existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId)
+ var existingTimer = _timerProvider.GetTimer(timer.Id)
+ ?? (string.IsNullOrWhiteSpace(timer.ProgramId)
? null
- : _timerProvider.GetTimerByProgramId(timer.ProgramId);
- }
+ : _timerProvider.GetTimerByProgramId(timer.ProgramId));
if (existingTimer == null)
{
@@ -2604,7 +2601,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Locations = new string[] { customPath },
Name = "Recorded Movies",
- CollectionType = CollectionType.Movies
+ CollectionType = CollectionTypeOptions.Movies
};
}
@@ -2615,7 +2612,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Locations = new string[] { customPath },
Name = "Recorded Shows",
- CollectionType = CollectionType.TvShows
+ CollectionType = CollectionTypeOptions.TvShows
};
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 78a82118e..26e4ef1ed 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,6 +12,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -28,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IServerApplicationPaths _appPaths;
private readonly TaskCompletionSource _taskCompletionSource = new TaskCompletionSource();
private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private bool _hasExited;
private Stream _logFileStream;
private string _targetPath;
@@ -307,13 +310,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
using (var reader = new StreamReader(source))
{
- while (!reader.EndOfStream)
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- var line = await reader.ReadLineAsync().ConfigureAwait(false);
-
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
- await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+ await target.WriteAsync(bytes.AsMemory()).ConfigureAwait(false);
await target.FlushAsync().ConfigureAwait(false);
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
index 463d0ed0a..0ec52a959 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
@@ -6,58 +6,46 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
-
internal class EpgChannelData
{
+
+ private readonly Dictionary _channelsById;
+
+ private readonly Dictionary _channelsByNumber;
+
+ private readonly Dictionary _channelsByName;
+
public EpgChannelData(IEnumerable channels)
{
- ChannelsById = new Dictionary(StringComparer.OrdinalIgnoreCase);
- ChannelsByNumber = new Dictionary(StringComparer.OrdinalIgnoreCase);
- ChannelsByName = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _channelsById = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _channelsByNumber = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _channelsByName = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var channel in channels)
{
- ChannelsById[channel.Id] = channel;
+ _channelsById[channel.Id] = channel;
if (!string.IsNullOrEmpty(channel.Number))
{
- ChannelsByNumber[channel.Number] = channel;
+ _channelsByNumber[channel.Number] = channel;
}
var normalizedName = NormalizeName(channel.Name ?? string.Empty);
if (!string.IsNullOrWhiteSpace(normalizedName))
{
- ChannelsByName[normalizedName] = channel;
+ _channelsByName[normalizedName] = channel;
}
}
}
- private Dictionary ChannelsById { get; set; }
+ public ChannelInfo? GetChannelById(string id)
+ => _channelsById.GetValueOrDefault(id);
- private Dictionary ChannelsByNumber { get; set; }
+ public ChannelInfo? GetChannelByNumber(string number)
+ => _channelsByNumber.GetValueOrDefault(number);
- private Dictionary ChannelsByName { get; set; }
-
- public ChannelInfo GetChannelById(string id)
- {
- ChannelsById.TryGetValue(id, out var result);
-
- return result;
- }
-
- public ChannelInfo GetChannelByNumber(string number)
- {
- ChannelsByNumber.TryGetValue(number, out var result);
-
- return result;
- }
-
- public ChannelInfo GetChannelByName(string name)
- {
- ChannelsByName.TryGetValue(name, out var result);
-
- return result;
- }
+ public ChannelInfo? GetChannelByName(string name)
+ => _channelsByName.GetValueOrDefault(name);
public static string NormalizeName(string value)
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index 57424f043..bdab8c3e4 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -1,12 +1,12 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text;
using System.Text.Json;
-using System.Threading.Tasks;
using MediaBrowser.Common.Json;
using Microsoft.Extensions.Logging;
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
private readonly string _dataPath;
private readonly object _fileDataLock = new object();
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private T[] _items;
public ItemDataProvider(
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
index 142c59542..32245f899 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
- internal class RecordingHelper
+ internal static class RecordingHelper
{
public static DateTime GetStartTime(TimerInfo timer)
{
@@ -70,17 +70,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private static string GetDateString(DateTime date)
{
- date = date.ToLocalTime();
-
- return string.Format(
- CultureInfo.InvariantCulture,
- "{0}_{1}_{2}_{3}_{4}_{5}",
- date.Year.ToString("0000", CultureInfo.InvariantCulture),
- date.Month.ToString("00", CultureInfo.InvariantCulture),
- date.Day.ToString("00", CultureInfo.InvariantCulture),
- date.Hour.ToString("00", CultureInfo.InvariantCulture),
- date.Minute.ToString("00", CultureInfo.InvariantCulture),
- date.Second.ToString("00", CultureInfo.InvariantCulture));
+ return date.ToLocalTime().ToString("yyyy_MM_dd_HH_mm_ss", CultureInfo.InvariantCulture);
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
index da707fec6..b1259de23 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
@@ -2,7 +2,6 @@
using System;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 1efa90e25..6c52a9a73 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 7567ea312..00d02873c 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -35,8 +37,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ICryptoProvider _cryptoProvider;
private readonly ConcurrentDictionary _tokens = new ConcurrentDictionary();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private DateTime _lastErrorResponse;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
public SchedulesDirect(
ILogger logger,
@@ -111,7 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var dailySchedules = await JsonSerializer.DeserializeAsync>(responseStream, _jsonOptions).ConfigureAwait(false);
+ var dailySchedules = await JsonSerializer.DeserializeAsync>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
@@ -122,12 +124,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var programDetails = await JsonSerializer.DeserializeAsync>(innerResponseStream, _jsonOptions).ConfigureAwait(false);
+ var programDetails = await JsonSerializer.DeserializeAsync>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
var programDict = programDetails.ToDictionary(p => p.programID, y => y);
- var programIdsWithImages =
- programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
- .ToList();
+ var programIdsWithImages = programDetails
+ .Where(p => p.hasImageArtwork).Select(p => p.programID)
+ .ToList();
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
@@ -182,8 +184,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private static int GetSizeOrder(ScheduleDirect.ImageData image)
{
- if (!string.IsNullOrWhiteSpace(image.height)
- && int.TryParse(image.height, out int value))
+ if (int.TryParse(image.height, out int value))
{
return value;
}
@@ -704,7 +705,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
- var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
}
@@ -776,7 +777,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
_logger.LogInformation("Mapping Stations to Channel");
@@ -788,14 +789,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var channelNumber = GetChannelNumber(channel);
- var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase));
- if (station == null)
- {
- station = new ScheduleDirect.Station
+ var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase))
+ ?? new ScheduleDirect.Station
{
stationID = channel.stationID
};
- }
var channelInfo = new ChannelInfo
{
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 76c875737..ebad4eddf 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -1,10 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Threading;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
index ba916af38..098f193fb 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
@@ -1,21 +1,23 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.LiveTv;
namespace Emby.Server.Implementations.LiveTv
{
+ ///
+ /// implementation for .
+ ///
public class LiveTvConfigurationFactory : IConfigurationFactory
{
+ ///
public IEnumerable GetConfigurations()
{
return new ConfigurationStore[]
{
new ConfigurationStore
{
- ConfigurationType = typeof(LiveTvOptions),
- Key = "livetv"
+ ConfigurationType = typeof(LiveTvOptions),
+ Key = "livetv"
}
};
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 6af49dd45..21e1409ac 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 63a3146aa..d964769b5 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -987,10 +989,7 @@ namespace Emby.Server.Implementations.LiveTv
var externalProgramId = programTuple.Item2;
string externalSeriesId = programTuple.Item3;
- if (timerList == null)
- {
- timerList = (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
- }
+ timerList ??= (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase));
var foundSeriesTimer = false;
@@ -1018,10 +1017,7 @@ namespace Emby.Server.Implementations.LiveTv
continue;
}
- if (seriesTimerList == null)
- {
- seriesTimerList = (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
- }
+ seriesTimerList ??= (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase));
@@ -1974,10 +1970,7 @@ namespace Emby.Server.Implementations.LiveTv
};
}
- if (service == null)
- {
- service = _services[0];
- }
+ service ??= _services[0];
var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
@@ -2273,7 +2266,7 @@ namespace Emby.Server.Implementations.LiveTv
if (dataSourceChanged)
{
- _taskManager.CancelIfRunningAndQueue();
+ _taskManager.CancelIfRunningAndQueue();
}
return info;
@@ -2316,7 +2309,7 @@ namespace Emby.Server.Implementations.LiveTv
_config.SaveConfiguration("livetv", config);
- _taskManager.CancelIfRunningAndQueue();
+ _taskManager.CancelIfRunningAndQueue();
return info;
}
@@ -2328,7 +2321,7 @@ namespace Emby.Server.Implementations.LiveTv
config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
_config.SaveConfiguration("livetv", config);
- _taskManager.CancelIfRunningAndQueue();
+ _taskManager.CancelIfRunningAndQueue();
}
public async Task SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId)
@@ -2362,7 +2355,7 @@ namespace Emby.Server.Implementations.LiveTv
var tunerChannelMappings =
tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList();
- _taskManager.CancelIfRunningAndQueue();
+ _taskManager.CancelIfRunningAndQueue();
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase));
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index 3a738fd5d..ecd28097d 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
similarity index 64%
rename from Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
rename to Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
index 582b64923..15df0dcf1 100644
--- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.LiveTv;
@@ -10,34 +9,55 @@ using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.LiveTv
{
- public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
+ ///
+ /// The "Refresh Guide" scheduled task.
+ ///
+ public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledTask
{
private readonly ILiveTvManager _liveTvManager;
private readonly IConfigurationManager _config;
- public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The live tv manager.
+ /// The configuration manager.
+ public RefreshGuideScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
{
_liveTvManager = liveTvManager;
_config = config;
}
+ ///
public string Name => "Refresh Guide";
+ ///
public string Description => "Downloads channel information from live tv services.";
+ ///
public string Category => "Live TV";
- public Task Execute(System.Threading.CancellationToken cancellationToken, IProgress progress)
+ ///
+ public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
+
+ ///
+ public bool IsEnabled => true;
+
+ ///
+ public bool IsLogged => true;
+
+ ///
+ public string Key => "RefreshGuide";
+
+ ///
+ public Task Execute(CancellationToken cancellationToken, IProgress progress)
{
var manager = (LiveTvManager)_liveTvManager;
return manager.RefreshChannels(progress, cancellationToken);
}
- ///
- /// Creates the triggers that define when the task will run.
- ///
- /// IEnumerable{BaseTaskTrigger}.
+ ///
public IEnumerable GetDefaultTriggers()
{
return new[]
@@ -51,13 +71,5 @@ namespace Emby.Server.Implementations.LiveTv
{
return _config.GetConfiguration("livetv");
}
-
- public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
-
- public bool IsEnabled => true;
-
- public bool IsLogged => true;
-
- public string Key => "RefreshGuide";
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index fbcd4ef37..5941613cf 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -38,6 +40,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public virtual bool IsSupported => true;
protected abstract Task> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+
public abstract string Type { get; }
public async Task> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
index 740cbb66e..0f0453189 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
internal class Channels
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
index 09d77f838..42068cd34 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 5ef83f274..54de841fe 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,10 +10,8 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
-using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
@@ -19,7 +19,6 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -61,7 +60,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_networkManager = networkManager;
_streamHelper = streamHelper;
- _jsonOptions = JsonDefaults.GetOptions();
+ _jsonOptions = JsonDefaults.Options;
}
public string Name => "HD Homerun";
@@ -77,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var lineup = await JsonSerializer.DeserializeAsync>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false) ?? new List();
@@ -185,16 +184,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List();
- while (!sr.EndOfStream)
+ await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
{
- string line = StripXML(sr.ReadLine());
- if (line.Contains("Channel", StringComparison.Ordinal))
+ string stripedLine = StripXML(line);
+ if (stripedLine.Contains("Channel", StringComparison.Ordinal))
{
LiveTvTunerStatus status;
- var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
- var name = line.Substring(0, index - 1);
- var currentChannel = line.Substring(index + 7);
- if (currentChannel != "none")
+ var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+ var name = stripedLine.Substring(0, index - 1);
+ var currentChannel = stripedLine.Substring(index + 7);
+ if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
{
status = LiveTvTunerStatus.LiveTv;
}
@@ -335,11 +334,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new Uri(url).AbsoluteUri.TrimEnd('/');
}
- protected EncodingOptions GetEncodingOptions()
- {
- return Config.GetConfiguration("encoding");
- }
-
private static string GetHdHrIdFromChannelId(string channelId)
{
return channelId.Split('_')[1];
@@ -429,10 +423,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
string audioCodec = channelInfo.AudioCodec;
- if (!videoBitrate.HasValue)
- {
- videoBitrate = isHd ? 15000000 : 2000000;
- }
+ videoBitrate ??= isHd ? 15000000 : 2000000;
int? audioBitrate = isHd ? 448000 : 192000;
@@ -592,7 +583,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Logger,
Config,
_appHost,
- _networkManager,
_streamHelper);
}
@@ -633,7 +623,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Logger,
Config,
_appHost,
- _networkManager,
_streamHelper);
}
@@ -669,7 +658,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_modelCache.Clear();
}
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token;
+ using var timedCancellationToken = new CancellationTokenSource(discoveryDurationMs);
+ using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timedCancellationToken.Token, cancellationToken);
+ cancellationToken = linkedCancellationTokenSource.Token;
var list = new List();
// Create udp broadcast discovery message
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index f09338330..3016eeda2 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -1,7 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Buffers;
+using System.Buffers.Binary;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
@@ -10,6 +13,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common;
using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -120,17 +124,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private static async Task CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
{
- var lockkeyMsg = CreateGetMessage(tuner, "lockkey");
- await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
-
byte[] buffer = ArrayPool.Shared.Rent(8192);
try
{
- int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+ var msgLen = WriteGetMessage(buffer, tuner, "lockkey");
+ await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false);
- ParseReturnMessage(buffer, receivedBytes, out string returnVal);
+ int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
- return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase);
+ return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
}
finally
{
@@ -166,24 +168,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_activeTuner = i;
var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
- var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
- await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
- int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+ var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null);
+ await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false);
+ int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out _))
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
continue;
}
foreach (var command in commands.GetCommands())
{
- var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
- await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
- receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+ var channelMsgLen = WriteSetMessage(buffer, i, command.Item1, command.Item2, lockKeyValue);
+ await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
+ receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out _))
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
@@ -191,13 +193,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
- var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
+ var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
- await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
- receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+ await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
+ receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out _))
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
@@ -232,12 +234,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
foreach (var command in commandList)
{
- var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
- await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
- int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+ var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.Item1, command.Item2, _lockkey);
+ await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
+ int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out _))
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
return;
}
@@ -265,17 +267,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var stream = client.GetStream();
- var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue);
- await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false);
-
var buffer = ArrayPool.Shared.Rent(8192);
try
{
- await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
- var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue);
+ var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue);
+ await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false);
+
+ await stream.ReadAsync(buffer).ConfigureAwait(false);
+ var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue);
_lockkey = null;
- await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false);
- await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+ await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false);
+ await stream.ReadAsync(buffer).ConfigureAwait(false);
}
finally
{
@@ -283,249 +285,136 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- private static byte[] CreateGetMessage(int tuner, string name)
+ internal static int WriteGetMessage(Span buffer, int tuner, string name)
{
- var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
- int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length
-
- var message = new byte[messageLength];
-
- int offset = InsertHeaderAndName(byteName, messageLength, message);
-
- bool flipEndian = BitConverter.IsLittleEndian;
-
- // calculate crc and insert at the end of the message
- var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
- if (flipEndian)
- {
- Array.Reverse(crcBytes);
- }
-
- Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
-
- return message;
+ var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
+ int offset = WriteHeaderAndPayload(buffer, byteName);
+ return FinishPacket(buffer, offset);
}
- private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey)
+ internal static int WriteSetMessage(Span buffer, int tuner, string name, string value, uint? lockkey)
{
- var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
- var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value));
+ var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
+ int offset = WriteHeaderAndPayload(buffer, byteName);
+
+ buffer[offset++] = GetSetValue;
+ offset += WriteNullTerminatedString(buffer.Slice(offset), value);
- int messageLength = byteName.Length + byteValue.Length + 12;
if (lockkey.HasValue)
{
- messageLength += 6;
- }
-
- var message = new byte[messageLength];
-
- int offset = InsertHeaderAndName(byteName, messageLength, message);
-
- bool flipEndian = BitConverter.IsLittleEndian;
-
- message[offset++] = GetSetValue;
- message[offset++] = Convert.ToByte(byteValue.Length);
- Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length);
- offset += byteValue.Length;
- if (lockkey.HasValue)
- {
- message[offset++] = GetSetLockkey;
- message[offset++] = 4;
- var lockKeyBytes = BitConverter.GetBytes(lockkey.Value);
- if (flipEndian)
- {
- Array.Reverse(lockKeyBytes);
- }
-
- Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4);
+ buffer[offset++] = GetSetLockkey;
+ buffer[offset++] = 4;
+ BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value);
offset += 4;
}
- // calculate crc and insert at the end of the message
- var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
- if (flipEndian)
- {
- Array.Reverse(crcBytes);
- }
-
- Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
-
- return message;
+ return FinishPacket(buffer, offset);
}
- private static int InsertHeaderAndName(byte[] byteName, int messageLength, byte[] message)
+ internal static int WriteNullTerminatedString(Span buffer, ReadOnlySpan payload)
{
- // check to see if we need to flip endiannes
- bool flipEndian = BitConverter.IsLittleEndian;
- int offset = 0;
+ int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1;
- // create header bytes
- var getSetBytes = BitConverter.GetBytes(GetSetRequest);
- var msgLenBytes = BitConverter.GetBytes((ushort)(messageLength - 8)); // Subtrace 4 bytes for header and 4 bytes for crc
+ // TODO: variable length: this can be 2 bytes if len > 127
+ // Write length in front of value
+ buffer[0] = Convert.ToByte(len);
- if (flipEndian)
- {
- Array.Reverse(getSetBytes);
- Array.Reverse(msgLenBytes);
- }
+ // null-terminate
+ buffer[len++] = 0;
- // insert header bytes into message
- Buffer.BlockCopy(getSetBytes, 0, message, offset, 2);
- offset += 2;
- Buffer.BlockCopy(msgLenBytes, 0, message, offset, 2);
- offset += 2;
+ return len;
+ }
- // insert tag name and length
- message[offset++] = GetSetName;
- message[offset++] = Convert.ToByte(byteName.Length);
+ private static int WriteHeaderAndPayload(Span buffer, ReadOnlySpan payload)
+ {
+ // Packet type
+ BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
- // insert name string
- Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length);
- offset += byteName.Length;
+ // We write the payload length at the end
+ int offset = 4;
+
+ // Tag
+ buffer[offset++] = GetSetName;
+
+ // Payload length + data
+ int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload);
+ offset += strLen;
return offset;
}
- private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal)
+ private static int FinishPacket(Span buffer, int offset)
{
- returnVal = string.Empty;
+ // Payload length
+ BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4));
- if (numBytes < 4)
- {
- return false;
- }
+ // calculate crc and insert at the end of the message
+ var crc = Crc32.Compute(buffer.Slice(0, offset));
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc);
- var flipEndian = BitConverter.IsLittleEndian;
- int offset = 0;
- byte[] msgTypeBytes = new byte[2];
- Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length);
-
- if (flipEndian)
- {
- Array.Reverse(msgTypeBytes);
- }
-
- var msgType = BitConverter.ToUInt16(msgTypeBytes, 0);
- offset += 2;
-
- if (msgType != GetSetReply)
- {
- return false;
- }
-
- byte[] msgLengthBytes = new byte[2];
- Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length);
- if (flipEndian)
- {
- Array.Reverse(msgLengthBytes);
- }
-
- var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0);
- offset += 2;
-
- if (numBytes < msgLength + 8)
- {
- return false;
- }
-
- offset++; // Name Tag
-
- var nameLength = buf[offset++];
-
- // skip the name field to get to value for return
- offset += nameLength;
-
- offset++; // Value Tag
-
- var valueLength = buf[offset++];
-
- returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator
- return true;
+ return offset + 4;
}
- private static class HdHomerunCrc
+ internal static bool VerifyReturnValueOfGetSet(ReadOnlySpan buffer, string expected)
{
- private static uint[] crc_table = {
- 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
- 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
- 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
- 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
- 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
- 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
- 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
- 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
- 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
- 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
- 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
- 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
- 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
- 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
- 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
- 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
- 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
- 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
- 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
- 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
- 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
- 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
- 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
- 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
- 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
- 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
- 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
- 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
- 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
- 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
- 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
- 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
- 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
- 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
- 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
- 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
- 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
- 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
- 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
- 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
- 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
- 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
- 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
- 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
- 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
- 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
- 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
- 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
- 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
- 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
- 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
- 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
- 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
- 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
- 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
- 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
- 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
- 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
- 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
- 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
- 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
- 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
- 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
- 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d };
+ return TryGetReturnValueOfGetSet(buffer, out var value)
+ && string.Equals(Encoding.UTF8.GetString(value), expected, StringComparison.OrdinalIgnoreCase);
+ }
- public static uint GetCrc32(byte[] bytes, int numBytes)
+ internal static bool TryGetReturnValueOfGetSet(ReadOnlySpan buffer, out ReadOnlySpan value)
+ {
+ value = ReadOnlySpan.Empty;
+
+ if (buffer.Length < 8)
{
- var hash = 0xffffffff;
- for (var i = 0; i < numBytes; i++)
- {
- hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff];
- }
-
- var tmp = ~hash & 0xffffffff;
- var b0 = tmp & 0xff;
- var b1 = (tmp >> 8) & 0xff;
- var b2 = (tmp >> 16) & 0xff;
- var b3 = (tmp >> 24) & 0xff;
- return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
+ return false;
}
+
+ uint crc = BinaryPrimitives.ReadUInt32LittleEndian(buffer[^4..]);
+ if (crc != Crc32.Compute(buffer[..^4]))
+ {
+ return false;
+ }
+
+ if (BinaryPrimitives.ReadUInt16BigEndian(buffer) != GetSetReply)
+ {
+ return false;
+ }
+
+ var msgLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2));
+ if (buffer.Length != 2 + 2 + 4 + msgLength)
+ {
+ return false;
+ }
+
+ var offset = 4;
+ if (buffer[offset++] != GetSetName)
+ {
+ return false;
+ }
+
+ var nameLength = buffer[offset++];
+ if (buffer.Length < 4 + 1 + offset + nameLength)
+ {
+ return false;
+ }
+
+ offset += nameLength;
+
+ if (buffer[offset++] != GetSetValue)
+ {
+ return false;
+ }
+
+ var valueLength = buffer[offset++];
+ if (buffer.Length < 4 + offset + valueLength)
+ {
+ return false;
+ }
+
+ // remove null terminator
+ value = buffer.Slice(offset, valueLength - 1);
+ return true;
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index cf653f87d..58e0c7448 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,7 +12,6 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -28,7 +29,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IServerApplicationHost _appHost;
private readonly IHdHomerunChannelCommands _channelCommands;
private readonly int _numTuners;
- private readonly INetworkManager _networkManager;
public HdHomerunUdpStream(
MediaSourceInfo mediaSource,
@@ -40,12 +40,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
ILogger logger,
IConfigurationManager configurationManager,
IServerApplicationHost appHost,
- INetworkManager networkManager,
IStreamHelper streamHelper)
: base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
{
_appHost = appHost;
- _networkManager = networkManager;
OriginalStreamId = originalStreamId;
_channelCommands = channelCommands;
_numTuners = numTuners;
@@ -92,7 +90,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
try
{
- await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false);
+ await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort, openCancellationToken).ConfigureAwait(false);
localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address;
tcpClient.Close();
}
@@ -126,7 +124,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using (udpClient)
using (hdHomerunManager)
{
- if (!(ex is OperationCanceledException))
+ if (ex is not OperationCanceledException)
{
Logger.LogError(ex, "Error opening live stream:");
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index 78e62ff0a..96a678c1d 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -150,7 +152,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
{
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token).Token;
+ using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token);
+ cancellationToken = linkedCancellationTokenSource.Token;
// use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039
var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 4b170b2e4..8fa6f5ad6 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -27,6 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
+ private static readonly string[] _disallowedSharedStreamExtensions =
+ {
+ ".mkv",
+ ".mp4",
+ ".m3u8",
+ ".mpd"
+ };
+
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly INetworkManager _networkManager;
@@ -65,7 +75,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
- return await new M3uParser(Logger, _httpClientFactory, _appHost)
+ return await new M3uParser(Logger, _httpClientFactory)
.Parse(info, channelIdPrefix, cancellationToken)
.ConfigureAwait(false);
}
@@ -86,14 +96,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
- private static readonly string[] _disallowedSharedStreamExtensions =
- {
- ".mkv",
- ".mp4",
- ".m3u8",
- ".mpd"
- };
-
protected override async Task GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = info.TunerCount;
@@ -128,7 +130,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info)
{
- using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
+ using (var stream = await new M3uParser(Logger, _httpClientFactory).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
{
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index c82b67b41..40a162890 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -1,10 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
@@ -20,15 +21,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3uParser
{
+ private const string ExtInfPrefix = "#EXTINF:";
+
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerApplicationHost _appHost;
- public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost)
+ public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
- _appHost = appHost;
}
public async Task> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
@@ -36,16 +37,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
// Read the file and display it line by line.
using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
{
- return GetChannels(reader, channelIdPrefix, info.Id);
- }
- }
-
- public List ParseString(string text, string channelIdPrefix, string tunerHostId)
- {
- // Read the file and display it line by line.
- using (var reader = new StringReader(text))
- {
- return GetChannels(reader, channelIdPrefix, tunerHostId);
+ return await GetChannelsAsync(reader, channelIdPrefix, info.Id).ConfigureAwait(false);
}
}
@@ -69,45 +61,42 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return File.OpenRead(info.Url);
}
- private const string ExtInfPrefix = "#EXTINF:";
-
- private List GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId)
+ private async Task> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
{
var channels = new List();
- string line;
string extInf = string.Empty;
- while ((line = reader.ReadLine()) != null)
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- line = line.Trim();
- if (string.IsNullOrWhiteSpace(line))
+ var trimmedLine = line.Trim();
+ if (string.IsNullOrWhiteSpace(trimmedLine))
{
continue;
}
- if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
+ if (trimmedLine.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
{
continue;
}
- if (line.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
+ if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
{
- extInf = line.Substring(ExtInfPrefix.Length).Trim();
+ extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim();
_logger.LogInformation("Found m3u channel: {0}", extInf);
}
- else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith('#'))
+ else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
- var channel = GetChannelnfo(extInf, tunerHostId, line);
+ var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
if (string.IsNullOrWhiteSpace(channel.Id))
{
- channel.Id = channelIdPrefix + line.GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
else
{
channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
- channel.Path = line;
+ channel.Path = trimmedLine;
channels.Add(channel);
extInf = string.Empty;
}
@@ -133,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
channel.ImageUrl = value;
}
+ if (attributes.TryGetValue("group-title", out string groupTitle))
+ {
+ channel.ChannelGroup = groupTitle;
+ }
+
channel.Name = GetChannelName(extInf, attributes);
channel.Number = GetChannelNumber(extInf, attributes, mediaUrl);
@@ -155,7 +149,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (channelIdValues.Count > 0)
{
- channel.Id = string.Join("_", channelIdValues);
+ channel.Id = string.Join('_', channelIdValues);
}
return channel;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index f7507e6ba..f572151b8 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -89,8 +91,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var taskCompletionSource = new TaskCompletionSource();
- var now = DateTime.UtcNow;
-
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
// OpenedMediaSource.Protocol = MediaProtocol.File;
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!taskCompletionSource.Task.Result)
{
Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
- throw new EndOfStreamException(String.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
+ throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
}
}
@@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
EnableStreamSharing = false;
await DeleteTempFiles(new List { TempFilePath }).ConfigureAwait(false);
- });
+ }, CancellationToken.None);
}
private void Resolve(TaskCompletionSource openTaskCompletionSource)
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 977a1c2d7..4f21c66bc 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -112,5 +112,10 @@
"TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
"TaskRefreshLibrary": "Skandeer Media Versameling",
"TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
- "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde"
+ "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde",
+ "Undefined": "Ongedefineerd",
+ "Forced": "Geforseer",
+ "Default": "Oorspronklik",
+ "TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.",
+ "TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 4b898e6fe..3d6e159b1 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -113,5 +113,10 @@
"TaskRefreshPeopleDescription": "تحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
"TaskRefreshPeople": "إعادة تحميل الأشخاص",
"TaskCleanLogsDescription": "حذف السجلات الأقدم من {0} يوم.",
- "TaskCleanLogs": "حذف دليل السجل"
+ "TaskCleanLogs": "حذف دليل السجل",
+ "TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الموضوع.",
+ "TaskCleanActivityLog": "حذف سجل الأنشطة",
+ "Default": "الإعدادات الافتراضية",
+ "Undefined": "غير معرف",
+ "Forced": "ملحقة"
}
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 9db3b50d9..bc25531d3 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -39,7 +39,7 @@
"MixedContent": "Смесено съдържание",
"Movies": "Филми",
"Music": "Музика",
- "MusicVideos": "Музикални клипове",
+ "MusicVideos": "Музикални видеа",
"NameInstallFailed": "{0} не можа да се инсталира",
"NameSeasonNumber": "Сезон {0}",
"NameSeasonUnknown": "Неразпознат сезон",
@@ -62,7 +62,7 @@
"NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно",
"Photos": "Снимки",
"Playlists": "Списъци",
- "Plugin": "Приставка",
+ "Plugin": "Добавка",
"PluginInstalledWithName": "{0} е инсталиранa",
"PluginUninstalledWithName": "{0} е деинсталиранa",
"PluginUpdatedWithName": "{0} е обновенa",
@@ -116,5 +116,7 @@
"TasksMaintenanceCategory": "Поддръжка",
"Undefined": "Неопределено",
"Forced": "Принудително",
- "Default": "По подразбиране"
+ "Default": "По подразбиране",
+ "TaskCleanActivityLogDescription": "Изтрива записите в дневника с активност по стари от конфигурираната възраст.",
+ "TaskCleanActivityLog": "Изчисти дневника с активност"
}
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index a23037af8..c3fbe2408 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -1,7 +1,7 @@
{
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
- "Collections": "কলেক্শন",
+ "Collections": "সংগ্রহ",
"ChapterNameValue": "অধ্যায় {0}",
"Channels": "চ্যানেল",
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
@@ -115,7 +115,7 @@
"TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।",
"Undefined": "অসঙ্গায়িত",
"Forced": "জোরকরে",
- "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন",
+ "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
"TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
"Default": "প্রাথমিক"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 775267183..ff14c1929 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -39,7 +39,7 @@
"MixedContent": "Smíšený obsah",
"Movies": "Filmy",
"Music": "Hudba",
- "MusicVideos": "Hudební klipy",
+ "MusicVideos": "Hudební videa",
"NameInstallFailed": "Instalace {0} selhala",
"NameSeasonNumber": "Sezóna {0}",
"NameSeasonUnknown": "Neznámá sezóna",
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 4ee4eb989..3453507d9 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -1,5 +1,5 @@
{
- "Albums": "Albums",
+ "Albums": "Albummer",
"AppDeviceValues": "App: {0}, Enhed: {1}",
"Application": "Applikation",
"Artists": "Kunstnere",
@@ -39,7 +39,7 @@
"MixedContent": "Blandet indhold",
"Movies": "Film",
"Music": "Musik",
- "MusicVideos": "Musikvideoer",
+ "MusicVideos": "Musik videoer",
"NameInstallFailed": "{0} installationen mislykkedes",
"NameSeasonNumber": "Sæson {0}",
"NameSeasonUnknown": "Ukendt Sæson",
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index f8f595faa..65964f6d9 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -117,5 +117,7 @@
"TaskRefreshChannels": "Refresh Channels",
"TaskRefreshChannelsDescription": "Refreshes internet channel information.",
"TaskDownloadMissingSubtitles": "Download missing subtitles",
- "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration."
+ "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.",
+ "TaskOptimizeDatabase": "Optimize database",
+ "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance."
}
diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json
index 3ff7eddae..ca615cc8c 100644
--- a/Emby.Server.Implementations/Localization/Core/eo.json
+++ b/Emby.Server.Implementations/Localization/Core/eo.json
@@ -22,5 +22,26 @@
"Artists": "Artistoj",
"Application": "Aplikaĵo",
"AppDeviceValues": "Aplikaĵo: {0}, Aparato: {1}",
- "Albums": "Albumoj"
+ "Albums": "Albumoj",
+ "TasksLibraryCategory": "Libraro",
+ "VersionNumber": "Versio {0}",
+ "UserDownloadingItemWithValues": "{0} elŝutas {1}",
+ "UserCreatedWithName": "Uzanto {0} kreiĝis",
+ "User": "Uzanto",
+ "System": "Sistemo",
+ "Songs": "Kantoj",
+ "ScheduledTaskStartedWithName": "{0} komencis",
+ "ScheduledTaskFailedWithName": "{0} malsukcesis",
+ "PluginUninstalledWithName": "{0} malinstaliĝis",
+ "PluginInstalledWithName": "{0} instaliĝis",
+ "Plugin": "Kromprogramo",
+ "Playlists": "Ludlistoj",
+ "Photos": "Fotoj",
+ "NotificationOptionPluginUninstalled": "Kromprogramo malinstaliĝis",
+ "NotificationOptionNewLibraryContent": "Nova enhavo aldoniĝis",
+ "NotificationOptionPluginInstalled": "Kromprogramo instaliĝis",
+ "MusicVideos": "Muzikvideoj",
+ "LabelIpAddressValue": "IP-adreso: {0}",
+ "Genres": "Ĝenroj",
+ "DeviceOfflineWithName": "{0} malkonektis"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index 05181116d..5d7ed243f 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -117,5 +117,6 @@
"TaskCleanActivityLogDescription": "Elimina entradas del registro de actividad que sean más antiguas al periodo establecido.",
"TaskCleanActivityLog": "Limpiar registro de actividades",
"Undefined": "Sin definir",
- "Forced": "Forzado"
+ "Forced": "Forzado",
+ "Default": "Predeterminado"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index 16fde325f..91939843f 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Limpiar registro de actividad",
"Undefined": "Indefinido",
"Forced": "Forzado",
- "Default": "Predeterminado"
+ "Default": "Predeterminado",
+ "TaskOptimizeDatabase": "Optimizar la base de datos",
+ "TaskOptimizeDatabaseDescription": "Compacta y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index e9e4f61b8..8ab657e5b 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -34,7 +34,7 @@
"Latest": "جدیدترینها",
"MessageApplicationUpdated": "سرور Jellyfin بروزرسانی شد",
"MessageApplicationUpdatedTo": "سرور Jellyfin به نسخه {0} بروزرسانی شد",
- "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
+ "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
"MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد",
"MixedContent": "محتوای مخلوط",
"Movies": "فیلمها",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index fd6148e78..633968d26 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -1,5 +1,5 @@
{
- "HeaderLiveTV": "Live TV",
+ "HeaderLiveTV": "Suora TV",
"NewVersionIsAvailable": "Uusi versio Jellyfin-palvelimesta on ladattavissa.",
"NameSeasonUnknown": "Tuntematon kausi",
"NameSeasonNumber": "Kausi {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 1e195378f..ce1493be8 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -15,7 +15,7 @@
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes",
+ "HeaderAlbumArtists": "Artistes de l'album",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
@@ -39,7 +39,7 @@
"MixedContent": "Contenu mixte",
"Movies": "Films",
"Music": "Musique",
- "MusicVideos": "Vidéos musicales",
+ "MusicVideos": "Clips musicaux",
"NameInstallFailed": "{0} échec de l'installation",
"NameSeasonNumber": "Saison {0}",
"NameSeasonUnknown": "Saison Inconnue",
@@ -99,7 +99,7 @@
"TaskRefreshChannels": "Rafraîchir les chaines",
"TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.",
"TaskCleanTranscode": "Nettoyer les dossier des transcodages",
- "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour être mises à jour automatiquement.",
+ "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurées pour être mises à jour automatiquement.",
"TaskUpdatePlugins": "Mettre à jour les extensions",
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.",
"TaskRefreshPeople": "Rafraîchir les acteurs",
@@ -107,7 +107,7 @@
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
"TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
- "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
+ "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
"TaskCleanCache": "Vider le répertoire cache",
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index faee2519a..0398e1c9e 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -7,5 +7,86 @@
"Books": "Libros",
"AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
"Artists": "Artistas",
- "Application": "Aplicativo"
+ "Application": "Aplicativo",
+ "NotificationOptionServerRestartRequired": "Necesario un reinicio do servidor",
+ "NotificationOptionPluginUpdateInstalled": "Actualización do Plugin instalada",
+ "NotificationOptionPluginUninstalled": "Plugin desinstalado",
+ "NotificationOptionPluginInstalled": "Plugin instalado",
+ "NotificationOptionPluginError": "Fallo do Plugin",
+ "NotificationOptionNewLibraryContent": "Novo contido engadido",
+ "NotificationOptionInstallationFailed": "Fallo na instalación",
+ "NotificationOptionCameraImageUploaded": "Imaxe da cámara subida",
+ "NotificationOptionAudioPlaybackStopped": "Reproducción de audio parada",
+ "NotificationOptionAudioPlayback": "Reproducción de audio comezada",
+ "NotificationOptionApplicationUpdateInstalled": "Actualización da aplicación instalada",
+ "NotificationOptionApplicationUpdateAvailable": "Actualización da aplicación dispoñible",
+ "NewVersionIsAvailable": "Unha nova versión do Servidor Jellyfin está dispoñible para descarga.",
+ "NameSeasonUnknown": "Tempada descoñecida",
+ "NameSeasonNumber": "Tempada {0}",
+ "NameInstallFailed": "{0} instalación fallida",
+ "MusicVideos": "Vídeos Musicais",
+ "Music": "Música",
+ "Movies": "Películas",
+ "MixedContent": "Contido Mixto",
+ "MessageServerConfigurationUpdated": "A configuración do servidor foi actualizada",
+ "MessageNamedServerConfigurationUpdatedWithValue": "A sección de configuración {0} do servidor foi actualizada",
+ "MessageApplicationUpdatedTo": "O servidor Jellyfin foi actualizado a {0}",
+ "MessageApplicationUpdated": "O servidor Jellyfin foi actualizado",
+ "Latest": "Último",
+ "LabelRunningTimeValue": "Tempo de execución: {0}",
+ "LabelIpAddressValue": "Enderezo IP: {0}",
+ "ItemRemovedWithName": "{0} foi eliminado da biblioteca",
+ "ItemAddedWithName": "{0} foi engadido a biblioteca",
+ "Inherit": "Herdar",
+ "HomeVideos": "Videos caseiros",
+ "HeaderRecordingGroups": "Grupos de Grabación",
+ "HeaderNextUp": "De seguido",
+ "HeaderLiveTV": "TV en directo",
+ "HeaderFavoriteSongs": "Cancións Favoritas",
+ "HeaderFavoriteShows": "Series de TV Favoritas",
+ "HeaderFavoriteEpisodes": "Episodios Favoritos",
+ "HeaderFavoriteArtists": "Artistas Favoritos",
+ "HeaderFavoriteAlbums": "Álbunes Favoritos",
+ "HeaderContinueWatching": "Seguir mirando",
+ "HeaderAlbumArtists": "Artistas de Album",
+ "Genres": "Xéneros",
+ "Forced": "Forzado",
+ "Folders": "Cartafoles",
+ "Favorites": "Favoritos",
+ "FailedLoginAttemptWithUserName": "Intento de incio de sesión fallido {0}",
+ "DeviceOnlineWithName": "{0} conectouse",
+ "DeviceOfflineWithName": "{0} desconectouse",
+ "Default": "Por defecto",
+ "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
+ "TaskCleanLogs": "Limpar Carpeta de Rexistros",
+ "TaskCleanActivityLog": "Limpar Rexistro de Actividade",
+ "TasksChannelsCategory": "Canáis de Internet",
+ "TaskUpdatePlugins": "Actualizar Plugins",
+ "User": "Usuario",
+ "Undefined": "Sen definir",
+ "TvShows": "Programas de TV",
+ "System": "Sistema",
+ "Sync": "Sincronizar",
+ "SubtitleDownloadFailureFromForItem": "Fallou a descarga de subtítulos para {1} dende {0}",
+ "StartupEmbyServerIsLoading": "O Servidor Jellyfin está cargando. Por favor, reinténteo en breve.",
+ "Songs": "Cancións",
+ "Shows": "Programas",
+ "ServerNameNeedsToBeRestarted": "{0} precisa ser reiniciado",
+ "ScheduledTaskStartedWithName": "{0} comezou",
+ "ScheduledTaskFailedWithName": "{0} fallou",
+ "ProviderValue": "Provedor: {0}",
+ "PluginUpdatedWithName": "{0} foi actualizado",
+ "PluginUninstalledWithName": "{0} foi desinstalado",
+ "PluginInstalledWithName": "{0} foi instalado",
+ "Playlists": "Listas de reproducción",
+ "Photos": "Fotos",
+ "UserLockedOutWithName": "O usuario {0} foi bloqueado",
+ "UserDownloadingItemWithValues": "{0} está a ser transferido {1}",
+ "UserDeletedWithName": "O usuario {0} foi borrado",
+ "UserCreatedWithName": "O usuario {0} foi creado",
+ "Plugin": "Plugin",
+ "NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo parada",
+ "NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada",
+ "NotificationOptionUserLockedOut": "Usuario bloqueado",
+ "NotificationOptionTaskFailed": "Falla na tarefa axendada"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index ef3697b15..82dc601bc 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -51,5 +51,14 @@
"Latest": "सबसे नया",
"LabelIpAddressValue": "आई पी एड्रेस: {0}",
"ItemRemovedWithName": "{0} लाइब्रेरी में से निकाल दिया है",
- "HomeVideos": "होम वीडियोस"
+ "HomeVideos": "होम वीडियोस",
+ "NotificationOptionVideoPlayback": "वीडियो प्लेबैक शुरू हुआ",
+ "NotificationOptionUserLockedOut": "उपयोगकर्ता लॉक हो गया",
+ "NotificationOptionTaskFailed": "निर्धारित कार्य विफलता",
+ "NotificationOptionServerRestartRequired": "सर्वर पुनरारंभ आवश्यक है",
+ "NotificationOptionPluginUpdateInstalled": "प्लगइन अद्यतन स्थापित",
+ "NotificationOptionNewLibraryContent": "नई सामग्री जोड़ी गई",
+ "LabelRunningTimeValue": "चलने का समय: {0}",
+ "ItemAddedWithName": "{0} को लाइब्रेरी में जोड़ा गया",
+ "Inherit": "इनहेरिट"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index e5707e78c..85848fed6 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -39,7 +39,7 @@
"MixedContent": "Vegyes tartalom",
"Movies": "Filmek",
"Music": "Zene",
- "MusicVideos": "Zenei videók",
+ "MusicVideos": "Zenei videóklippek",
"NameInstallFailed": "{0} sikertelen telepítés",
"NameSeasonNumber": "{0}. évad",
"NameSeasonUnknown": "Ismeretlen évad",
@@ -49,7 +49,7 @@
"NotificationOptionAudioPlayback": "Audió lejátszás elkezdve",
"NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva",
"NotificationOptionCameraImageUploaded": "Kamera kép feltöltve",
- "NotificationOptionInstallationFailed": "Telepítési hiba",
+ "NotificationOptionInstallationFailed": "Telepítés sikertelen",
"NotificationOptionNewLibraryContent": "Új tartalom hozzáadva",
"NotificationOptionPluginError": "Bővítmény hiba",
"NotificationOptionPluginInstalled": "Bővítmény telepítve",
@@ -74,7 +74,7 @@
"Songs": "Dalok",
"StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}",
+ "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}",
"Sync": "Szinkronizál",
"System": "Rendszer",
"TvShows": "TV műsorok",
@@ -82,12 +82,12 @@
"UserCreatedWithName": "{0} felhasználó létrehozva",
"UserDeletedWithName": "{0} felhasználó törölve",
"UserDownloadingItemWithValues": "{0} letölti {1}",
- "UserLockedOutWithName": "{0} felhasználó zárolva van",
- "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}",
+ "UserLockedOutWithName": "{0} felhasználó zárolva van",
+ "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}",
"UserOnlineFromDevice": "{0} online innen: {1}",
"UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}",
"UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}",
- "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}",
+ "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}",
"UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}",
"ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz",
"ValueSpecialEpisodeName": "Special - {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 105ef7be9..ba3513870 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -1,7 +1,7 @@
{
"Albums": "Album",
"AuthenticationSucceededWithUserName": "{0} berhasil diautentikasi",
- "AppDeviceValues": "Aplikasi : {0}, Alat : {1}",
+ "AppDeviceValues": "Aplikasi : {0}, Perangkat : {1}",
"LabelRunningTimeValue": "Waktu berjalan: {0}",
"MessageApplicationUpdatedTo": "Jellyfin Server sudah diperbarui ke {0}",
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 0f769eaad..b262a8b42 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -25,7 +25,7 @@
"Channels": "Stöðvar",
"CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}",
"Books": "Bækur",
- "AuthenticationSucceededWithUserName": "{0} náði að auðkennast",
+ "AuthenticationSucceededWithUserName": "{0} auðkenning tókst",
"Artists": "Listamaður",
"Application": "Forrit",
"AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
@@ -106,5 +106,6 @@
"TasksChannelsCategory": "Netrásir",
"TasksApplicationCategory": "Forrit",
"TasksLibraryCategory": "Miðlasafn",
- "TasksMaintenanceCategory": "Viðhald"
+ "TasksMaintenanceCategory": "Viðhald",
+ "Default": "Sjálfgefið"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 110f8043d..bd06f0a25 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -62,7 +62,7 @@
"NotificationOptionVideoPlaybackStopped": "La riproduzione video è stata interrotta",
"Photos": "Foto",
"Playlists": "Playlist",
- "Plugin": "Plug-in",
+ "Plugin": "Plugin",
"PluginInstalledWithName": "{0} è stato Installato",
"PluginUninstalledWithName": "{0} è stato disinstallato",
"PluginUpdatedWithName": "{0} è stato aggiornato",
@@ -87,7 +87,7 @@
"UserOnlineFromDevice": "{0} è online su {1}",
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
- "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
+ "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di \"{1}\" su {2}",
"UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1} su {2}",
"ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale",
"ValueSpecialEpisodeName": "Speciale - {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index a321e35d0..4eee36989 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -5,23 +5,23 @@
"Artists": "Oryndauşylar",
"AuthenticationSucceededWithUserName": "{0} tüpnūsqalyq rastaluy sättı aiaqtaldy",
"Books": "Kıtaptar",
- "CameraImageUploadedFrom": "{0} kamerasynan jaŋa suret jüktep salyndy",
+ "CameraImageUploadedFrom": "{0} kamerasynan jaña suret jüktep salyndy",
"Channels": "Arnalar",
"ChapterNameValue": "{0}-sahna",
"Collections": "Jiyntyqtar",
"DeviceOfflineWithName": "{0} ajyratylğan",
"DeviceOnlineWithName": "{0} qosylğan",
"FailedLoginAttemptWithUserName": "{0} tarapynan kıru äreketı sätsız aiaqtaldy",
- "Favorites": "Taŋdaulylar",
+ "Favorites": "Tañdaulylar",
"Folders": "Qaltalar",
"Genres": "Janrlar",
"HeaderAlbumArtists": "Älbom oryndauşylary",
"HeaderContinueWatching": "Qaraudy jalğastyru",
- "HeaderFavoriteAlbums": "Taŋdauly älbomdar",
- "HeaderFavoriteArtists": "Taŋdauly oryndauşylar",
- "HeaderFavoriteEpisodes": "Taŋdauly telebölımder",
- "HeaderFavoriteShows": "Taŋdauly körsetımder",
- "HeaderFavoriteSongs": "Taŋdauly äuender",
+ "HeaderFavoriteAlbums": "Tañdauly älbomdar",
+ "HeaderFavoriteArtists": "Tañdauly oryndauşylar",
+ "HeaderFavoriteEpisodes": "Tañdauly telebölımder",
+ "HeaderFavoriteShows": "Tañdauly körsetımder",
+ "HeaderFavoriteSongs": "Tañdauly äuender",
"HeaderLiveTV": "Efir",
"HeaderNextUp": "Kezektı",
"HeaderRecordingGroups": "Jazba toptary",
@@ -31,11 +31,11 @@
"ItemRemovedWithName": "{0} tasyğyşhanadan alastaldy",
"LabelIpAddressValue": "IP-mekenjaiy: {0}",
"LabelRunningTimeValue": "Oinatu uaqyty: {0}",
- "Latest": "Eŋ keiıngı",
- "MessageApplicationUpdated": "Jellyfin Serverı jaŋartyldy",
- "MessageApplicationUpdatedTo": "Jellyfin Serverı {0} nūsqasyna jaŋartyldy",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server teŋşelımderınıŋ {0} bölımı jaŋartyldy",
- "MessageServerConfigurationUpdated": "Server teŋşelımderı jaŋartyldy",
+ "Latest": "Eñ keiıngı",
+ "MessageApplicationUpdated": "Jellyfin Serverı jañartyldy",
+ "MessageApplicationUpdatedTo": "Jellyfin Serverı {0} nūsqasyna jañartyldy",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Server teñşelımderınıñ {0} bölımı jañartyldy",
+ "MessageServerConfigurationUpdated": "Server teñşelımderı jañartyldy",
"MixedContent": "Aralas mazmūn",
"Movies": "Filmder",
"Music": "Muzyka",
@@ -43,18 +43,18 @@
"NameInstallFailed": "{0} ornatyluy sätsız",
"NameSeasonNumber": "{0}-mausym",
"NameSeasonUnknown": "Belgısız mausym",
- "NewVersionIsAvailable": "Jaŋa Jellyfin Server nūsqasy jüktep aluğa qoljetımdı.",
- "NotificationOptionApplicationUpdateAvailable": "Qoldanba jaŋartuy qoljetımdı",
- "NotificationOptionApplicationUpdateInstalled": "Qoldanba jaŋartuy ornatyldy",
+ "NewVersionIsAvailable": "Jaña Jellyfin Server nūsqasy jüktep aluğa qoljetımdı.",
+ "NotificationOptionApplicationUpdateAvailable": "Qoldanba jañartuy qoljetımdı",
+ "NotificationOptionApplicationUpdateInstalled": "Qoldanba jañartuy ornatyldy",
"NotificationOptionAudioPlayback": "Dybys oinatuy bastaldy",
"NotificationOptionAudioPlaybackStopped": "Dybys oinatuy toqtatyldy",
"NotificationOptionCameraImageUploaded": "Kameradan fotosuret jüktep salynğan",
"NotificationOptionInstallationFailed": "Ornatu sätsızdıgı",
- "NotificationOptionNewLibraryContent": "Jaŋa mazmūn üstelıngen",
+ "NotificationOptionNewLibraryContent": "Jaña mazmūn üstelıngen",
"NotificationOptionPluginError": "Plagin sätsızdıgı",
"NotificationOptionPluginInstalled": "Plagin ornatyldy",
"NotificationOptionPluginUninstalled": "Plagin ornatuy boldyrylmady",
- "NotificationOptionPluginUpdateInstalled": "Plagin jaŋartuy ornatyldy",
+ "NotificationOptionPluginUpdateInstalled": "Plagin jañartuy ornatyldy",
"NotificationOptionServerRestartRequired": "Serverdı qaita ıske qosu qajet",
"NotificationOptionTaskFailed": "Josparlağan tapsyrma sätsızdıgı",
"NotificationOptionUserLockedOut": "Paidalanuşy qūrsauly",
@@ -65,14 +65,14 @@
"Plugin": "Plagin",
"PluginInstalledWithName": "{0} ornatyldy",
"PluginUninstalledWithName": "{0} joiyldy",
- "PluginUpdatedWithName": "{0} jaŋartyldy",
+ "PluginUpdatedWithName": "{0} jañartyldy",
"ProviderValue": "Jetkızuşı: {0}",
"ScheduledTaskFailedWithName": "{0} sätsız",
"ScheduledTaskStartedWithName": "{0} ıske qosyldy",
"ServerNameNeedsToBeRestarted": "{0} qaita ıske qosu qajet",
"Shows": "Körsetımder",
"Songs": "Äuender",
- "StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalaŋyz.",
+ "StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalañyz.",
"SubtitleDownloadFailureForItem": "Субтитрлер {0} үшін жүктеліп алынуы сәтсіз",
"SubtitleDownloadFailureFromForItem": "{1} üşın subtitrlerdı {0} közınen jüktep alu sätsız",
"Sync": "Ündestıru",
@@ -86,7 +86,7 @@
"UserOfflineFromDevice": "{0} — {1} tarapynan ajyratyldy",
"UserOnlineFromDevice": "{0} — {1} tarapynan qosyldy",
"UserPasswordChangedWithName": "Paidalanuşy {0} üşın paröl özgertıldı",
- "UserPolicyUpdatedWithName": "Paidalanuşy {0} üşın saiasattary jaŋartyldy",
+ "UserPolicyUpdatedWithName": "Paidalanuşy {0} üşın saiasattary jañartyldy",
"UserStartedPlayingItemWithValues": "{0} — {2} tarapynan {1} oinatuda",
"UserStoppedPlayingItemWithValues": "{0} — {2} tarapynan {1} oinatuyn toqtatty",
"ValueHasBeenAddedToLibrary": "{0} tasyğyşhanağa üstelındı",
@@ -94,10 +94,10 @@
"VersionNumber": "Nūsqasy {0}",
"Default": "Ädepkı",
"TaskDownloadMissingSubtitles": "Joq subtitrlerdı jüktep alu",
- "TaskRefreshChannels": "Arnalardy jaŋğyrtu",
+ "TaskRefreshChannels": "Arnalardy jañğyrtu",
"TaskCleanTranscode": "Qaita kodtau katalogyn tazalau",
- "TaskUpdatePlugins": "Plaginderdı jaŋartu",
- "TaskRefreshPeople": "Adamdardy jaŋğyrtu",
+ "TaskUpdatePlugins": "Plaginderdı jañartu",
+ "TaskRefreshPeople": "Adamdardy jañğyrtu",
"TaskCleanLogs": "Jūrnal katalogyn tazalau",
"TaskRefreshLibrary": "Tasyğyşhanany skanerleu",
"TaskRefreshChapterImages": "Sahna suretterın şyğaryp alu",
@@ -109,14 +109,14 @@
"TasksMaintenanceCategory": "Qyzmet körsetu",
"Undefined": "Anyqtalmağan",
"Forced": "Mäjbürlı",
- "TaskDownloadMissingSubtitlesDescription": "Metaderekter teŋşelımderı negızınde joq subtitrlerdı Internetten ızdeidı.",
- "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jaŋğyrtady.",
+ "TaskDownloadMissingSubtitlesDescription": "Metaderekter teñşelımderı negızınde joq subtitrlerdı İnternetten ızdeidı.",
+ "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jañğyrtady.",
"TaskCleanTranscodeDescription": "Bіr künnen asqan qaita kodtau faildaryn joiady.",
- "TaskUpdatePluginsDescription": "Avtomatty türde jaŋartuğa teŋşelgen plaginder üşın jaŋartulardy jüktep alady jäne ornatady.",
- "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jaŋartady.",
+ "TaskUpdatePluginsDescription": "Avtomatty türde jañartuğa teñşelgen plaginder üşın jañartulardy jüktep alady jäne ornatady.",
+ "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jañartady.",
"TaskCleanLogsDescription": "{0} künnen asqan jūrnal faildaryn joiady.",
- "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaŋa faildardy skanerleidі jäne metaderekterdı jaŋğyrtady.",
- "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşіn nobailar jasaidy.",
+ "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaña faildardy skanerleidі jäne metaderekterdı jañğyrtady.",
+ "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.",
"TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.",
- "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teŋşelgen jasynan asqan jazbalary joiady."
+ "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teñşelgen jasynan asqan jazbalary joiady."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index d4cb592ef..f3a131d40 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -113,5 +113,10 @@
"TasksChannelsCategory": "Internetiniai Kanalai",
"TasksApplicationCategory": "Programa",
"TasksLibraryCategory": "Mediateka",
- "TasksMaintenanceCategory": "Priežiūra"
+ "TasksMaintenanceCategory": "Priežiūra",
+ "TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
+ "Undefined": "Neapibrėžtas",
+ "Forced": "Priverstas",
+ "Default": "Numatytas",
+ "TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 5e3d095ff..5b4c8ae10 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -39,29 +39,29 @@
"MixedContent": "Kandungan campuran",
"Movies": "Filem",
"Music": "Muzik",
- "MusicVideos": "Video muzik",
+ "MusicVideos": "Muzik video",
"NameInstallFailed": "{0} pemasangan gagal",
"NameSeasonNumber": "Musim {0}",
"NameSeasonUnknown": "Musim Tidak Diketahui",
"NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
"NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
- "NotificationOptionApplicationUpdateInstalled": "Application update installed",
- "NotificationOptionAudioPlayback": "Audio playback started",
- "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
- "NotificationOptionCameraImageUploaded": "Camera image uploaded",
+ "NotificationOptionApplicationUpdateInstalled": "Kemas kini aplikasi telah dipasang",
+ "NotificationOptionAudioPlayback": "Ulangmain audio bermula",
+ "NotificationOptionAudioPlaybackStopped": "Ulangmain audio dihentikan",
+ "NotificationOptionCameraImageUploaded": "Imej kamera telah dimuatnaik",
"NotificationOptionInstallationFailed": "Pemasangan gagal",
- "NotificationOptionNewLibraryContent": "New content added",
- "NotificationOptionPluginError": "Plugin failure",
- "NotificationOptionPluginInstalled": "Plugin installed",
+ "NotificationOptionNewLibraryContent": "Kandungan baru telah ditambah",
+ "NotificationOptionPluginError": "Kegagalan plugin",
+ "NotificationOptionPluginInstalled": "Plugin telah dipasang",
"NotificationOptionPluginUninstalled": "Plugin uninstalled",
"NotificationOptionPluginUpdateInstalled": "Plugin update installed",
"NotificationOptionServerRestartRequired": "Server restart required",
"NotificationOptionTaskFailed": "Scheduled task failure",
"NotificationOptionUserLockedOut": "User locked out",
"NotificationOptionVideoPlayback": "Video playback started",
- "NotificationOptionVideoPlaybackStopped": "Video playback stopped",
- "Photos": "Photos",
- "Playlists": "Playlists",
+ "NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan",
+ "Photos": "Gambar-gambar",
+ "Playlists": "Senarai main",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed",
"PluginUninstalledWithName": "{0} was uninstalled",
@@ -71,10 +71,10 @@
"ScheduledTaskStartedWithName": "{0} bermula",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"Shows": "Series",
- "Songs": "Songs",
- "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
+ "Songs": "Lagu-lagu",
+ "StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+ "SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}",
"Sync": "Sync",
"System": "Sistem",
"TvShows": "TV Shows",
@@ -82,14 +82,24 @@
"UserCreatedWithName": "User {0} has been created",
"UserDeletedWithName": "User {0} has been deleted",
"UserDownloadingItemWithValues": "{0} is downloading {1}",
- "UserLockedOutWithName": "User {0} has been locked out",
- "UserOfflineFromDevice": "{0} has disconnected from {1}",
- "UserOnlineFromDevice": "{0} is online from {1}",
- "UserPasswordChangedWithName": "Password has been changed for user {0}",
- "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
+ "UserLockedOutWithName": "Pengguna {0} telah dikunci",
+ "UserOfflineFromDevice": "{0} telah terputus dari {1}",
+ "UserOnlineFromDevice": "{0} berada dalam talian dari {1}",
+ "UserPasswordChangedWithName": "Kata laluan telah ditukar bagi pengguna {0}",
+ "UserPolicyUpdatedWithName": "Dasar pengguna telah dikemas kini untuk {0}",
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
"ValueSpecialEpisodeName": "Khas - {0}",
- "VersionNumber": "Versi {0}"
+ "VersionNumber": "Versi {0}",
+ "TaskCleanActivityLog": "Log Aktiviti Bersih",
+ "TasksChannelsCategory": "Saluran Internet",
+ "TasksApplicationCategory": "Aplikasi",
+ "TasksLibraryCategory": "Perpustakaan",
+ "TasksMaintenanceCategory": "Penyelenggaraan",
+ "Undefined": "Tidak ditentukan",
+ "Forced": "Paksa",
+ "Default": "Asal",
+ "TaskCleanCache": "Bersihkan Direktori Cache",
+ "TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index d5bca9f6c..fbe1f7c4d 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -30,20 +30,20 @@
"ItemAddedWithName": "{0} ble lagt til i biblioteket",
"ItemRemovedWithName": "{0} ble fjernet fra biblioteket",
"LabelIpAddressValue": "IP-adresse: {0}",
- "LabelRunningTimeValue": "Kjøretid {0}",
+ "LabelRunningTimeValue": "Spilletid {0}",
"Latest": "Siste",
- "MessageApplicationUpdated": "Jellyfin Server har blitt oppdatert",
- "MessageApplicationUpdatedTo": "Jellyfin Server ble oppdatert til {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurasjon seksjon {0} har blitt oppdatert",
- "MessageServerConfigurationUpdated": "Serverkonfigurasjon er oppdatert",
+ "MessageApplicationUpdated": "Jellyfin-tjeneren har blitt oppdatert",
+ "MessageApplicationUpdatedTo": "Jellyfin-tjeneren ble oppdatert til {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Tjenerkonfigurasjonsseksjon {0} har blitt oppdatert",
+ "MessageServerConfigurationUpdated": "Tjenerkonfigurasjon er oppdatert",
"MixedContent": "Blandet innhold",
"Movies": "Filmer",
"Music": "Musikk",
"MusicVideos": "Musikkvideoer",
- "NameInstallFailed": "{0}-installasjonen mislyktes",
+ "NameInstallFailed": "Installasjonen av {0} mislyktes",
"NameSeasonNumber": "Sesong {0}",
- "NameSeasonUnknown": "Sesong ukjent",
- "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.",
+ "NameSeasonUnknown": "Ukjent sesong",
+ "NewVersionIsAvailable": "En ny versjon av Jellyfin-tjeneren er tilgjengelig for nedlasting.",
"NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig",
"NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert",
"NotificationOptionAudioPlayback": "Lydavspilling startet",
@@ -51,18 +51,18 @@
"NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp",
"NotificationOptionInstallationFailed": "Installasjonen feilet",
"NotificationOptionNewLibraryContent": "Nytt innhold lagt til",
- "NotificationOptionPluginError": "Pluginfeil",
- "NotificationOptionPluginInstalled": "Plugin installert",
- "NotificationOptionPluginUninstalled": "Plugin avinstallert",
- "NotificationOptionPluginUpdateInstalled": "Pluginoppdatering installert",
- "NotificationOptionServerRestartRequired": "Serveromstart er nødvendig",
+ "NotificationOptionPluginError": "Programvareutvidelsesfeil",
+ "NotificationOptionPluginInstalled": "Programvareutvidelse installert",
+ "NotificationOptionPluginUninstalled": "Programvareutvidelse avinstallert",
+ "NotificationOptionPluginUpdateInstalled": "Programvareutvidelsesoppdatering installert",
+ "NotificationOptionServerRestartRequired": "Tjeneromstart er nødvendig",
"NotificationOptionTaskFailed": "Feil under utføring av planlagt oppgave",
"NotificationOptionUserLockedOut": "Bruker er utestengt",
"NotificationOptionVideoPlayback": "Videoavspilling startet",
"NotificationOptionVideoPlaybackStopped": "Videoavspilling stoppet",
"Photos": "Bilder",
"Playlists": "Spillelister",
- "Plugin": "Plugin",
+ "Plugin": "Programvareutvidelse",
"PluginInstalledWithName": "{0} ble installert",
"PluginUninstalledWithName": "{0} ble avinstallert",
"PluginUpdatedWithName": "{0} ble oppdatert",
@@ -72,7 +72,7 @@
"ServerNameNeedsToBeRestarted": "{0} må startes på nytt",
"Shows": "Program",
"Songs": "Sanger",
- "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
+ "StartupEmbyServerIsLoading": "Jellyfin-tjener laster. Prøv igjen snart.",
"SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}",
"SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}",
"Sync": "Synkroniser",
@@ -86,37 +86,37 @@
"UserOfflineFromDevice": "{0} har koblet fra {1}",
"UserOnlineFromDevice": "{0} er tilkoblet fra {1}",
"UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
- "UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}",
+ "UserPolicyUpdatedWithName": "Brukerretningslinjene har blitt oppdatert for {0}",
"UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}",
- "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}",
+ "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}",
"ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
"ValueSpecialEpisodeName": "Spesialepisode - {0}",
"VersionNumber": "Versjon {0}",
- "TasksChannelsCategory": "Internett kanaler",
+ "TasksChannelsCategory": "Internettkanaler",
"TasksApplicationCategory": "Applikasjon",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedlikehold",
- "TaskCleanCache": "Tøm buffer katalog",
+ "TaskCleanCache": "Tøm hurtigbuffer",
"TaskRefreshLibrary": "Skann mediebibliotek",
"TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.",
- "TaskRefreshChapterImages": "Trekk ut Kapittelbilder",
+ "TaskRefreshChapterImages": "Trekk ut kapittelbilder",
"TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet.",
- "TaskDownloadMissingSubtitlesDescription": "Søker etter manglende underteksting på nett basert på metadatakonfigurasjon.",
- "TaskDownloadMissingSubtitles": "Last ned manglende underteksting",
- "TaskRefreshChannelsDescription": "Frisker opp internettkanalinformasjon.",
- "TaskRefreshChannels": "Oppfrisk kanaler",
+ "TaskDownloadMissingSubtitlesDescription": "Søker etter manglende undertekster på nett basert på metadatakonfigurasjon.",
+ "TaskDownloadMissingSubtitles": "Last ned manglende undertekster",
+ "TaskRefreshChannelsDescription": "Oppdaterer internettkanalinformasjon.",
+ "TaskRefreshChannels": "Oppdater kanaler",
"TaskCleanTranscodeDescription": "Sletter omkodede filer som er mer enn én dag gamle.",
"TaskCleanTranscode": "Tøm transkodingmappe",
- "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringer for utvidelser som er stilt inn til å oppdatere automatisk.",
- "TaskUpdatePlugins": "Oppdater utvidelser",
+ "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringer for programvareutvidelser som er stilt inn til å oppdatere automatisk.",
+ "TaskUpdatePlugins": "Oppdater programvareutvidelse",
"TaskRefreshPeopleDescription": "Oppdaterer metadata for skuespillere og regissører i mediebiblioteket ditt.",
- "TaskRefreshPeople": "Oppfrisk personer",
+ "TaskRefreshPeople": "Oppdater personer",
"TaskCleanLogsDescription": "Sletter loggfiler som er eldre enn {0} dager gamle.",
"TaskCleanLogs": "Tøm loggmappe",
"TaskRefreshLibraryDescription": "Skanner mediebibliotekene dine for nye filer og oppdaterer metadata.",
"TaskCleanActivityLog": "Tøm aktivitetslogg",
"Undefined": "Udefinert",
- "Forced": "Tvungen",
+ "Forced": "Tvunget",
"Default": "Standard",
"TaskCleanActivityLogDescription": "Sletter oppføringer i aktivitetsloggen som er eldre enn den konfigurerte alderen."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index ffc329e35..2973c8c6e 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -3,9 +3,9 @@
"AppDeviceValues": "App: {0}, Apparaat: {1}",
"Application": "Applicatie",
"Artists": "Artiesten",
- "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
+ "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
"Books": "Boeken",
- "CameraImageUploadedFrom": "Er is een nieuwe camera afbeelding toegevoegd via {0}",
+ "CameraImageUploadedFrom": "Nieuwe camera afbeelding toegevoegd vanaf {0}",
"Channels": "Kanalen",
"ChapterNameValue": "Hoofdstuk {0}",
"Collections": "Verzamelingen",
diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json
index 6236515b2..32d4f3a8b 100644
--- a/Emby.Server.Implementations/Localization/Core/nn.json
+++ b/Emby.Server.Implementations/Localization/Core/nn.json
@@ -1,25 +1,25 @@
{
- "MessageServerConfigurationUpdated": "Tenar konfigurasjonen har blitt oppdatert",
+ "MessageServerConfigurationUpdated": "Tenarkonfigurasjonen har blitt oppdatert",
"MessageNamedServerConfigurationUpdatedWithValue": "Tenar konfigurasjon seksjon {0} har blitt oppdatert",
- "MessageApplicationUpdatedTo": "Jellyfin Tenaren har blitt oppdatert til {0}",
- "MessageApplicationUpdated": "Jellyfin Tenaren har blitt oppdatert",
+ "MessageApplicationUpdatedTo": "Jellyfin-tenaren har blitt oppdatert til {0}",
+ "MessageApplicationUpdated": "Jellyfin-tenaren har blitt oppdatert",
"Latest": "Nyaste",
"LabelRunningTimeValue": "Speletid: {0}",
- "LabelIpAddressValue": "IP adresse: {0}",
+ "LabelIpAddressValue": "IP-adresse: {0}",
"ItemRemovedWithName": "{0} vart fjerna frå biblioteket",
"ItemAddedWithName": "{0} vart lagt til i biblioteket",
- "Inherit": "Arv",
- "HomeVideos": "Heime Videoar",
+ "Inherit": "Arve",
+ "HomeVideos": "Heimevideoar",
"HeaderRecordingGroups": "Innspelingsgrupper",
"HeaderNextUp": "Neste",
"HeaderLiveTV": "Direkte TV",
- "HeaderFavoriteSongs": "Favoritt Songar",
- "HeaderFavoriteShows": "Favoritt Seriar",
- "HeaderFavoriteEpisodes": "Favoritt Episodar",
- "HeaderFavoriteArtists": "Favoritt Artistar",
- "HeaderFavoriteAlbums": "Favoritt Album",
+ "HeaderFavoriteSongs": "Favorittsongar",
+ "HeaderFavoriteShows": "Favorittseriar",
+ "HeaderFavoriteEpisodes": "Favorittepisodar",
+ "HeaderFavoriteArtists": "Favorittartistar",
+ "HeaderFavoriteAlbums": "Favorittalbum",
"HeaderContinueWatching": "Fortsett å sjå",
- "HeaderAlbumArtists": "Album Artist",
+ "HeaderAlbumArtists": "Albumartist",
"Genres": "Sjangrar",
"Folders": "Mapper",
"Favorites": "Favorittar",
@@ -29,18 +29,18 @@
"Collections": "Samlingar",
"ChapterNameValue": "Kapittel {0}",
"Channels": "Kanalar",
- "CameraImageUploadedFrom": "Eit nytt kamera bilete har blitt lasta opp frå {0}",
+ "CameraImageUploadedFrom": "Eit nytt kamerabilete har blitt lasta opp frå {0}",
"Books": "Bøker",
- "AuthenticationSucceededWithUserName": "{0} Har logga inn",
+ "AuthenticationSucceededWithUserName": "{0} har logga inn",
"Artists": "Artistar",
"Application": "Program",
"AppDeviceValues": "App: {0}, Eining: {1}",
"Albums": "Album",
"NotificationOptionServerRestartRequired": "Tenaren krev omstart",
- "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert",
- "NotificationOptionPluginUninstalled": "Tilleggsprogram avinstallert",
- "NotificationOptionPluginInstalled": "Tilleggsprogram installert",
- "NotificationOptionPluginError": "Tilleggsprogram feila",
+ "NotificationOptionPluginUpdateInstalled": "Programvaretilleggoppdatering vart installert",
+ "NotificationOptionPluginUninstalled": "Programvaretillegg avinstallert",
+ "NotificationOptionPluginInstalled": "Programvaretillegg installert",
+ "NotificationOptionPluginError": "Programvaretillegg feila",
"NotificationOptionNewLibraryContent": "Nytt innhald er lagt til",
"NotificationOptionInstallationFailed": "Installasjonsfeil",
"NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp",
@@ -48,33 +48,33 @@
"NotificationOptionAudioPlayback": "Lydavspilling påbyrja",
"NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering er installert",
"NotificationOptionApplicationUpdateAvailable": "Applikasjonsoppdatering er tilgjengeleg",
- "NewVersionIsAvailable": "Ein ny versjon av Jellyfin serveren er tilgjengeleg for nedlasting.",
+ "NewVersionIsAvailable": "Ein ny versjon av Jellyfin-tjenaren er tilgjengeleg for nedlasting.",
"NameSeasonUnknown": "Ukjend sesong",
"NameSeasonNumber": "Sesong {0}",
- "NameInstallFailed": "{0} Installasjonen feila",
+ "NameInstallFailed": "Installasjonen av {0} feila",
"MusicVideos": "Musikkvideoar",
"Music": "Musikk",
"Movies": "Filmar",
"MixedContent": "Blanda innhald",
- "Sync": "Synkronisera",
+ "Sync": "Synkroniser",
"TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.",
"TaskDownloadMissingSubtitles": "Last ned manglande undertekstar",
"TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.",
"TaskRefreshChannels": "Oppdater kanalar",
- "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.",
- "TaskCleanTranscode": "Reins transkodemappe",
+ "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gammal.",
+ "TaskCleanTranscode": "Fjern transkodemappe",
"TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.",
- "TaskUpdatePlugins": "Oppdaterer programtillegg",
+ "TaskUpdatePlugins": "Oppdaterer programvaretillegg",
"TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.",
"TaskRefreshPeople": "Oppdater personar",
"TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.",
- "TaskCleanLogs": "Reins loggmappe",
+ "TaskCleanLogs": "Slett loggmappa",
"TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.",
"TaskRefreshLibrary": "Skann mediebibliotek",
"TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.",
"TaskRefreshChapterImages": "Trekk ut kapittelbilete",
- "TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.",
- "TaskCleanCache": "Rens mappe for hurtiglager",
+ "TaskCleanCacheDescription": "Sletter mellomlagra filer som ikkje lengre trengst av systemet.",
+ "TaskCleanCache": "Fjern hurtigbuffer",
"TasksChannelsCategory": "Internettkanalar",
"TasksApplicationCategory": "Applikasjon",
"TasksLibraryCategory": "Bibliotek",
@@ -96,9 +96,9 @@
"TvShows": "TV-seriar",
"System": "System",
"SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}",
- "StartupEmbyServerIsLoading": "Jellyfintenaren laster. Prøv igjen om litt.",
- "Songs": "Songar",
- "Shows": "Program",
+ "StartupEmbyServerIsLoading": "Jellyfin-tenaren laster. Prøv igjen seinare.",
+ "Songs": "Sangar",
+ "Shows": "Seriar",
"ServerNameNeedsToBeRestarted": "{0} må omstartast",
"ScheduledTaskStartedWithName": "{0} starta",
"ScheduledTaskFailedWithName": "{0} feila",
@@ -106,11 +106,16 @@
"PluginUpdatedWithName": "{0} blei oppdatert",
"PluginUninstalledWithName": "{0} blei avinstallert",
"PluginInstalledWithName": "{0} blei installert",
- "Plugin": "Programtillegg",
- "Playlists": "Speleliste",
- "Photos": "Foto",
+ "Plugin": "Programvaretillegg",
+ "Playlists": "Spelelister",
+ "Photos": "Bilete",
"NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa",
"NotificationOptionVideoPlayback": "Videoavspeling starta",
"NotificationOptionUserLockedOut": "Brukar er utestengd",
- "NotificationOptionTaskFailed": "Planlagt oppgåve feila"
+ "NotificationOptionTaskFailed": "Planlagt oppgåve feila",
+ "TaskCleanActivityLogDescription": "Sletter aktivitetslogginnlegg som er eldre enn den konfigurerte alderen.",
+ "TaskCleanActivityLog": "Slett aktivitetslogg",
+ "Undefined": "Udefinert",
+ "Forced": "Tvungen",
+ "Default": "Standard"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json
index 469fa89b6..d1db09232 100644
--- a/Emby.Server.Implementations/Localization/Core/pa.json
+++ b/Emby.Server.Implementations/Localization/Core/pa.json
@@ -1,5 +1,5 @@
{
- "TaskRefreshChapterImages": "ਐਬਸਟਰੈਕਟ ਅਧਿਆਇ ਅਧਿਆਇ",
+ "TaskRefreshChapterImages": "ਐਕਸਟਰੈਕਟ ਚੈਪਟਰ ਚਿੱਤਰ",
"TaskDownloadMissingSubtitlesDescription": "ਮੈਟਾਡੇਟਾ ਕੌਂਫਿਗਰੇਸ਼ਨ ਦੇ ਅਧਾਰ ਤੇ ਗਾਇਬ ਉਪਸਿਰਲੇਖਾਂ ਲਈ ਇੰਟਰਨੈਟ ਦੀ ਭਾਲ ਕਰਦਾ ਹੈ.",
"TaskDownloadMissingSubtitles": "ਗਾਇਬ ਉਪਸਿਰਲੇਖ ਡਾ Download ਨਲੋਡ ਕਰੋ",
"TaskRefreshChannelsDescription": "ਇੰਟਰਨੈੱਟ ਚੈਨਲ ਦੀ ਜਾਣਕਾਰੀ ਨੂੰ ਤਾਜ਼ਾ ਕਰਦਾ ਹੈ.",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 5ec8f1e88..323dcced0 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -16,7 +16,7 @@
"Folders": "Pastas",
"Genres": "Gêneros",
"HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderContinueWatching": "Continuar Assistindo",
+ "HeaderContinueWatching": "Continuar assistindo",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episódios favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 46b47cf4a..e58f8c39d 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -39,7 +39,7 @@
"MixedContent": "Смешанное содержимое",
"Movies": "Кино",
"Music": "Музыка",
- "MusicVideos": "Музыкальные клипы",
+ "MusicVideos": "Муз. видео",
"NameInstallFailed": "Установка {0} неудачна",
"NameSeasonNumber": "Сезон {0}",
"NameSeasonUnknown": "Сезон неопознан",
@@ -75,7 +75,7 @@
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
- "Sync": "Синхронизация",
+ "Sync": "Синхро",
"System": "Система",
"TvShows": "ТВ",
"User": "Пользователь",
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index d785bcb90..15fb34186 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -4,11 +4,11 @@
"VersionNumber": "Верзија {0}",
"ValueSpecialEpisodeName": "Специјал - {0}",
"ValueHasBeenAddedToLibrary": "{0} је додато у вашу медијску библиотеку",
- "UserStoppedPlayingItemWithValues": "{0} заврши пуштање {1} на {2}",
+ "UserStoppedPlayingItemWithValues": "{0} завршио пуштање {1} на {2}",
"UserStartedPlayingItemWithValues": "{0} пушта {1} на {2}",
"UserPasswordChangedWithName": "Лозинка је промењена за корисника {0}",
"UserOnlineFromDevice": "{0} је на вези од {1}",
- "UserOfflineFromDevice": "{0} се одвезао са {1}",
+ "UserOfflineFromDevice": "{0} је прекинуо/а везу са {1}",
"UserLockedOutWithName": "Корисник {0} је закључан",
"UserDownloadingItemWithValues": "{0} преузима {1}",
"UserDeletedWithName": "Корисник {0} је обрисан",
@@ -41,7 +41,7 @@
"NotificationOptionPluginError": "Грешка прикључка",
"NotificationOptionNewLibraryContent": "Додат нови садржај",
"NotificationOptionInstallationFailed": "Неуспела инсталација",
- "NotificationOptionCameraImageUploaded": "Слика са камере послата",
+ "NotificationOptionCameraImageUploaded": "Слика са камере отпремљена",
"NotificationOptionAudioPlaybackStopped": "Заустављено пуштање звука",
"NotificationOptionAudioPlayback": "Покренуто пуштање звука",
"NotificationOptionApplicationUpdateInstalled": "Ажурирање инсталирано",
@@ -86,7 +86,7 @@
"Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографија је учитана са {0}",
"Books": "Књиге",
- "AuthenticationSucceededWithUserName": "{0} успешно проверено",
+ "AuthenticationSucceededWithUserName": "{0} Успешна аутентикација",
"Artists": "Извођачи",
"Application": "Апликација",
"AppDeviceValues": "Апликација: {0}, Уређај: {1}",
@@ -100,7 +100,7 @@
"TaskUpdatePluginsDescription": "Преузима и инсталира исправке за додатке који су конфигурисани за аутоматско ажурирање.",
"TaskUpdatePlugins": "Ажурирајте додатке",
"TaskRefreshPeopleDescription": "Ажурира метаподатке за глумце и редитеље у вашој медијској библиотеци.",
- "TaskRefreshPeople": "Освежите људе",
+ "TaskRefreshPeople": "Освежите кориснике",
"TaskCleanLogsDescription": "Брише логове старије од {0} дана.",
"TaskCleanLogs": "Очистите директоријум логова",
"TaskRefreshLibraryDescription": "Скенира вашу медијску библиотеку за нове датотеке и освежава метаподатке.",
@@ -116,6 +116,6 @@
"TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
"TaskCleanActivityLog": "Очисти историју активности",
"Undefined": "Недефинисано",
- "Forced": "Форсирано",
+ "Forced": "Принудно",
"Default": "Подразумевано"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 345d41e9e..d992bf79b 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -39,7 +39,7 @@
"MixedContent": "Blandat innehåll",
"Movies": "Filmer",
"Music": "Musik",
- "MusicVideos": "Musikvideos",
+ "MusicVideos": "Musikvideor",
"NameInstallFailed": "{0} installationen misslyckades",
"NameSeasonNumber": "Säsong {0}",
"NameSeasonUnknown": "Okänd säsong",
@@ -117,5 +117,6 @@
"TaskCleanActivityLogDescription": "Radera aktivitets logg inlägg som är äldre än definerad ålder.",
"TaskCleanActivityLog": "Rensa Aktivitets Logg",
"Undefined": "odefinierad",
- "Forced": "Tvingad"
+ "Forced": "Tvingad",
+ "Default": "Standard"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index c737ba42b..129986ed0 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -69,7 +69,7 @@
"NameSeasonUnknown": "அறியப்படாத பருவம்",
"NameSeasonNumber": "பருவம் {0}",
"NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது",
- "MusicVideos": "இசைப்படங்கள்",
+ "MusicVideos": "இசை கானொளி",
"Music": "இசை",
"Movies": "திரைப்படங்கள்",
"Latest": "புதியவை",
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 71dd2c7a3..e26010423 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -50,7 +50,7 @@
"HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
"HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
"HeaderContinueWatching": "ดูต่อ",
- "HeaderAlbumArtists": "อัลบั้มศิลปิน",
+ "HeaderAlbumArtists": "ศิลปินอัลบั้ม",
"Genres": "ประเภท",
"Folders": "โฟลเดอร์",
"Favorites": "รายการโปรด",
@@ -112,5 +112,10 @@
"System": "ระบบ",
"Sync": "ซิงค์",
"SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
- "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
+ "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่",
+ "Default": "ค่าเริ่มต้น",
+ "TaskCleanActivityLogDescription": "ลบบันทึกกิจกรรมที่เก่ากว่าค่าที่กำหนดไว้",
+ "TaskCleanActivityLog": "ล้างบันทึกกิจกรรม",
+ "Undefined": "ไม่ได้กำหนด",
+ "Forced": "บังคับใช้"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index b6073bf6a..5a2069df5 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -1,5 +1,5 @@
{
- "MusicVideos": "Музичні кліпи",
+ "MusicVideos": "Музичні відеокліпи",
"Music": "Музика",
"Movies": "Фільми",
"MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
@@ -113,7 +113,7 @@
"MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено",
"Inherit": "Успадкувати",
"HeaderRecordingGroups": "Групи запису",
- "Forced": "Примусово",
+ "Forced": "Форсовані",
"TaskCleanActivityLogDescription": "Видаляє старші за встановлений термін записи з журналу активності.",
"TaskCleanActivityLog": "Очистити журнал активності",
"Undefined": "Не визначено",
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 40368d464..58652c469 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -3,7 +3,7 @@
"Favorites": "Yêu Thích",
"Folders": "Thư Mục",
"Genres": "Thể Loại",
- "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ",
+ "HeaderAlbumArtists": "Tuyển Tập Nghệ sĩ",
"HeaderContinueWatching": "Xem Tiếp",
"HeaderLiveTV": "TV Trực Tiếp",
"Movies": "Phim",
@@ -13,7 +13,7 @@
"Songs": "Các Bài Hát",
"Sync": "Đồng Bộ",
"ValueSpecialEpisodeName": "Đặc Biệt - {0}",
- "Albums": "Albums",
+ "Albums": "Tuyển Tập",
"Artists": "Các Nghệ Sĩ",
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
"TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index affb0e099..c3b223f63 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -37,7 +37,7 @@
"MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
- "MusicVideos": "音樂MV",
+ "MusicVideos": "音樂錄影帶",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 3f9e22106..b1ff28c2c 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -1,17 +1,18 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Localization
@@ -36,7 +37,7 @@ namespace Emby.Server.Implementations.Localization
private List _cultures;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
///
/// Initializes a new instance of the class.
@@ -73,8 +74,7 @@ namespace Emby.Server.Implementations.Localization
using (var str = _assembly.GetManifestResourceStream(resource))
using (var reader = new StreamReader(str))
{
- string line;
- while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
if (string.IsNullOrWhiteSpace(line))
{
@@ -119,10 +119,8 @@ namespace Emby.Server.Implementations.Localization
using (var stream = _assembly.GetManifestResourceStream(ResourcePath))
using (var reader = new StreamReader(stream))
{
- while (!reader.EndOfStream)
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- var line = await reader.ReadLineAsync().ConfigureAwait(false);
-
if (string.IsNullOrWhiteSpace(line))
{
continue;
@@ -170,17 +168,27 @@ namespace Emby.Server.Implementations.Localization
///
public CultureDto FindLanguageInfo(string language)
- => GetCultures()
- .FirstOrDefault(i =>
- string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase)
- || string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase)
- || i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase)
- || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase));
+ {
+ // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
+ for (var i = 0; i < _cultures.Count; i++)
+ {
+ var culture = _cultures[i];
+ if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
+ || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
+ || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase)
+ || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
+ {
+ return culture;
+ }
+ }
+
+ return default;
+ }
///
public IEnumerable GetCountries()
{
- StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
+ using StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
return JsonSerializer.Deserialize>(reader.ReadToEnd(), _jsonOptions);
}
@@ -225,7 +233,7 @@ namespace Emby.Server.Implementations.Localization
throw new ArgumentNullException(nameof(rating));
}
- if (_unratedValues.Contains(rating, StringComparer.OrdinalIgnoreCase))
+ if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return null;
}
@@ -253,11 +261,11 @@ namespace Emby.Server.Implementations.Localization
var index = rating.IndexOf(':', StringComparison.Ordinal);
if (index != -1)
{
- rating = rating.Substring(index).TrimStart(':').Trim();
+ var trimmedRating = rating.AsSpan(index).TrimStart(':').Trim();
- if (!string.IsNullOrWhiteSpace(rating))
+ if (!trimmedRating.IsEmpty)
{
- return GetRatingLevel(rating);
+ return GetRatingLevel(trimmedRating.ToString());
}
}
@@ -316,11 +324,11 @@ namespace Emby.Server.Implementations.Localization
}
const string Prefix = "Core";
- var key = Prefix + culture;
return _dictionaries.GetOrAdd(
- key,
- f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
+ culture,
+ (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(),
+ this);
}
private async Task> GetDictionary(string prefix, string culture, string baseFilename)
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index f27305cbe..8aaa1f7bb 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -15,7 +17,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.MediaEncoder
@@ -82,11 +83,6 @@ namespace Emby.Server.Implementations.MediaEncoder
return false;
}
- if (video.VideoType == VideoType.Dvd)
- {
- return false;
- }
-
if (video.IsShortcut)
{
return false;
@@ -166,7 +162,7 @@ namespace Emby.Server.Implementations.MediaEncoder
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(",", video.Path));
+ _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path));
success = false;
break;
}
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 0781a0e33..137728616 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index 4e25768cf..a8b18d292 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 932f721ab..9a1ca9946 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -215,7 +217,7 @@ namespace Emby.Server.Implementations.Playlists
// Create a list of the new linked children to add to the playlist
var childrenToAdd = newItems
- .Select(i => LinkedChild.Create(i))
+ .Select(LinkedChild.Create)
.ToList();
// Log duplicates that have been ignored, if any
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index c26ccfd88..8fd61f2bc 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -1,8 +1,9 @@
-#nullable enable
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
@@ -11,9 +12,11 @@ using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Json.Converters;
+using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -30,9 +33,19 @@ namespace Emby.Server.Implementations.Plugins
private readonly ILogger _logger;
private readonly IApplicationHost _appHost;
private readonly ServerConfiguration _config;
- private readonly IList _plugins;
+ private readonly List _plugins;
private readonly Version _minimumVersion;
+ private IHttpClientFactory? _httpClientFactory;
+
+ private IHttpClientFactory HttpClientFactory
+ {
+ get
+ {
+ return _httpClientFactory ?? (_httpClientFactory = _appHost.Resolve());
+ }
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -51,7 +64,7 @@ namespace Emby.Server.Implementations.Plugins
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_pluginsPath = pluginsPath;
_appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
- _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions())
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options)
{
WriteIndented = true
};
@@ -75,7 +88,7 @@ namespace Emby.Server.Implementations.Plugins
///
/// Gets the Plugins.
///
- public IList Plugins => _plugins;
+ public IReadOnlyList Plugins => _plugins;
///
/// Returns all the assemblies.
@@ -146,9 +159,7 @@ namespace Emby.Server.Implementations.Plugins
///
public void CreatePlugins()
{
- _ = _appHost.GetExports(CreatePluginInstance)
- .Where(i => i != null)
- .ToArray();
+ _ = _appHost.GetExports(CreatePluginInstance);
}
///
@@ -258,11 +269,7 @@ namespace Emby.Server.Implementations.Plugins
// If no version is given, return the current instance.
var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList();
- plugin = plugins.FirstOrDefault(p => p.Instance != null);
- if (plugin == null)
- {
- plugin = plugins.OrderByDescending(p => p.Version).FirstOrDefault();
- }
+ plugin = plugins.FirstOrDefault(p => p.Instance != null) ?? plugins.OrderByDescending(p => p.Version).FirstOrDefault();
}
else
{
@@ -332,34 +339,76 @@ namespace Emby.Server.Implementations.Plugins
ChangePluginState(plugin, PluginStatus.Malfunctioned);
}
- ///
- /// Saves the manifest back to disk.
- ///
- /// The to save.
- /// The path where to save the manifest.
- /// True if successful.
+ ///
public bool SaveManifest(PluginManifest manifest, string path)
{
- if (manifest == null)
- {
- return false;
- }
-
try
{
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
File.WriteAllText(Path.Combine(path, "meta.json"), data);
return true;
}
-#pragma warning disable CA1031 // Do not catch general exception types
- catch (Exception e)
-#pragma warning restore CA1031 // Do not catch general exception types
+ catch (ArgumentException e)
{
- _logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
+ _logger.LogWarning(e, "Unable to save plugin manifest due to invalid value. {Path}", path);
return false;
}
}
+ ///
+ public async Task GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
+ {
+ if (packageInfo == null)
+ {
+ return false;
+ }
+
+ var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
+ var imagePath = string.Empty;
+
+ if (!string.IsNullOrEmpty(packageInfo.ImageUrl))
+ {
+ var url = new Uri(packageInfo.ImageUrl);
+ imagePath = Path.Join(path, url.Segments[^1]);
+
+ await using var fileStream = File.OpenWrite(imagePath);
+
+ try
+ {
+ await using var downloadStream = await HttpClientFactory
+ .CreateClient(NamedClient.Default)
+ .GetStreamAsync(url)
+ .ConfigureAwait(false);
+
+ await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
+ }
+ catch (HttpRequestException ex)
+ {
+ _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
+ imagePath = string.Empty;
+ }
+ }
+
+ var manifest = new PluginManifest
+ {
+ Category = packageInfo.Category,
+ Changelog = versionInfo.Changelog ?? string.Empty,
+ Description = packageInfo.Description,
+ Id = packageInfo.Id,
+ Name = packageInfo.Name,
+ Overview = packageInfo.Overview,
+ Owner = packageInfo.Owner,
+ TargetAbi = versionInfo.TargetAbi ?? string.Empty,
+ Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
+ Version = versionInfo.Version,
+ Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
+ AutoUpdate = true,
+ ImagePath = imagePath
+ };
+
+ return SaveManifest(manifest, path);
+ }
+
///
/// Changes a plugin's load status.
///
@@ -406,11 +455,12 @@ namespace Emby.Server.Implementations.Plugins
try
{
_logger.LogDebug("Creating instance of {Type}", type);
- var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
+ // _appHost.ServiceProvider is already assigned when we create the plugins
+ var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider!, type);
if (plugin == null)
{
// Create a dummy record for the providers.
- // TODO: remove this code, if all provided have been released as separate plugins.
+ // TODO: remove this code once all provided have been released as separate plugins.
plugin = new LocalPlugin(
instance.AssemblyFilePath,
true,
@@ -617,7 +667,7 @@ namespace Emby.Server.Implementations.Plugins
var entry = versions[x];
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
{
- entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories));
+ entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories);
if (entry.IsEnabledAndSupported)
{
lastName = entry.Name;
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index 7bed06de3..7cfd1fced 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -1,9 +1,10 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
-using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
@@ -258,20 +259,17 @@ namespace Emby.Server.Implementations.QuickConnect
}
// Expire stale connection requests
- var code = string.Empty;
- var values = _currentRequests.Values.ToList();
-
- for (int i = 0; i < values.Count; i++)
+ foreach (var (_, currentRequest) in _currentRequests)
{
- var added = values[i].DateAdded ?? DateTime.UnixEpoch;
- if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
+ var added = currentRequest.DateAdded ?? DateTime.UnixEpoch;
+ if (expireAll || DateTime.UtcNow > added.AddMinutes(Timeout))
{
- code = values[i].Code;
- _logger.LogDebug("Removing expired request {code}", code);
+ var code = currentRequest.Code;
+ _logger.LogDebug("Removing expired request {Code}", code);
if (!_currentRequests.TryRemove(code, out _))
{
- _logger.LogWarning("Request {code} already expired", code);
+ _logger.LogWarning("Request {Code} already expired", code);
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index b302303f8..d7e320754 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -1,10 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -69,7 +70,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
///
/// The options for the json Serializer.
///
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
///
/// Initializes a new instance of the class.
@@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
lock (_lastExecutionResultSyncLock)
{
using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
- JsonSerializer.SerializeAsync(createStream, value, _jsonOptions);
+ using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream);
+ JsonSerializer.Serialize(jsonStream, value, _jsonOptions);
}
}
}
@@ -301,12 +303,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
get
{
- if (_id == null)
- {
- _id = ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
-
- return _id;
+ return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
}
@@ -348,9 +345,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
var trigger = (ITaskTrigger)sender;
- var configurableTask = ScheduledTask as IConfigurableScheduledTask;
-
- if (configurableTask != null && !configurableTask.IsEnabled)
+ if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled)
{
return;
}
@@ -578,7 +573,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
Directory.CreateDirectory(Path.GetDirectoryName(path));
using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
- JsonSerializer.SerializeAsync(createStream, triggers, _jsonOptions);
+ using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream);
+ JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions);
}
///
@@ -715,11 +711,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
}
- return new DailyTrigger
- {
- TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
- TaskOptions = options
- };
+ return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options);
}
if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
@@ -734,12 +726,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info));
}
- return new WeeklyTrigger
- {
- TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
- DayOfWeek = info.DayOfWeek.Value,
- TaskOptions = options
- };
+ return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options);
}
if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
@@ -749,16 +736,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info));
}
- return new IntervalTrigger
- {
- Interval = TimeSpan.FromTicks(info.IntervalTicks.Value),
- TaskOptions = options
- };
+ return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options);
}
if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
{
- return new StartupTrigger();
+ return new StartupTrigger(options);
}
throw new ArgumentException("Unrecognized trigger type: " + info.Type);
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index af316e108..4f0df75bf 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 171e44258..baeb86a22 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -12,9 +12,9 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks
{
@@ -140,10 +140,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
previouslyFailedImages.Add(key);
var parentPath = Path.GetDirectoryName(failHistoryPath);
+ if (parentPath != null)
+ {
+ Directory.CreateDirectory(parentPath);
+ }
- Directory.CreateDirectory(parentPath);
-
- string text = string.Join("|", previouslyFailedImages);
+ string text = string.Join('|', previouslyFailedImages);
File.WriteAllText(failHistoryPath, text);
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
index 4abbf784b..50ba9bc89 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -75,4 +75,4 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return Enumerable.Empty();
}
}
-}
\ No newline at end of file
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
new file mode 100644
index 000000000..1ad1d0f50
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Server.Implementations;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+{
+ ///
+ /// Optimizes Jellyfin's database by issuing a VACUUM command.
+ ///
+ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ private readonly ILogger _logger;
+ private readonly ILocalizationManager _localization;
+ private readonly JellyfinDbProvider _provider;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public OptimizeDatabaseTask(
+ ILogger logger,
+ ILocalizationManager localization,
+ JellyfinDbProvider provider)
+ {
+ _logger = logger;
+ _localization = localization;
+ _provider = provider;
+ }
+
+ ///
+ public string Name => _localization.GetLocalizedString("TaskOptimizeDatabase");
+
+ ///
+ public string Description => _localization.GetLocalizedString("TaskOptimizeDatabaseDescription");
+
+ ///
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ ///
+ public string Key => "OptimizeDatabaseTask";
+
+ ///
+ public bool IsHidden => false;
+
+ ///
+ public bool IsEnabled => true;
+
+ ///
+ public bool IsLogged => true;
+
+ ///
+ /// Creates the triggers that define when the task will run.
+ ///
+ /// IEnumerable{BaseTaskTrigger}.
+ public IEnumerable GetDefaultTriggers()
+ {
+ return new[]
+ {
+ // Every so often
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ };
+ }
+
+ ///
+ /// Returns the task to be executed.
+ ///
+ /// The cancellation token.
+ /// The progress.
+ /// Task.
+ public Task Execute(CancellationToken cancellationToken, IProgress progress)
+ {
+ _logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
+
+ try
+ {
+ using var context = _provider.CreateContext();
+ if (context.Database.IsSqlite())
+ {
+ context.Database.ExecuteSqlRaw("PRAGMA optimize");
+ context.Database.ExecuteSqlRaw("VACUUM");
+ _logger.LogInformation("jellyfin.db optimized successfully!");
+ }
+ else
+ {
+ _logger.LogInformation("This database doesn't support optimization");
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error while optimizing jellyfin.db");
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
index c384cf4bb..57d294a40 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
@@ -5,8 +5,8 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.ScheduledTasks
{
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index a69380cbb..11a5fb79f 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -9,7 +9,6 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
index e470adcf4..51b620404 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
@@ -6,8 +6,8 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.ScheduledTasks
{
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
index 3b40320ab..29ab6a73d 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
@@ -8,29 +8,31 @@ namespace Emby.Server.Implementations.ScheduledTasks
///
/// Represents a task trigger that fires everyday.
///
- public class DailyTrigger : ITaskTrigger
+ public sealed class DailyTrigger : ITaskTrigger
{
+ private readonly TimeSpan _timeOfDay;
+ private Timer? _timer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The time of day to trigger the task to run.
+ /// The options of this task.
+ public DailyTrigger(TimeSpan timeofDay, TaskOptions taskOptions)
+ {
+ _timeOfDay = timeofDay;
+ TaskOptions = taskOptions;
+ }
+
///
/// Occurs when [triggered].
///
- public event EventHandler Triggered;
+ public event EventHandler? Triggered;
///
- /// Gets or sets the time of day to trigger the task to run.
+ /// Gets the options of this task.
///
- /// The time of day.
- public TimeSpan TimeOfDay { get; set; }
-
- ///
- /// Gets or sets the options of this task.
- ///
- public TaskOptions TaskOptions { get; set; }
-
- ///
- /// Gets or sets the timer.
- ///
- /// The timer.
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
///
/// Stars waiting for the trigger action.
@@ -45,14 +47,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
var now = DateTime.Now;
- var triggerDate = now.TimeOfDay > TimeOfDay ? now.Date.AddDays(1) : now.Date;
- triggerDate = triggerDate.Add(TimeOfDay);
+ var triggerDate = now.TimeOfDay > _timeOfDay ? now.Date.AddDays(1) : now.Date;
+ triggerDate = triggerDate.Add(_timeOfDay);
var dueTime = triggerDate - now;
logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime);
- Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
}
///
@@ -68,10 +70,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
///
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
///
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
index b04fd7c7e..30568e809 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
@@ -9,31 +9,32 @@ namespace Emby.Server.Implementations.ScheduledTasks
///
/// Represents a task trigger that runs repeatedly on an interval.
///
- public class IntervalTrigger : ITaskTrigger
+ public sealed class IntervalTrigger : ITaskTrigger
{
+ private readonly TimeSpan _interval;
private DateTime _lastStartDate;
+ private Timer? _timer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The interval.
+ /// The options of this task.
+ public IntervalTrigger(TimeSpan interval, TaskOptions taskOptions)
+ {
+ _interval = interval;
+ TaskOptions = taskOptions;
+ }
///
/// Occurs when [triggered].
///
- public event EventHandler Triggered;
+ public event EventHandler? Triggered;
///
- /// Gets or sets the interval.
+ /// Gets the options of this task.
///
- /// The interval.
- public TimeSpan Interval { get; set; }
-
- ///
- /// Gets or sets the options of this task.
- ///
- public TaskOptions TaskOptions { get; set; }
-
- ///
- /// Gets or sets the timer.
- ///
- /// The timer.
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
///
/// Stars waiting for the trigger action.
@@ -55,7 +56,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
else
{
- triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(Interval);
+ triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(_interval);
}
if (DateTime.UtcNow > triggerDate)
@@ -71,7 +72,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
dueTime = maxDueTime;
}
- Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
}
///
@@ -87,10 +88,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
///
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
///
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
index 7cd5493da..18b9a8b75 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
@@ -10,24 +10,28 @@ namespace Emby.Server.Implementations.ScheduledTasks
///
/// Class StartupTaskTrigger.
///
- public class StartupTrigger : ITaskTrigger
+ public sealed class StartupTrigger : ITaskTrigger
{
+ public const int DelayMs = 3000;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The options of this task.
+ public StartupTrigger(TaskOptions taskOptions)
+ {
+ TaskOptions = taskOptions;
+ }
+
///
/// Occurs when [triggered].
///
- public event EventHandler Triggered;
-
- public int DelayMs { get; set; }
+ public event EventHandler? Triggered;
///
- /// Gets or sets the options of this task.
+ /// Gets the options of this task.
///
- public TaskOptions TaskOptions { get; set; }
-
- public StartupTrigger()
- {
- DelayMs = 3000;
- }
+ public TaskOptions TaskOptions { get; }
///
/// Stars waiting for the trigger action.
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
index 0c0ebec08..36ae190b0 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
@@ -8,35 +8,34 @@ namespace Emby.Server.Implementations.ScheduledTasks
///
/// Represents a task trigger that fires on a weekly basis.
///
- public class WeeklyTrigger : ITaskTrigger
+ public sealed class WeeklyTrigger : ITaskTrigger
{
+ private readonly TimeSpan _timeOfDay;
+ private readonly DayOfWeek _dayOfWeek;
+ private Timer? _timer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The time of day to trigger the task to run.
+ /// The day of week.
+ /// The options of this task.
+ public WeeklyTrigger(TimeSpan timeofDay, DayOfWeek dayOfWeek, TaskOptions taskOptions)
+ {
+ _timeOfDay = timeofDay;
+ _dayOfWeek = dayOfWeek;
+ TaskOptions = taskOptions;
+ }
+
///
/// Occurs when [triggered].
///
- public event EventHandler Triggered;
+ public event EventHandler? Triggered;
///
- /// Gets or sets the time of day to trigger the task to run.
+ /// Gets the options of this task.
///
- /// The time of day.
- public TimeSpan TimeOfDay { get; set; }
-
- ///
- /// Gets or sets the day of week.
- ///
- /// The day of week.
- public DayOfWeek DayOfWeek { get; set; }
-
- ///
- /// Gets or sets the options of this task.
- ///
- public TaskOptions TaskOptions { get; set; }
-
- ///
- /// Gets or sets the timer.
- ///
- /// The timer.
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
///
/// Stars waiting for the trigger action.
@@ -51,7 +50,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
var triggerDate = GetNextTriggerDateTime();
- Timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
}
///
@@ -63,22 +62,22 @@ namespace Emby.Server.Implementations.ScheduledTasks
var now = DateTime.Now;
// If it's on the same day
- if (now.DayOfWeek == DayOfWeek)
+ if (now.DayOfWeek == _dayOfWeek)
{
// It's either later today, or a week from now
- return now.TimeOfDay < TimeOfDay ? now.Date.Add(TimeOfDay) : now.Date.AddDays(7).Add(TimeOfDay);
+ return now.TimeOfDay < _timeOfDay ? now.Date.Add(_timeOfDay) : now.Date.AddDays(7).Add(_timeOfDay);
}
var triggerDate = now.Date;
// Walk the date forward until we get to the trigger day
- while (triggerDate.DayOfWeek != DayOfWeek)
+ while (triggerDate.DayOfWeek != _dayOfWeek)
{
triggerDate = triggerDate.AddDays(1);
}
// Return the trigger date plus the time offset
- return triggerDate.Add(TimeOfDay);
+ return triggerDate.Add(_timeOfDay);
}
///
@@ -94,10 +93,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
///
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
///
diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
index 4bc12f44a..e8eac315b 100644
--- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs
+++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -289,7 +291,7 @@ namespace Emby.Server.Implementations.Security
return result;
}
- private static AuthenticationInfo Get(IReadOnlyList reader)
+ private static AuthenticationInfo Get(IReadOnlyList reader)
{
var info = new AuthenticationInfo
{
@@ -297,50 +299,49 @@ namespace Emby.Server.Implementations.Security
AccessToken = reader[1].ToString()
};
- if (reader[2].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(2, out var deviceId))
{
- info.DeviceId = reader[2].ToString();
+ info.DeviceId = deviceId;
}
- if (reader[3].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(3, out var appName))
{
- info.AppName = reader[3].ToString();
+ info.AppName = appName;
}
- if (reader[4].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(4, out var appVersion))
{
- info.AppVersion = reader[4].ToString();
+ info.AppVersion = appVersion;
}
- if (reader[5].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(6, out var userId))
{
- info.DeviceName = reader[5].ToString();
+ info.UserId = new Guid(userId);
}
- if (reader[6].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(7, out var userName))
{
- info.UserId = new Guid(reader[6].ToString());
- }
-
- if (reader[7].SQLiteType != SQLiteType.Null)
- {
- info.UserName = reader[7].ToString();
+ info.UserName = userName;
}
info.DateCreated = reader[8].ReadDateTime();
- if (reader[9].SQLiteType != SQLiteType.Null)
+ if (reader.TryReadDateTime(9, out var dateLastActivity))
{
- info.DateLastActivity = reader[9].ReadDateTime();
+ info.DateLastActivity = dateLastActivity;
}
else
{
info.DateLastActivity = info.DateCreated;
}
- if (reader[10].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(10, out var customName))
{
- info.DeviceName = reader[10].ToString();
+ info.DeviceName = customName;
+ }
+ else if (reader.TryGetString(5, out var deviceName))
+ {
+ info.DeviceName = deviceName;
}
return info;
@@ -361,9 +362,9 @@ namespace Emby.Server.Implementations.Security
foreach (var row in statement.ExecuteQuery())
{
- if (row[0].SQLiteType != SQLiteType.Null)
+ if (row.TryGetString(0, out var customName))
{
- result.CustomName = row[0].ToString();
+ result.CustomName = customName;
}
}
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 27024e4e1..5ff73de81 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -19,7 +19,10 @@ namespace Emby.Server.Implementations.Serialization
new ConcurrentDictionary();
private static XmlSerializer GetSerializer(Type type)
- => _serializers.GetOrAdd(type.FullName, _ => new XmlSerializer(type));
+ => _serializers.GetOrAdd(
+ type.FullName ?? throw new ArgumentException($"Invalid type {type}."),
+ (_, t) => new XmlSerializer(t),
+ type);
///
/// Serializes to writer.
@@ -38,7 +41,7 @@ namespace Emby.Server.Implementations.Serialization
/// The type.
/// The stream.
/// System.Object.
- public object DeserializeFromStream(Type type, Stream stream)
+ public object? DeserializeFromStream(Type type, Stream stream)
{
using (var reader = XmlReader.Create(stream))
{
@@ -81,7 +84,7 @@ namespace Emby.Server.Implementations.Serialization
/// The type.
/// The file.
/// System.Object.
- public object DeserializeFromFile(Type type, string file)
+ public object? DeserializeFromFile(Type type, string file)
{
using (var stream = File.OpenRead(file))
{
@@ -95,7 +98,7 @@ namespace Emby.Server.Implementations.Serialization
/// The type.
/// The buffer.
/// System.Object.
- public object DeserializeFromBytes(Type type, byte[] buffer)
+ public object? DeserializeFromBytes(Type type, byte[] buffer)
{
using (var stream = new MemoryStream(buffer, 0, buffer.Length, false, true))
{
diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs
index ac589b03c..6cf9a8f71 100644
--- a/Emby.Server.Implementations/ServerApplicationPaths.cs
+++ b/Emby.Server.Implementations/ServerApplicationPaths.cs
@@ -25,6 +25,10 @@ namespace Emby.Server.Implementations
cacheDirectoryPath,
webDirectoryPath)
{
+ // ProgramDataPath cannot change when the server is running, so cache these to avoid allocations.
+ RootFolderPath = Path.Join(ProgramDataPath, "root");
+ DefaultUserViewsPath = Path.Combine(RootFolderPath, "default");
+ DefaultInternalMetadataPath = Path.Combine(ProgramDataPath, "metadata");
InternalMetadataPath = DefaultInternalMetadataPath;
}
@@ -32,13 +36,13 @@ namespace Emby.Server.Implementations
/// Gets the path to the base root media directory.
///
/// The root folder path.
- public string RootFolderPath => Path.Combine(ProgramDataPath, "root");
+ public string RootFolderPath { get; }
///
/// Gets the path to the default user view directory. Used if no specific user view is defined.
///
/// The default user views path.
- public string DefaultUserViewsPath => Path.Combine(RootFolderPath, "default");
+ public string DefaultUserViewsPath { get; }
///
/// Gets the path to the People directory.
@@ -98,7 +102,7 @@ namespace Emby.Server.Implementations
public string UserConfigurationDirectoryPath => Path.Combine(ConfigurationDirectoryPath, "users");
///
- public string DefaultInternalMetadataPath => Path.Combine(ProgramDataPath, "metadata");
+ public string DefaultInternalMetadataPath { get; }
///
public string InternalMetadataPath { get; set; }
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 10e28c33a..62df354fd 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -1475,17 +1477,14 @@ namespace Emby.Server.Implementations.Session
user = _userManager.GetUserById(request.UserId);
}
- if (user == null)
- {
- user = _userManager.GetUserByName(request.Username);
- }
+ user ??= _userManager.GetUserByName(request.Username);
if (enforcePassword)
{
user = await _userManager.AuthenticateUser(
request.Username,
request.Password,
- request.PasswordSha1,
+ null,
request.RemoteEndPoint,
true).ConfigureAwait(false);
}
@@ -1543,23 +1542,26 @@ namespace Emby.Server.Implementations.Session
Limit = 1
}).Items.FirstOrDefault();
- var allExistingForDevice = _authRepo.Get(
- new AuthenticationInfoQuery
- {
- DeviceId = deviceId
- }).Items;
-
- foreach (var auth in allExistingForDevice)
+ if (!string.IsNullOrEmpty(deviceId))
{
- if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
+ var allExistingForDevice = _authRepo.Get(
+ new AuthenticationInfoQuery
+ {
+ DeviceId = deviceId
+ }).Items;
+
+ foreach (var auth in allExistingForDevice)
{
- try
+ if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
{
- Logout(auth);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error while logging out.");
+ try
+ {
+ Logout(auth);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while logging out.");
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 39c369a01..e9e3ca7f4 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index f9c6a13c6..9fa92a53a 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -1,6 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1600
-#nullable enable
using System;
using System.Collections.Generic;
@@ -8,7 +6,6 @@ using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 1f68a9c81..2b0ab536f 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
@@ -131,11 +131,11 @@ namespace Emby.Server.Implementations.Sorting
return GetSpecialCompareValue(x).CompareTo(GetSpecialCompareValue(y));
}
- private static int GetSpecialCompareValue(Episode item)
+ private static long GetSpecialCompareValue(Episode item)
{
// First sort by season number
// Since there are three sort orders, pad with 9 digits (3 for each, figure 1000 episode buffer should be enough)
- var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000;
+ var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000L;
// Second sort order is if it airs after the season
if (item.AirsAfterSeasonNumber.HasValue)
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index 7657cc74e..42e644970 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
}
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Sorting
///
/// The x.
/// System.String.
- private static string GetValue(BaseItem x)
+ private static string? GetValue(BaseItem? x)
{
var audio = x as IHasAlbumArtist;
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
index 7dfdd9ecf..1db3f5e9c 100644
--- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
}
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Sorting
///
/// The x.
/// System.String.
- private static string GetValue(BaseItem x)
+ private static string? GetValue(BaseItem? x)
{
var audio = x as Audio;
diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
index 756d3c5b6..98bee3fd9 100644
--- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
public string Name => ItemSortBy.Artist;
///
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
}
@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting
///
/// The x.
/// System.String.
- private static string GetValue(BaseItem x)
+ private static string? GetValue(BaseItem? x)
{
if (!(x is Audio audio))
{
diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
index 980954ba0..5f142fa4b 100644
--- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
index fa136c36d..d20dedc2d 100644
--- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
@@ -15,14 +15,14 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetValue(x).CompareTo(GetValue(y));
}
- private static float GetValue(BaseItem x)
+ private static float GetValue(BaseItem? x)
{
- return x.CriticRating ?? 0;
+ return x?.CriticRating ?? 0;
}
///
diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
index cbca300d2..d3f10f78c 100644
--- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index 03ff19d21..b1cb123ce 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 16bd2aff8..08a44319f 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
index 0c4e82d01..73e628cf7 100644
--- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
index a35192eff..3c5ddeefa 100644
--- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
@@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetValue(x).CompareTo(GetValue(y));
}
@@ -30,9 +30,9 @@ namespace Emby.Server.Implementations.Sorting
///
/// The x.
/// System.String.
- private static int GetValue(BaseItem x)
+ private static int GetValue(BaseItem? x)
{
- return x.IsFolder ? 0 : 1;
+ return x?.IsFolder ?? true ? 0 : 1;
}
}
}
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
index d95948406..7d77a8bc5 100644
--- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
index 1632c5a7a..926835f90 100644
--- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
index da020d8d8..4de81a69e 100644
--- a/Emby.Server.Implementations/Sorting/NameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
index 76bb798b5..a81f78ebf 100644
--- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
@@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index 5c2830322..04e4865cb 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
index 92ac04dc6..c98f97bf1 100644
--- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetDate(x).CompareTo(GetDate(y));
}
@@ -26,8 +26,13 @@ namespace Emby.Server.Implementations.Sorting
///
/// The x.
/// DateTime.
- private static DateTime GetDate(BaseItem x)
+ private static DateTime GetDate(BaseItem? x)
{
+ if (x == null)
+ {
+ return DateTime.MinValue;
+ }
+
if (x.PremiereDate.HasValue)
{
return x.PremiereDate.Value;
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
index e2857df0b..df9f9957d 100644
--- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetValue(x).CompareTo(GetValue(y));
}
@@ -25,8 +25,13 @@ namespace Emby.Server.Implementations.Sorting
///
/// The x.
/// DateTime.
- private static int GetValue(BaseItem x)
+ private static int GetValue(BaseItem? x)
{
+ if (x == null)
+ {
+ return 0;
+ }
+
if (x.ProductionYear.HasValue)
{
return x.ProductionYear.Value;
diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs
index 7739d0418..af3bc2750 100644
--- a/Emby.Server.Implementations/Sorting/RandomComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// The x.
/// The y.
/// System.Int32.
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return Guid.NewGuid().CompareTo(Guid.NewGuid());
}
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index dde44333d..129315303 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
index b9205ee07..4123a59f8 100644
--- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index f745e193b..8d30716d3 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
index 558a3d351..c3df7c47e 100644
--- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 5766dc542..01445c525 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs
index 7c2ad2477..12efff261 100644
--- a/Emby.Server.Implementations/SyncPlay/Group.cs
+++ b/Emby.Server.Implementations/SyncPlay/Group.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index aee959c53..993456196 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -87,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
_sessionManager = sessionManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger();
- _sessionManager.SessionControllerConnected += OnSessionControllerConnected;
+ _sessionManager.SessionEnded += OnSessionEnded;
}
///
@@ -269,14 +271,17 @@ namespace Emby.Server.Implementations.SyncPlay
var user = _userManager.GetUserById(session.UserId);
List list = new List();
- foreach (var group in _groups.Values)
+ lock (_groupsLock)
{
- // Locking required as group is not thread-safe.
- lock (group)
+ foreach (var (_, group) in _groups)
{
- if (group.HasAccessToPlayQueue(user))
+ // Locking required as group is not thread-safe.
+ lock (group)
{
- list.Add(group.GetInfo());
+ if (group.HasAccessToPlayQueue(user))
+ {
+ list.Add(group.GetInfo());
+ }
}
}
}
@@ -352,18 +357,18 @@ namespace Emby.Server.Implementations.SyncPlay
return;
}
- _sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
+ _sessionManager.SessionEnded -= OnSessionEnded;
_disposed = true;
}
- private void OnSessionControllerConnected(object sender, SessionEventArgs e)
+ private void OnSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
if (_sessionToGroupMap.TryGetValue(session.Id, out var group))
{
- var request = new JoinGroupRequest(group.GroupId);
- JoinGroup(session, request, CancellationToken.None);
+ var leaveGroupRequest = new LeaveGroupRequest();
+ LeaveGroup(session, leaveGroupRequest, CancellationToken.None);
}
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 839b62448..af453d148 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -1,8 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -11,7 +12,6 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Series = MediaBrowser.Controller.Entities.TV.Series;
@@ -23,12 +23,14 @@ namespace Emby.Server.Implementations.TV
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _configurationManager;
- public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager)
+ public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager)
{
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
+ _configurationManager = configurationManager;
}
public QueryResult GetNextUp(NextUpQuery request, DtoOptions dtoOptions)
@@ -43,9 +45,7 @@ namespace Emby.Server.Implementations.TV
string presentationUniqueKey = null;
if (!string.IsNullOrEmpty(request.SeriesId))
{
- var series = _libraryManager.GetItemById(request.SeriesId) as Series;
-
- if (series != null)
+ if (_libraryManager.GetItemById(request.SeriesId) is Series series)
{
presentationUniqueKey = GetUniqueSeriesKey(series);
}
@@ -95,9 +95,7 @@ namespace Emby.Server.Implementations.TV
int? limit = null;
if (!string.IsNullOrEmpty(request.SeriesId))
{
- var series = _libraryManager.GetItemById(request.SeriesId) as Series;
-
- if (series != null)
+ if (_libraryManager.GetItemById(request.SeriesId) is Series series)
{
presentationUniqueKey = GetUniqueSeriesKey(series);
limit = 1;
@@ -156,7 +154,7 @@ namespace Emby.Server.Implementations.TV
return i.Item1 != DateTime.MinValue;
}
- if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue)
+ if (alwaysEnableFirstEpisode || (i.Item1 != DateTime.MinValue && i.Item1.Date >= request.NextUpDateCutoff))
{
anyFound = true;
return true;
@@ -200,13 +198,10 @@ namespace Emby.Server.Implementations.TV
ParentIndexNumberNotEquals = 0,
DtoOptions = new DtoOptions
{
- Fields = new ItemFields[]
- {
- ItemFields.SortName
- },
+ Fields = new[] { ItemFields.SortName },
EnableImages = false
}
- }).FirstOrDefault();
+ }).Cast().FirstOrDefault();
Func getEpisode = () =>
{
@@ -224,6 +219,43 @@ namespace Emby.Server.Implementations.TV
DtoOptions = dtoOptions
}).Cast().FirstOrDefault();
+ if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
+ {
+ var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ AncestorWithPresentationUniqueKey = null,
+ SeriesPresentationUniqueKey = seriesKey,
+ ParentIndexNumber = 0,
+ IncludeItemTypes = new[] { nameof(Episode) },
+ IsPlayed = false,
+ IsVirtualItem = false,
+ DtoOptions = dtoOptions
+ })
+ .Cast()
+ .Where(episode => episode.AirsBeforeSeasonNumber != null || episode.AirsAfterSeasonNumber != null)
+ .ToList();
+
+ if (lastWatchedEpisode != null)
+ {
+ // Last watched episode is added, because there could be specials that aired before the last watched episode
+ consideredEpisodes.Add(lastWatchedEpisode);
+ }
+
+ if (nextEpisode != null)
+ {
+ consideredEpisodes.Add(nextEpisode);
+ }
+
+ var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
+ .Cast();
+ if (lastWatchedEpisode != null)
+ {
+ sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => episode.Id != lastWatchedEpisode.Id).Skip(1);
+ }
+
+ nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
+ }
+
if (nextEpisode != null)
{
var userData = _userDataManager.GetUserData(user, nextEpisode);
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 4fd7ac0c1..8179e26c5 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -17,6 +17,11 @@ namespace Emby.Server.Implementations.Udp
///
public sealed class UdpServer : IDisposable
{
+ ///
+ /// Address Override Configuration Key.
+ ///
+ public const string AddressOverrideConfigKey = "PublishedServerUrl";
+
///
/// The _logger.
///
@@ -24,11 +29,6 @@ namespace Emby.Server.Implementations.Udp
private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _config;
- ///
- /// Address Override Configuration Key.
- ///
- public const string AddressOverrideConfigKey = "PublishedServerUrl";
-
private Socket _udpSocket;
private IPEndPoint _endpoint;
private readonly byte[] _receiveBuffer = new byte[8192];
@@ -38,54 +38,58 @@ namespace Emby.Server.Implementations.Udp
///
/// Initializes a new instance of the class.
///
- public UdpServer(ILogger logger, IServerApplicationHost appHost, IConfiguration configuration)
+ /// The logger.
+ /// The application host.
+ /// The configuration manager.
+ /// The port.
+ public UdpServer(
+ ILogger logger,
+ IServerApplicationHost appHost,
+ IConfiguration configuration,
+ int port)
{
_logger = logger;
_appHost = appHost;
_config = configuration;
+
+ _endpoint = new IPEndPoint(IPAddress.Any, port);
+
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
{
- string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey])
- ? _config[AddressOverrideConfigKey]
- : _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
-
- if (!string.IsNullOrEmpty(localUrl))
+ string? localUrl = _config[AddressOverrideConfigKey];
+ if (string.IsNullOrEmpty(localUrl))
{
- var response = new ServerDiscoveryInfo
- {
- Address = localUrl,
- Id = _appHost.SystemId,
- Name = _appHost.FriendlyName
- };
-
- try
- {
- await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
- }
- catch (SocketException ex)
- {
- _logger.LogError(ex, "Error sending response message");
- }
+ localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
}
- else
+
+ if (string.IsNullOrEmpty(localUrl))
{
_logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined.");
+ return;
+ }
+
+ var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
+
+ try
+ {
+ await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
+ }
+ catch (SocketException ex)
+ {
+ _logger.LogError(ex, "Error sending response message");
}
}
///
/// Starts the specified port.
///
- /// The port.
- ///
- public void Start(int port, CancellationToken cancellationToken)
+ /// The cancellation token to cancel operation.
+ public void Start(CancellationToken cancellationToken)
{
- _endpoint = new IPEndPoint(IPAddress.Any, port);
-
- _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
- _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_udpSocket.Bind(_endpoint);
_ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
@@ -93,9 +97,9 @@ namespace Emby.Server.Implementations.Udp
private async Task BeginReceiveAsync(CancellationToken cancellationToken)
{
+ var infiniteTask = Task.Delay(-1, cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
- var infiniteTask = Task.Delay(-1, cancellationToken);
try
{
var task = _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint);
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index abcb4313f..b0921cbd8 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -22,6 +20,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
@@ -92,7 +91,7 @@ namespace Emby.Server.Implementations.Updates
_httpClientFactory = httpClientFactory;
_config = config;
_zipClient = zipClient;
- _jsonSerializerOptions = JsonDefaults.GetOptions();
+ _jsonSerializerOptions = JsonDefaults.Options;
_pluginManager = pluginManager;
}
@@ -100,12 +99,12 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable CompletedInstallations => _completedInstallationsInternal;
///
- public async Task> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
+ public async Task GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
{
try
{
- List? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
- .GetFromJsonAsync>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
+ PackageInfo[]? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetFromJsonAsync(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
if (packages == null)
{
@@ -178,31 +177,20 @@ namespace Emby.Server.Implementations.Updates
// Where repositories have the same content, the details from the first is taken.
foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
{
- if (!Guid.TryParse(package.Id, out var packageGuid))
- {
- // Package doesn't have a valid GUID, skip.
- continue;
- }
-
- var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault();
+ var existing = FilterPackages(result, package.Name, package.Id).FirstOrDefault();
// Remove invalid versions from the valid package.
for (var i = package.Versions.Count - 1; i >= 0; i--)
{
var version = package.Versions[i];
- var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
- // Update the manifests, if anything changes.
+ var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
if (plugin != null)
{
- if (!string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal))
- {
- plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
- _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
- }
+ await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
}
- // Remove versions with a target abi that is greater then the current application version.
+ // Remove versions with a target ABI greater then the current application version.
if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
{
package.Versions.RemoveAt(i);
@@ -235,7 +223,7 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable FilterPackages(
IEnumerable availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? specificVersion = null)
{
if (name != null)
@@ -243,9 +231,9 @@ namespace Emby.Server.Implementations.Updates
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
- if (id != Guid.Empty)
+ if (id != default)
{
- availablePackages = availablePackages.Where(x => Guid.Parse(x.Id) == id);
+ availablePackages = availablePackages.Where(x => x.Id == id);
}
if (specificVersion != null)
@@ -260,7 +248,7 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable GetCompatibleVersions(
IEnumerable availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? minVersion = null,
Version? specificVersion = null)
{
@@ -290,11 +278,12 @@ namespace Emby.Server.Implementations.Updates
yield return new InstallationInfo
{
Changelog = v.Changelog,
- Id = new Guid(package.Id),
+ Id = package.Id,
Name = package.Name,
Version = v.VersionNumber,
SourceUrl = v.SourceUrl,
- Checksum = v.Checksum
+ Checksum = v.Checksum,
+ PackageInfo = package
};
}
}
@@ -504,7 +493,8 @@ namespace Emby.Server.Implementations.Updates
var plugins = _pluginManager.Plugins;
foreach (var plugin in plugins)
{
- if (plugin.Manifest?.AutoUpdate == false)
+ // Don't auto update when plugin marked not to, or when it's disabled.
+ if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled)
{
continue;
}
@@ -519,7 +509,7 @@ namespace Emby.Server.Implementations.Updates
}
}
- private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
+ private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
{
var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
@@ -571,24 +561,16 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
+ await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
_pluginManager.ImportPluginFrom(targetDir);
}
private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
- // Set last update time if we were installed before
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
- if (plugin != null)
- {
- plugin.Manifest.Timestamp = DateTime.UtcNow;
- _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
- }
- // Do the install
- await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
-
- // Do plugin-specific processing
+ await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false);
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
return plugin != null;
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index 7d68aecf9..392498c53 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -77,8 +77,9 @@ namespace Jellyfin.Api.Auth
return false;
}
- var ip = _httpContextAccessor.HttpContext.GetNormalizedRemoteIp();
- var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
+ var isInLocalNetwork = _httpContextAccessor.HttpContext != null
+ && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp());
+
// User cannot access remotely and user is remote
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
{
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 4b2e5e7ea..154a56702 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -77,6 +77,8 @@ namespace Jellyfin.Api.Controllers
/// Optional filter by items whose name is sorted equally or greater than a given input string.
/// Optional filter by items whose name is sorted equally than a given input string.
/// Optional filter by items whose name is equally or lesser than a given input string.
+ /// Optional. Specify one or more sort orders, comma delimited.
+ /// Sort Order - Ascending,Descending.
/// Optional, include image information in output.
/// Total record count.
/// Artists returned.
@@ -112,6 +114,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -150,7 +154,8 @@ namespace Jellyfin.Api.Controllers
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
};
if (parentId.HasValue)
@@ -276,6 +281,8 @@ namespace Jellyfin.Api.Controllers
/// Optional filter by items whose name is sorted equally or greater than a given input string.
/// Optional filter by items whose name is sorted equally than a given input string.
/// Optional filter by items whose name is equally or lesser than a given input string.
+ /// Optional. Specify one or more sort orders, comma delimited.
+ /// Sort Order - Ascending,Descending.
/// Optional, include image information in output.
/// Total record count.
/// Album artists returned.
@@ -311,6 +318,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -349,7 +358,8 @@ namespace Jellyfin.Api.Controllers
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
};
if (parentId.HasValue)
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 616fe5b91..a6e70e72d 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -168,22 +168,22 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
@@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -309,7 +309,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -333,22 +333,22 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index e1c9f69f6..b6309baab 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,3 +1,4 @@
+using System;
using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using System.Text.Json;
@@ -25,7 +26,7 @@ namespace Jellyfin.Api.Controllers
private readonly IServerConfigurationManager _configurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
///
/// Initializes a new instance of the class.
@@ -94,6 +95,11 @@ namespace Jellyfin.Api.Controllers
{
var configurationType = _configurationManager.GetConfigurationType(key);
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
+ if (configuration == null)
+ {
+ throw new ArgumentException("Body doesn't contain a valid configuration");
+ }
+
_configurationManager.SaveConfiguration(key, configuration);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index b2baa9cea..445733c24 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -6,7 +6,6 @@ using System.Net.Mime;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Models;
using MediaBrowser.Common.Plugins;
-using MediaBrowser.Controller;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
using Microsoft.AspNetCore.Http;
@@ -22,22 +21,18 @@ namespace Jellyfin.Api.Controllers
public class DashboardController : BaseJellyfinApiController
{
private readonly ILogger _logger;
- private readonly IServerApplicationHost _appHost;
private readonly IPluginManager _pluginManager;
///
/// Initializes a new instance of the class.
///
/// Instance of interface.
- /// Instance of interface.
/// Instance of interface.
public DashboardController(
ILogger logger,
- IServerApplicationHost appHost,
IPluginManager pluginManager)
{
_logger = logger;
- _appHost = appHost;
_pluginManager = pluginManager;
}
@@ -51,7 +46,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("web/ConfigurationPages")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult> GetConfigurationPages(
+ public ActionResult> GetConfigurationPages(
[FromQuery] bool? enableInMainMenu)
{
var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
@@ -77,38 +72,22 @@ namespace Jellyfin.Api.Controllers
[ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
{
- IPlugin? plugin = null;
- Stream? stream = null;
-
- var isJs = false;
- var isTemplate = false;
-
var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
- if (altPage != null)
+ if (altPage == null)
{
- plugin = altPage.Item2;
- stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
-
- isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
- isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
+ return NotFound();
}
- if (plugin != null && stream != null)
+ IPlugin plugin = altPage.Item2;
+ string resourcePath = altPage.Item1.EmbeddedResourcePath;
+ Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath);
+ if (stream == null)
{
- if (isJs)
- {
- return File(stream, MimeTypes.GetMimeType("page.js"));
- }
-
- if (isTemplate)
- {
- return File(stream, MimeTypes.GetMimeType("page.html"));
- }
-
- return File(stream, MimeTypes.GetMimeType("page.html"));
+ _logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name);
+ return NotFound();
}
- return NotFound();
+ return File(stream, MimeTypes.GetMimeType(resourcePath));
}
private IEnumerable GetConfigPages(LocalPlugin plugin)
@@ -116,11 +95,11 @@ namespace Jellyfin.Api.Controllers
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
}
- private IEnumerable> GetPluginPages(LocalPlugin? plugin)
+ private IEnumerable> GetPluginPages(LocalPlugin plugin)
{
- if (plugin?.Instance is not IHasWebPages hasWebPages)
+ if (plugin.Instance is not IHasWebPages hasWebPages)
{
- return new List>();
+ return Enumerable.Empty>();
}
return hasWebPages.GetPages().Select(i => new Tuple(i, plugin.Instance));
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index e375645cf..62283d038 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -28,7 +28,6 @@ using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@@ -52,8 +51,6 @@ namespace Jellyfin.Api.Controllers
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ILogger _logger;
@@ -72,12 +69,11 @@ namespace Jellyfin.Api.Controllers
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
/// Instance of the interface.
/// Instance of the class.
/// Instance of the interface.
/// Instance of .
+ /// Instance of .
public DynamicHlsController(
ILibraryManager libraryManager,
IUserManager userManager,
@@ -87,15 +83,12 @@ namespace Jellyfin.Api.Controllers
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
ILogger logger,
- DynamicHlsHelper dynamicHlsHelper)
+ DynamicHlsHelper dynamicHlsHelper,
+ EncodingHelper encodingHelper)
{
- _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
-
_libraryManager = libraryManager;
_userManager = userManager;
_dlnaManager = dlnaManager;
@@ -104,12 +97,12 @@ namespace Jellyfin.Api.Controllers
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
+ _encodingHelper = encodingHelper;
+
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
}
@@ -203,7 +196,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -218,14 +211,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
var streamingRequest = new HlsVideoRequestDto
{
Id = itemId,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -249,28 +242,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
@@ -370,7 +363,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -385,14 +378,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
var streamingRequest = new HlsAudioRequestDto
{
Id = itemId,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -416,28 +409,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
@@ -533,7 +526,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -548,14 +541,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions)
{
- var cancellationTokenSource = new CancellationTokenSource();
+ using var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
{
Id = itemId,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -579,28 +572,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -698,7 +691,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -713,14 +706,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions)
{
- var cancellationTokenSource = new CancellationTokenSource();
+ using var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new StreamingRequestDto
{
Id = itemId,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -744,28 +737,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -868,7 +861,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -883,14 +876,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions)
{
var streamingRequest = new VideoRequestDto
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -914,28 +907,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -1040,7 +1033,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -1055,14 +1048,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions)
{
var streamingRequest = new StreamingRequestDto
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -1086,28 +1079,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -1126,9 +1119,7 @@ namespace Jellyfin.Api.Controllers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
@@ -1146,7 +1137,7 @@ namespace Jellyfin.Api.Controllers
var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase);
var hlsVersion = isHlsInFmp4 ? "7" : "3";
- var builder = new StringBuilder();
+ var builder = new StringBuilder(128);
builder.AppendLine("#EXTM3U")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
@@ -1199,6 +1190,7 @@ namespace Jellyfin.Api.Controllers
throw new ArgumentException("StartTimeTicks is not allowed.");
}
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
@@ -1211,14 +1203,12 @@ namespace Jellyfin.Api.Controllers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
TranscodingJobType,
- cancellationTokenSource.Token)
+ cancellationToken)
.ConfigureAwait(false);
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
@@ -1237,7 +1227,7 @@ namespace Jellyfin.Api.Controllers
}
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
var released = false;
var startTranscoding = false;
@@ -1333,24 +1323,28 @@ namespace Jellyfin.Api.Controllers
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
- private double[] GetSegmentLengths(StreamState state)
+ private static double[] GetSegmentLengths(StreamState state)
+ => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength);
+
+ internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength)
{
- var result = new List();
+ var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks;
+ var wholeSegments = runtimeTicks / segmentLengthTicks;
+ var remainingTicks = runtimeTicks % segmentLengthTicks;
- var ticks = state.RunTimeTicks ?? 0;
-
- var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
-
- while (ticks > 0)
+ var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1);
+ var segments = new double[segmentsLen];
+ for (int i = 0; i < wholeSegments; i++)
{
- var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
-
- result.Add(TimeSpan.FromTicks(length).TotalSeconds);
-
- ticks -= length;
+ segments[i] = segmentlength;
}
- return result.ToArray();
+ if (remainingTicks != 0)
+ {
+ segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds;
+ }
+
+ return segments;
}
private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber)
@@ -1386,18 +1380,13 @@ namespace Jellyfin.Api.Controllers
}
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
{
- var outputFmp4HeaderArg = string.Empty;
- var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- if (isWindows)
+ var outputFmp4HeaderArg = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) switch
{
// on Windows, the path of fmp4 header file needs to be configured
- outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
- }
- else
- {
+ true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"",
// on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
- outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
- }
+ false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
+ };
segmentFormat = "fmp4" + outputFmp4HeaderArg;
}
@@ -1772,9 +1761,9 @@ namespace Jellyfin.Api.Controllers
private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
{
- var folder = Path.GetDirectoryName(playlist);
+ var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist));
- var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
+ var filePrefix = Path.GetFileNameWithoutExtension(playlist);
try
{
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 7bcf4674c..5aa457153 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -63,6 +63,8 @@ namespace Jellyfin.Api.Controllers
/// Optional filter by items whose name is sorted equally or greater than a given input string.
/// Optional filter by items whose name is sorted equally than a given input string.
/// Optional filter by items whose name is equally or lesser than a given input string.
+ /// Optional. Specify one or more sort orders, comma delimited.
+ /// Sort Order - Ascending,Descending.
/// Optional, include image information in output.
/// Optional. Include total record count.
/// Genres returned.
@@ -84,6 +86,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -107,7 +111,8 @@ namespace Jellyfin.Api.Controllers
NameStartsWithOrGreater = nameStartsWithOrGreater,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
};
if (parentId.HasValue)
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 25abe73ed..473bdc523 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -61,7 +61,13 @@ namespace Jellyfin.Api.Controllers
{
// TODO: Deprecate with new iOS app
var file = segmentId + Path.GetExtension(Request.Path);
- file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
+ var transcodePath = _serverConfigurationManager.GetTranscodePath();
+ file = Path.GetFullPath(Path.Combine(transcodePath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath))
+ {
+ return BadRequest("Invalid segment.");
+ }
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
}
@@ -81,7 +87,13 @@ namespace Jellyfin.Api.Controllers
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
var file = playlistId + Path.GetExtension(Request.Path);
- file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
+ var transcodePath = _serverConfigurationManager.GetTranscodePath();
+ file = Path.GetFullPath(Path.Combine(transcodePath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8")
+ {
+ return BadRequest("Invalid segment.");
+ }
return GetFileResult(file, file);
}
@@ -96,7 +108,9 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Videos/ActiveEncodings")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId)
+ public ActionResult StopEncodingProcess(
+ [FromQuery, Required] string deviceId,
+ [FromQuery, Required] string playSessionId)
{
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
return NoContent();
@@ -128,7 +142,12 @@ namespace Jellyfin.Api.Controllers
var file = segmentId + Path.GetExtension(Request.Path);
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
- file = Path.Combine(transcodeFolderPath, file);
+ file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath))
+ {
+ return BadRequest("Invalid segment.");
+ }
var normalizedPlaylistId = playlistId;
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index 198dbc51f..e1b808098 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers
: type;
var path = BaseItem.SupportedImageExtensions
- .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i))
+ .Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i)))
.FirstOrDefault(System.IO.File.Exists);
if (path == null)
@@ -82,6 +82,11 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
+ if (!path.StartsWith(_applicationPaths.GeneralPath))
+ {
+ return BadRequest("Invalid image path.");
+ }
+
var contentType = MimeTypes.GetMimeType(path);
return File(System.IO.File.OpenRead(path), contentType);
}
@@ -163,7 +168,8 @@ namespace Jellyfin.Api.Controllers
/// A containing the image contents on success, or a if the image could not be found.
private ActionResult GetImageFile(string basePath, string theme, string? name)
{
- var themeFolder = Path.Combine(basePath, theme);
+ var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme));
+
if (Directory.Exists(themeFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
@@ -171,12 +177,18 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
+ if (!path.StartsWith(basePath))
+ {
+ return BadRequest("Invalid image path.");
+ }
+
var contentType = MimeTypes.GetMimeType(path);
+
return PhysicalFile(path, contentType);
}
}
- var allFolder = Path.Combine(basePath, "all");
+ var allFolder = Path.GetFullPath(Path.Combine(basePath, "all"));
if (Directory.Exists(allFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
@@ -184,6 +196,11 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
+ if (!path.StartsWith(basePath))
+ {
+ return BadRequest("Invalid image path.");
+ }
+
var contentType = MimeTypes.GetMimeType(path);
return PhysicalFile(path, contentType);
}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index dc3634970..8f7500ac6 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@@ -196,6 +196,11 @@ namespace Jellyfin.Api.Controllers
}
var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage == null)
+ {
+ return NoContent();
+ }
+
try
{
System.IO.File.Delete(user.ProfileImage.Path);
@@ -235,6 +240,11 @@ namespace Jellyfin.Api.Controllers
}
var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage == null)
+ {
+ return NoContent();
+ }
+
try
{
System.IO.File.Delete(user.ProfileImage.Path);
@@ -392,7 +402,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
- [FromQuery] int newIndex)
+ [FromQuery, Required] int newIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -480,6 +490,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Supply the cache tag from the item object to receive strong caching headers.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. The of the returned image.
@@ -509,8 +521,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
[FromQuery] string? tag,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] double? percentPlayed,
@@ -539,7 +553,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -560,6 +575,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Supply the cache tag from the item object to receive strong caching headers.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. The of the returned image.
@@ -589,8 +606,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
[FromQuery] string? tag,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] double? percentPlayed,
@@ -618,7 +637,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -638,6 +658,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Supply the cache tag from the item object to receive strong caching headers.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Determines the output format of the image - original,gif,jpg,png.
@@ -667,8 +689,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
[FromRoute, Required] string tag,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromRoute, Required] ImageFormat format,
[FromQuery] bool? addPlayedIndicator,
[FromRoute, Required] double percentPlayed,
@@ -697,7 +721,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -721,6 +746,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -741,7 +768,7 @@ namespace Jellyfin.Api.Controllers
public async Task GetArtistImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -750,7 +777,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -776,7 +805,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -800,6 +830,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -820,7 +852,7 @@ namespace Jellyfin.Api.Controllers
public async Task GetGenreImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -829,7 +861,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -855,7 +889,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -880,6 +915,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -900,7 +937,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -909,7 +946,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -934,7 +973,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -958,6 +998,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -978,7 +1020,7 @@ namespace Jellyfin.Api.Controllers
public async Task GetMusicGenreImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -987,7 +1029,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1013,7 +1057,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1038,6 +1083,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -1058,7 +1105,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1067,7 +1114,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1092,7 +1141,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1116,6 +1166,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -1136,7 +1188,7 @@ namespace Jellyfin.Api.Controllers
public async Task GetPersonImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1145,7 +1197,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1171,7 +1225,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1196,6 +1251,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -1216,7 +1273,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1225,7 +1282,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1250,7 +1309,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1274,6 +1334,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -1303,7 +1365,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1329,7 +1393,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1354,6 +1419,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -1383,7 +1450,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1408,7 +1477,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1432,6 +1502,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -1461,7 +1533,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1469,7 +1543,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageIndex)
{
var user = _userManager.GetUserById(userId);
- if (user == null)
+ if (user?.ProfileImage == null)
{
return NotFound();
}
@@ -1504,7 +1578,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1530,6 +1605,8 @@ namespace Jellyfin.Api.Controllers
/// The fixed image width to return.
/// The fixed image height to return.
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
+ /// Width of box to fill.
+ /// Height of box to fill.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
/// Optional. Add a played indicator.
/// Optional. Blur image.
@@ -1559,7 +1636,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1601,7 +1680,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1685,7 +1765,8 @@ namespace Jellyfin.Api.Controllers
int? width,
int? height,
int? quality,
- bool? cropWhitespace,
+ int? fillWidth,
+ int? fillHeight,
bool? addPlayedIndicator,
int? blur,
string? backgroundColor,
@@ -1727,8 +1808,6 @@ namespace Jellyfin.Api.Controllers
}
}
- cropWhitespace ??= imageType == ImageType.Logo || imageType == ImageType.Art;
-
var outputFormats = GetOutputFormats(format);
TimeSpan? cacheDuration = null;
@@ -1748,11 +1827,13 @@ namespace Jellyfin.Api.Controllers
item,
itemId,
imageIndex,
- height,
- maxHeight,
- maxWidth,
- quality,
width,
+ height,
+ maxWidth,
+ maxHeight,
+ fillWidth,
+ fillHeight,
+ quality,
addPlayedIndicator,
percentPlayed,
unplayedCount,
@@ -1760,7 +1841,6 @@ namespace Jellyfin.Api.Controllers
backgroundColor,
foregroundLayer,
imageInfo,
- cropWhitespace.Value,
outputFormats,
cacheDuration,
responseHeaders,
@@ -1779,17 +1859,15 @@ namespace Jellyfin.Api.Controllers
private ImageFormat[] GetClientSupportedFormats()
{
- var acceptTypes = Request.Headers[HeaderNames.Accept];
- var supportedFormats = new List();
- if (acceptTypes.Count > 0)
+ var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
+ for (var i = 0; i < supportedFormats.Length; i++)
{
- foreach (var type in acceptTypes)
+ // Remove charsets etc. (anything after semi-colon)
+ var type = supportedFormats[i];
+ int index = type.IndexOf(';', StringComparison.Ordinal);
+ if (index != -1)
{
- int index = type.IndexOf(';', StringComparison.Ordinal);
- if (index != -1)
- {
- supportedFormats.Add(type.Substring(0, index));
- }
+ supportedFormats[i] = type.Substring(0, index);
}
}
@@ -1847,11 +1925,13 @@ namespace Jellyfin.Api.Controllers
BaseItem? item,
Guid itemId,
int? index,
- int? height,
- int? maxHeight,
- int? maxWidth,
- int? quality,
int? width,
+ int? height,
+ int? maxWidth,
+ int? maxHeight,
+ int? fillWidth,
+ int? fillHeight,
+ int? quality,
bool? addPlayedIndicator,
double? percentPlayed,
int? unplayedCount,
@@ -1859,7 +1939,6 @@ namespace Jellyfin.Api.Controllers
string? backgroundColor,
string? foregroundLayer,
ItemImageInfo imageInfo,
- bool cropWhitespace,
IReadOnlyCollection supportedFormats,
TimeSpan? cacheDuration,
IDictionary headers,
@@ -1872,7 +1951,6 @@ namespace Jellyfin.Api.Controllers
var options = new ImageProcessingOptions
{
- CropWhiteSpace = cropWhitespace,
Height = height,
ImageIndex = index ?? 0,
Image = imageInfo,
@@ -1880,6 +1958,8 @@ namespace Jellyfin.Api.Controllers
ItemId = itemId,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
+ FillHeight = fillHeight,
+ FillWidth = fillWidth,
Quality = quality ?? 100,
Width = width,
AddPlayedIndicator = addPlayedIndicator ?? false,
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index f061755c3..f232dffaa 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers
}
///
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given album.
///
/// The item id.
/// Optional. Filter by user id, and attach user data.
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
}
///
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given playlist.
///
/// The item id.
/// Optional. Filter by user id, and attach user data.
@@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
}
///
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given genre.
///
/// The genre name.
/// Optional. Filter by user id, and attach user data.
@@ -172,7 +172,7 @@ namespace Jellyfin.Api.Controllers
/// A with the playlist items.
[HttpGet("MusicGenres/{name}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> GetInstantMixFromMusicGenre(
+ public ActionResult> GetInstantMixFromMusicGenreByName(
[FromRoute, Required] string name,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
}
///
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given artist.
///
/// The item id.
/// Optional. Filter by user id, and attach user data.
@@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers
}
///
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given genre.
///
/// The item id.
/// Optional. Filter by user id, and attach user data.
@@ -243,7 +243,7 @@ namespace Jellyfin.Api.Controllers
/// A with the playlist items.
[HttpGet("MusicGenres/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> GetInstantMixFromMusicGenres(
+ public ActionResult> GetInstantMixFromMusicGenreById(
[FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
@@ -265,7 +265,7 @@ namespace Jellyfin.Api.Controllers
}
///
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given item.
///
/// The item id.
/// Optional. Filter by user id, and attach user data.
@@ -300,6 +300,80 @@ namespace Jellyfin.Api.Controllers
return GetResult(items, user, limit, dtoOptions);
}
+ ///
+ /// Creates an instant playlist based on a given artist.
+ ///
+ /// The item id.
+ /// Optional. Filter by user id, and attach user data.
+ /// Optional. The maximum number of records to return.
+ /// Optional. Specify additional fields of information to return in the output.
+ /// Optional. Include image information in output.
+ /// Optional. Include user data.
+ /// Optional. The max number of images to return, per image type.
+ /// Optional. The image types to include in the output.
+ /// Instant playlist returned.
+ /// A with the playlist items.
+ [HttpGet("Artists/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetInstantMixFromArtists")]
+ public ActionResult> GetInstantMixFromArtists2(
+ [FromQuery, Required] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ {
+ return GetInstantMixFromArtists(
+ id,
+ userId,
+ limit,
+ fields,
+ enableImages,
+ enableUserData,
+ imageTypeLimit,
+ enableImageTypes);
+ }
+
+ ///
+ /// Creates an instant playlist based on a given genre.
+ ///
+ /// The item id.
+ /// Optional. Filter by user id, and attach user data.
+ /// Optional. The maximum number of records to return.
+ /// Optional. Specify additional fields of information to return in the output.
+ /// Optional. Include image information in output.
+ /// Optional. Include user data.
+ /// Optional. The max number of images to return, per image type.
+ /// Optional. The image types to include in the output.
+ /// Instant playlist returned.
+ /// A with the playlist items.
+ [HttpGet("MusicGenres/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetInstantMixFromMusicGenres instead")]
+ public ActionResult> GetInstantMixFromMusicGenreById2(
+ [FromQuery, Required] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ {
+ return GetInstantMixFromMusicGenreById(
+ id,
+ userId,
+ limit,
+ fields,
+ enableImages,
+ enableUserData,
+ imageTypeLimit,
+ enableImageTypes);
+ }
+
private QueryResult GetResult(List items, User? user, int? limit, DtoOptions dtoOptions)
{
var list = items;
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index dfc68ffce..9fa307858 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -237,48 +237,6 @@ namespace Jellyfin.Api.Controllers
return Ok(results);
}
- ///
- /// Gets a remote image.
- ///
- /// The image url.
- /// The provider name.
- /// Remote image retrieved.
- ///
- /// A that represents the asynchronous operation to get the remote search results.
- /// The task result contains an containing the images file stream.
- ///
- [HttpGet("Items/RemoteSearch/Image")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesImageFile]
- public async Task GetRemoteSearchImage(
- [FromQuery, Required] string imageUrl,
- [FromQuery, Required] string providerName)
- {
- var urlHash = imageUrl.GetMD5();
- var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
- try
- {
- var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
- if (System.IO.File.Exists(contentPath))
- {
- return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath));
- }
- }
- catch (FileNotFoundException)
- {
- // Means the file isn't cached yet
- }
- catch (IOException)
- {
- // Means the file isn't cached yet
- }
-
- await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
- var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
- return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath));
- }
-
///
/// Applies search criteria to an item and refreshes metadata.
///
@@ -320,53 +278,5 @@ namespace Jellyfin.Api.Controllers
return NoContent();
}
-
- ///
- /// Downloads the image.
- ///
- /// Name of the provider.
- /// The URL.
- /// The URL hash.
- /// The pointer cache path.
- /// Task.
- private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
- {
- using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
- if (result.Content.Headers.ContentType?.MediaType == null)
- {
- throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
- }
-
- var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
- var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
- var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
- Directory.CreateDirectory(directory);
- using (var stream = result.Content)
- {
- await using var fileStream = new FileStream(
- fullCachePath,
- FileMode.Create,
- FileAccess.Write,
- FileShare.Read,
- IODefaults.FileStreamBufferSize,
- true);
-
- await stream.CopyToAsync(fileStream).ConfigureAwait(false);
- }
-
- var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
-
- Directory.CreateDirectory(pointerCacheDirectory);
- await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
- }
-
- ///
- /// Gets the full cache path.
- ///
- /// The filename.
- /// System.String.
- private string GetFullCachePath(string filename)
- => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
}
}
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 9e1a39853..a9f4a5a58 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}/ContentType")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType)
+ public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 2c9760f6d..35c27dd0e 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -143,7 +143,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Items")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult> GetItems(
- [FromQuery] Guid? userId,
+ [FromQuery] Guid userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo,
@@ -224,8 +224,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
- var user = userId.HasValue && !userId.Equals(Guid.Empty)
- ? _userManager.GetUserById(userId.Value)
+ var user = !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId)
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
@@ -246,8 +246,13 @@ namespace Jellyfin.Api.Controllers
folder = _libraryManager.GetUserRootFolder();
}
- if (folder is IHasCollectionType hasCollectionType
- && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ string? collectionType = null;
+ if (folder is IHasCollectionType hasCollectionType)
+ {
+ collectionType = hasCollectionType.CollectionType;
+ }
+
+ if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{
recursive = true;
includeItemTypes = new[] { BaseItemKind.Playlist };
@@ -270,10 +275,11 @@ namespace Jellyfin.Api.Controllers
}
}
- if (!(item is UserRootFolder)
+ if (item is not UserRootFolder
&& !isInEnabledFolder
&& !user.HasPermission(PermissionKind.EnableAllFolders)
- && !user.HasPermission(PermissionKind.EnableAllChannels))
+ && !user.HasPermission(PermissionKind.EnableAllChannels)
+ && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index db4aa9668..4ed15e1d5 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -114,7 +114,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path));
+ return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
}
///
@@ -303,7 +303,7 @@ namespace Jellyfin.Api.Controllers
///
/// Library scan started.
/// A .
- [HttpGet("Library/Refresh")]
+ [HttpPost("Library/Refresh")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task RefreshLibrary()
@@ -590,17 +590,17 @@ namespace Jellyfin.Api.Controllers
///
/// Reports that new movies have been added by an external source.
///
- /// A list of updated media paths.
+ /// The update paths.
/// Report success.
/// A .
[HttpPost("Library/Media/Updated")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates)
+ public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
{
- foreach (var item in updates)
+ foreach (var item in dto.Updates)
{
- _libraryMonitor.ReportFileSystemChanged(item.Path);
+ _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null."));
}
return NoContent();
@@ -666,7 +666,7 @@ namespace Jellyfin.Api.Controllers
}
// TODO determine non-ASCII validity.
- return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
+ return PhysicalFile(path, MimeTypes.GetMimeType(path), filename, true);
}
///
@@ -777,7 +777,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult GetLibraryOptionsInfo(
[FromQuery] string? libraryContentType,
- [FromQuery] bool isNewLibrary)
+ [FromQuery] bool isNewLibrary = false)
{
var result = new LibraryOptionsResultDto();
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 94995650c..be9127dd3 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task AddVirtualFolder(
[FromQuery] string? name,
- [FromQuery] string? collectionType,
+ [FromQuery] CollectionTypeOptions? collectionType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
[FromQuery] bool refreshLibrary = false)
@@ -241,23 +241,20 @@ namespace Jellyfin.Api.Controllers
///
/// Updates a media path.
///
- /// The name of the library.
- /// The path info.
+ /// The name of the library and path infos.
/// A .
/// Media path updated.
/// The name of the library may not be empty.
[HttpPost("Paths/Update")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateMediaPath(
- [FromQuery] string? name,
- [FromBody] MediaPathInfo? pathInfo)
+ public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
{
- if (string.IsNullOrWhiteSpace(name))
+ if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
{
- throw new ArgumentNullException(nameof(name));
+ throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
}
- _libraryManager.UpdateMediaPath(name, pathInfo);
+ _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 4d788ad7d..010a3b19a 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -18,6 +18,7 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
@@ -89,7 +90,7 @@ namespace Jellyfin.Api.Controllers
// nameof(LiveTvProgram)
},
// IsMovie = true
- OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) },
Limit = 7,
ParentId = parentIdGuid,
Recursive = true,
@@ -110,7 +111,7 @@ namespace Jellyfin.Api.Controllers
{
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
- OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
Limit = 10,
IsFavoriteOrLiked = true,
ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
@@ -120,7 +121,7 @@ namespace Jellyfin.Api.Controllers
DtoOptions = dtoOptions
});
- var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList();
+ var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6));
// Get recently played directors
var recentDirectors = GetDirectors(mostRecentMovies)
.ToList();
@@ -191,7 +192,8 @@ namespace Jellyfin.Api.Controllers
foreach (var name in names)
{
- var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ var items = _libraryManager.GetItemList(
+ new InternalItemsQuery(user)
{
Person = name,
// Account for duplicates by imdb id, since the database doesn't support this yet
@@ -299,9 +301,8 @@ namespace Jellyfin.Api.Controllers
private IEnumerable GetActors(IEnumerable items)
{
- var people = _libraryManager.GetPeople(new InternalPeopleQuery
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty(), new[] { PersonType.Director })
{
- ExcludePersonTypes = new[] { PersonType.Director },
MaxListOrder = 3
});
@@ -315,10 +316,9 @@ namespace Jellyfin.Api.Controllers
private IEnumerable GetDirectors(IEnumerable items)
{
- var people = _libraryManager.GetPeople(new InternalPeopleQuery
- {
- PersonTypes = new[] { PersonType.Director }
- });
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery(
+ new[] { PersonType.Director },
+ Array.Empty()));
var itemIds = items.Select(i => i.Id).ToList();
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 7f7058b5e..27eec2b9a 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -63,6 +63,8 @@ namespace Jellyfin.Api.Controllers
/// Optional filter by items whose name is sorted equally or greater than a given input string.
/// Optional filter by items whose name is sorted equally than a given input string.
/// Optional filter by items whose name is equally or lesser than a given input string.
+ /// Optional. Specify one or more sort orders, comma delimited.
+ /// Sort Order - Ascending,Descending.
/// Optional, include image information in output.
/// Optional. Include total record count.
/// Music genres returned.
@@ -84,6 +86,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -107,7 +111,8 @@ namespace Jellyfin.Api.Controllers
NameStartsWithOrGreater = nameStartsWithOrGreater,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
};
if (parentId.HasValue)
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 0ceda6815..420630cdf 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using Jellyfin.Api.Constants;
@@ -86,26 +87,19 @@ namespace Jellyfin.Api.Controllers
///
/// Sends a notification to all admins.
///
- /// The URL of the notification.
- /// The level of the notification.
- /// The name of the notification.
- /// The description of the notification.
+ /// The notification request.
/// Notification sent.
/// A .
[HttpPost("Admin")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CreateAdminNotification(
- [FromQuery] string? url,
- [FromQuery] NotificationLevel? level,
- [FromQuery] string name = "",
- [FromQuery] string description = "")
+ public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto)
{
var notification = new NotificationRequest
{
- Name = name,
- Description = description,
- Url = url,
- Level = level ?? NotificationLevel.Normal,
+ Name = notificationDto.Name,
+ Description = notificationDto.Description,
+ Url = notificationDto.Url,
+ Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal,
UserIds = _userManager.Users
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
.Select(user => user.Id)
@@ -114,7 +108,6 @@ namespace Jellyfin.Api.Controllers
};
_notificationManager.SendNotification(notification, CancellationToken.None);
-
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 70a94e27c..b98307f87 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -94,10 +94,10 @@ namespace Jellyfin.Api.Controllers
}
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
- var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
+ var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery(
+ personTypes,
+ excludePersonTypes)
{
- PersonTypes = personTypes,
- ExcludePersonTypes = excludePersonTypes,
NameContains = searchTerm,
User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index ec7b84ff6..f256c8c25 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
/// A .
[HttpPost("Sessions/Playing/Ping")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PingPlaybackSession([FromQuery] string playSessionId)
+ public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
{
_transcodingJobHelper.PingTranscodingJob(playSessionId, null);
return NoContent();
@@ -202,9 +202,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] PlayMethod playMethod,
+ [FromQuery] PlayMethod? playMethod,
[FromQuery] string? liveStreamId,
- [FromQuery] string playSessionId,
+ [FromQuery] string? playSessionId,
[FromQuery] bool canSeek = false)
{
var playbackStartInfo = new PlaybackStartInfo
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
MediaSourceId = mediaSourceId,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
- PlayMethod = playMethod,
+ PlayMethod = playMethod ?? PlayMethod.Transcode,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId
};
@@ -254,10 +254,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? volumeLevel,
- [FromQuery] PlayMethod playMethod,
+ [FromQuery] PlayMethod? playMethod,
[FromQuery] string? liveStreamId,
- [FromQuery] string playSessionId,
- [FromQuery] RepeatMode repeatMode,
+ [FromQuery] string? playSessionId,
+ [FromQuery] RepeatMode? repeatMode,
[FromQuery] bool isPaused = false,
[FromQuery] bool isMuted = false)
{
@@ -271,10 +271,10 @@ namespace Jellyfin.Api.Controllers
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
VolumeLevel = volumeLevel,
- PlayMethod = playMethod,
+ PlayMethod = playMethod ?? PlayMethod.Transcode,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId,
- RepeatMode = repeatMode
+ RepeatMode = repeatMode ?? RepeatMode.RepeatNone
};
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
@@ -352,7 +352,7 @@ namespace Jellyfin.Api.Controllers
return _userDataRepository.GetUserDataDto(item, user);
}
- private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
+ private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
{
if (method == PlayMethod.Transcode)
{
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index a5aa9bfca..7a6130719 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -12,7 +12,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
using Microsoft.AspNetCore.Authorization;
@@ -45,7 +44,7 @@ namespace Jellyfin.Api.Controllers
{
_installationManager = installationManager;
_pluginManager = pluginManager;
- _serializerOptions = JsonDefaults.GetOptions();
+ _serializerOptions = JsonDefaults.Options;
_config = config;
}
@@ -208,12 +207,7 @@ namespace Jellyfin.Api.Controllers
var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId));
// Select the un-instanced one first.
- var plugin = plugins.FirstOrDefault(p => p.Instance == null);
- if (plugin == null)
- {
- // Then by the status.
- plugin = plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
- }
+ var plugin = plugins.FirstOrDefault(p => p.Instance == null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
if (plugin != null)
{
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 5284888d8..ec836f43e 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -145,58 +145,6 @@ namespace Jellyfin.Api.Controllers
return Ok(_providerManager.GetRemoteImageProviderInfo(item));
}
- ///
- /// Gets a remote image.
- ///
- /// The image url.
- /// Remote image returned.
- /// Remote image not found.
- /// Image Stream.
- [HttpGet("Images/Remote")]
- [Produces(MediaTypeNames.Application.Octet)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task GetRemoteImage([FromQuery, Required] Uri imageUrl)
- {
- var urlHash = imageUrl.ToString().GetMD5();
- var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
- string? contentPath = null;
- var hasFile = false;
-
- try
- {
- contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
- if (System.IO.File.Exists(contentPath))
- {
- hasFile = true;
- }
- }
- catch (FileNotFoundException)
- {
- // The file isn't cached yet
- }
- catch (IOException)
- {
- // The file isn't cached yet
- }
-
- if (!hasFile)
- {
- await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
- contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
- }
-
- if (string.IsNullOrEmpty(contentPath))
- {
- return NotFound();
- }
-
- var contentType = MimeTypes.GetMimeType(contentPath);
- return PhysicalFile(contentPath, contentType);
- }
-
///
/// Downloads a remote image for an item.
///
@@ -259,7 +207,8 @@ namespace Jellyfin.Api.Controllers
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
Directory.CreateDirectory(fullCacheDirectory);
- await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 6c22050a7..73bdf9018 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -228,10 +228,7 @@ namespace Jellyfin.Api.Controllers
itemWithImage = GetParentWithImage(item, ImageType.Thumb);
}
- if (itemWithImage == null)
- {
- itemWithImage = GetParentWithImage(item, ImageType.Thumb);
- }
+ itemWithImage ??= GetParentWithImage(item, ImageType.Thumb);
if (itemWithImage != null)
{
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index e2269a2ce..7bd0b6918 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -153,6 +153,10 @@ namespace Jellyfin.Api.Controllers
/// The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.
/// The ids of the items to play, comma delimited.
/// The starting position of the first item.
+ /// Optional. The media source id.
+ /// Optional. The index of the audio stream to play.
+ /// Optional. The index of the subtitle stream to play.
+ /// Optional. The start index.
/// Instruction sent to session.
/// A .
[HttpPost("Sessions/{sessionId}/Playing")]
@@ -162,13 +166,21 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
- [FromQuery] long? startPositionTicks)
+ [FromQuery] long? startPositionTicks,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] int? startIndex)
{
var playRequest = new PlayRequest
{
ItemIds = itemIds,
StartPositionTicks = startPositionTicks,
- PlayCommand = playCommand
+ PlayCommand = playCommand,
+ MediaSourceId = mediaSourceId,
+ AudioStreamIndex = audioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ StartIndex = startIndex
};
_sessionManager.SendPlayCommand(
@@ -301,9 +313,7 @@ namespace Jellyfin.Api.Controllers
/// Issues a command to a client to display a message to the user.
///
/// The session id.
- /// The message test.
- /// The message header.
- /// The message timeout. If omitted the user will have to confirm viewing the message.
+ /// The object containing Header, Message Text, and TimeoutMs.
/// Message sent.
/// A .
[HttpPost("Sessions/{sessionId}/Message")]
@@ -311,16 +321,12 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendMessageCommand(
[FromRoute, Required] string sessionId,
- [FromQuery, Required] string text,
- [FromQuery] string? header,
- [FromQuery] long? timeoutMs)
+ [FromBody, Required] MessageCommand command)
{
- var command = new MessageCommand
+ if (string.IsNullOrWhiteSpace(command.Header))
{
- Header = string.IsNullOrEmpty(header) ? "Message from Server" : header,
- TimeoutMs = timeoutMs,
- Text = text
- };
+ command.Header = "Message from Server";
+ }
_sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index d9cb34557..a01a617fc 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -132,7 +132,10 @@ namespace Jellyfin.Api.Controllers
{
var user = _userManager.Users.First();
- user.Username = startupUserDto.Name;
+ if (startupUserDto.Name != null)
+ {
+ user.Username = startupUserDto.Name;
+ }
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 16a47f2d8..b473574e0 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -182,6 +182,10 @@ namespace Jellyfin.Api.Controllers
///
/// Gets subtitles in a specified format.
///
+ /// The (route) item id.
+ /// The (route) media source id.
+ /// The (route) subtitle stream index.
+ /// The (route) format of the returned subtitle.
/// The item id.
/// The media source id.
/// The subtitle stream index.
@@ -189,22 +193,32 @@ namespace Jellyfin.Api.Controllers
/// Optional. The end position of the subtitle in ticks.
/// Optional. Whether to copy the timestamps.
/// Optional. Whether to add a VTT time map.
- /// Optional. The start position of the subtitle in ticks.
+ /// The start position of the subtitle in ticks.
/// File returned.
/// A with the subtitle file.
- [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
+ [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile("text/*")]
public async Task GetSubtitle(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string mediaSourceId,
- [FromRoute, Required] int index,
- [FromRoute, Required] string format,
+ [FromRoute, Required] Guid routeItemId,
+ [FromRoute, Required] string routeMediaSourceId,
+ [FromRoute, Required] int routeIndex,
+ [FromRoute, Required] string routeFormat,
+ [FromQuery, ParameterObsolete] Guid? itemId,
+ [FromQuery, ParameterObsolete] string? mediaSourceId,
+ [FromQuery, ParameterObsolete] int? index,
+ [FromQuery, ParameterObsolete] string? format,
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps = false,
[FromQuery] bool addVttTimeMap = false,
[FromQuery] long startPositionTicks = 0)
{
+ // Set parameters to route value if not provided via query.
+ itemId ??= routeItemId;
+ mediaSourceId ??= routeMediaSourceId;
+ index ??= routeIndex;
+ format ??= routeFormat;
+
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
{
format = "json";
@@ -212,9 +226,9 @@ namespace Jellyfin.Api.Controllers
if (string.IsNullOrEmpty(format))
{
- var item = (Video)_libraryManager.GetItemById(itemId);
+ var item = (Video)_libraryManager.GetItemById(itemId.Value);
- var idString = itemId.ToString("N", CultureInfo.InvariantCulture);
+ var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
.First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
@@ -226,7 +240,7 @@ namespace Jellyfin.Api.Controllers
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
{
- await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
+ await using Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
using var reader = new StreamReader(stream);
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
@@ -238,9 +252,9 @@ namespace Jellyfin.Api.Controllers
return File(
await EncodeSubtitles(
- itemId,
+ itemId.Value,
mediaSourceId,
- index,
+ index.Value,
format,
startPositionTicks,
endPositionTicks,
@@ -251,30 +265,44 @@ namespace Jellyfin.Api.Controllers
///
/// Gets subtitles in a specified format.
///
+ /// The (route) item id.
+ /// The (route) media source id.
+ /// The (route) subtitle stream index.
+ /// The (route) start position of the subtitle in ticks.
+ /// The (route) format of the returned subtitle.
/// The item id.
/// The media source id.
/// The subtitle stream index.
- /// Optional. The start position of the subtitle in ticks.
+ /// The start position of the subtitle in ticks.
/// The format of the returned subtitle.
/// Optional. The end position of the subtitle in ticks.
/// Optional. Whether to copy the timestamps.
/// Optional. Whether to add a VTT time map.
/// File returned.
/// A with the subtitle file.
- [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
+ [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile("text/*")]
public Task GetSubtitleWithTicks(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string mediaSourceId,
- [FromRoute, Required] int index,
- [FromRoute, Required] long startPositionTicks,
- [FromRoute, Required] string format,
+ [FromRoute, Required] Guid routeItemId,
+ [FromRoute, Required] string routeMediaSourceId,
+ [FromRoute, Required] int routeIndex,
+ [FromRoute, Required] long routeStartPositionTicks,
+ [FromRoute, Required] string routeFormat,
+ [FromQuery, ParameterObsolete] Guid? itemId,
+ [FromQuery, ParameterObsolete] string? mediaSourceId,
+ [FromQuery, ParameterObsolete] int? index,
+ [FromQuery, ParameterObsolete] long? startPositionTicks,
+ [FromQuery, ParameterObsolete] string? format,
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps = false,
[FromQuery] bool addVttTimeMap = false)
{
return GetSubtitle(
+ routeItemId,
+ routeMediaSourceId,
+ routeIndex,
+ routeFormat,
itemId,
mediaSourceId,
index,
@@ -282,7 +310,7 @@ namespace Jellyfin.Api.Controllers
endPositionTicks,
copyTimestamps,
addVttTimeMap,
- startPositionTicks);
+ startPositionTicks ?? routeStartPositionTicks);
}
///
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index a55f13e66..a811a29c3 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions().AddClientFields(Request);
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{
- OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
MediaTypes = mediaType,
IncludeItemTypes = type,
IsVirtualItem = false,
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index dd3836551..5cb7468b2 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -114,7 +114,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult> GetTrailers(
- [FromQuery] Guid? userId,
+ [FromQuery] Guid userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index e1c67f830..ffb726fab 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -65,6 +65,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. The max number of images to return, per image type.
/// Optional. The image types to include in the output.
/// Optional. Include user data.
+ /// Optional. Starting date of shows to show in Next Up section.
/// Whether to enable the total records count. Defaults to true.
/// Whether to disable sending the first episode in a series as next up.
/// A with the next up episodes.
@@ -81,6 +82,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
+ [FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool disableFirstEpisode = false)
{
@@ -97,7 +99,8 @@ namespace Jellyfin.Api.Controllers
StartIndex = startIndex,
UserId = userId ?? Guid.Empty,
EnableTotalRecordCount = enableTotalRecordCount,
- DisableFirstEpisode = disableFirstEpisode
+ DisableFirstEpisode = disableFirstEpisode,
+ NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue
},
options);
@@ -155,7 +158,7 @@ namespace Jellyfin.Api.Controllers
var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { nameof(Episode) },
- OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple(i, SortOrder.Ascending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
MinPremiereDate = minPremiereDate,
StartIndex = startIndex,
Limit = limit,
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index bacd95bac..679f055bc 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] bool? enableRemoteMedia,
- [FromQuery] bool breakOnNonKeyFrames,
+ [FromQuery] bool breakOnNonKeyFrames = false,
[FromQuery] bool enableRedirection = true)
{
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
@@ -219,11 +219,11 @@ namespace Jellyfin.Api.Controllers
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Hls,
- RequireAvc = true,
- DeInterlace = true,
- RequireNonAnamorphic = true,
- EnableMpegtsM2TsMode = true,
- TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+ RequireAvc = false,
+ DeInterlace = false,
+ RequireNonAnamorphic = false,
+ EnableMpegtsM2TsMode = false,
+ TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
Context = EncodingContext.Static,
StreamOptions = new Dictionary(),
EnableAdaptiveBitrateStreaming = true
@@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers
CopyTimestamps = true,
StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Embed,
- TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+ TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
Context = EncodingContext.Static
};
@@ -298,9 +298,9 @@ namespace Jellyfin.Api.Controllers
{
Type = DlnaProfileType.Audio,
Context = EncodingContext.Streaming,
- Container = transcodingContainer,
- AudioCodec = audioCodec,
- Protocol = transcodingProtocol,
+ Container = transcodingContainer ?? "mp3",
+ AudioCodec = audioCodec ?? "mp3",
+ Protocol = transcodingProtocol ?? "http",
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
}
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 43ee309b7..b13db4baa 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -21,6 +21,7 @@ using MediaBrowser.Model.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
{
@@ -36,6 +37,7 @@ namespace Jellyfin.Api.Controllers
private readonly IDeviceManager _deviceManager;
private readonly IAuthorizationContext _authContext;
private readonly IServerConfigurationManager _config;
+ private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
@@ -46,13 +48,15 @@ namespace Jellyfin.Api.Controllers
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
+ /// Instance of the interface.
public UserController(
IUserManager userManager,
ISessionManager sessionManager,
INetworkManager networkManager,
IDeviceManager deviceManager,
IAuthorizationContext authContext,
- IServerConfigurationManager config)
+ IServerConfigurationManager config,
+ ILogger logger)
{
_userManager = userManager;
_sessionManager = sessionManager;
@@ -60,6 +64,7 @@ namespace Jellyfin.Api.Controllers
_deviceManager = deviceManager;
_authContext = authContext;
_config = config;
+ _logger = logger;
}
///
@@ -118,7 +123,7 @@ namespace Jellyfin.Api.Controllers
return NotFound("User not found");
}
- var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp());
+ var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString());
return result;
}
@@ -172,11 +177,9 @@ namespace Jellyfin.Api.Controllers
return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed.");
}
- // Password should always be null
AuthenticateUserByName request = new AuthenticateUserByName
{
Username = user.Username,
- Password = null,
Pw = pw
};
return await AuthenticateUserByName(request).ConfigureAwait(false);
@@ -203,8 +206,7 @@ namespace Jellyfin.Api.Controllers
DeviceId = auth.DeviceId,
DeviceName = auth.Device,
Password = request.Pw,
- PasswordSha1 = request.Password,
- RemoteEndPoint = HttpContext.GetNormalizedRemoteIp(),
+ RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(),
Username = request.Username
}).ConfigureAwait(false);
@@ -291,7 +293,7 @@ namespace Jellyfin.Api.Controllers
user.Username,
request.CurrentPw,
request.CurrentPw,
- HttpContext.GetNormalizedRemoteIp(),
+ HttpContext.GetNormalizedRemoteIp().ToString(),
false).ConfigureAwait(false);
if (success == null)
@@ -483,7 +485,7 @@ namespace Jellyfin.Api.Controllers
await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
}
- var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp());
+ var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString());
return result;
}
@@ -498,8 +500,14 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
{
+ var ip = HttpContext.GetNormalizedRemoteIp();
var isLocal = HttpContext.IsLocal()
- || _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp());
+ || _networkManager.IsInLocalNetwork(ip);
+
+ if (isLocal)
+ {
+ _logger.LogWarning("Password reset proccess initiated from outside the local network with IP: {IP}", ip);
+ }
var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);
@@ -581,7 +589,7 @@ namespace Jellyfin.Api.Controllers
var result = users
.OrderBy(u => u.Username)
- .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp()));
+ .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString()));
return result;
}
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index ba51aa43e..6a720b1a4 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -48,9 +48,6 @@ namespace Jellyfin.Api.Controllers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ILogger _logger;
@@ -60,9 +57,6 @@ namespace Jellyfin.Api.Controllers
/// Initializes a new instance of the class.
///
/// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
@@ -72,11 +66,9 @@ namespace Jellyfin.Api.Controllers
/// Instance of the interface.
/// The singleton.
/// Instance of the .
+ /// Instance of .
public VideoHlsController(
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
IDlnaManager dlnaManager,
IUserManager userManger,
IAuthorizationContext authorizationContext,
@@ -85,10 +77,9 @@ namespace Jellyfin.Api.Controllers
IServerConfigurationManager serverConfigurationManager,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
- ILogger logger)
+ ILogger logger,
+ EncodingHelper encodingHelper)
{
- _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
-
_dlnaManager = dlnaManager;
_authContext = authorizationContext;
_userManager = userManger;
@@ -96,12 +87,11 @@ namespace Jellyfin.Api.Controllers
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_logger = logger;
+ _encodingHelper = encodingHelper;
+
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
}
@@ -198,7 +188,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -213,7 +203,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -223,7 +213,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -247,34 +237,35 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
};
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
@@ -285,9 +276,7 @@ namespace Jellyfin.Api.Controllers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 44dc63952..dc64a0f1b 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -49,12 +49,10 @@ namespace Jellyfin.Api.Controllers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly EncodingHelper _encodingHelper;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
@@ -69,12 +67,10 @@ namespace Jellyfin.Api.Controllers
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
/// Instance of the interface.
/// Instance of the class.
/// Instance of the interface.
+ /// Instance of .
public VideosController(
ILibraryManager libraryManager,
IUserManager userManager,
@@ -84,12 +80,10 @@ namespace Jellyfin.Api.Controllers
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
- IHttpClientFactory httpClientFactory)
+ IHttpClientFactory httpClientFactory,
+ EncodingHelper encodingHelper)
{
_libraryManager = libraryManager;
_userManager = userManager;
@@ -99,12 +93,10 @@ namespace Jellyfin.Api.Controllers
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_httpClientFactory = httpClientFactory;
+ _encodingHelper = encodingHelper;
}
///
@@ -217,9 +209,7 @@ namespace Jellyfin.Api.Controllers
return BadRequest("Please supply at least two videos to merge.");
}
- var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList();
-
- var primaryVersion = videosWithVersions.FirstOrDefault();
+ var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
if (primaryVersion == null)
{
primaryVersion = items
@@ -364,7 +354,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -379,16 +369,17 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -412,28 +403,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -446,9 +437,7 @@ namespace Jellyfin.Api.Controllers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
@@ -517,8 +506,7 @@ namespace Jellyfin.Api.Controllers
// Need to start ffmpeg (because media can't be returned directly)
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
- var ffmpegCommandLineArguments = encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
+ var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
return await FileStreamResponseHelpers.GetTranscodedFile(
state,
isHeadRequest,
@@ -540,7 +528,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. The dlna device profile id to utilize.
/// The play session id.
/// The segment container.
- /// The segment lenght.
+ /// The segment length.
/// The minimum number of segments.
/// The media version id, if playing an alternate version.
/// The device id of the client requesting. Used to stop encoding processes when needed.
@@ -569,7 +557,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. The maximum video bit depth.
/// Optional. Whether to require avc.
/// Optional. Whether to deinterlace the video.
- /// Optional. Whether to require a non anamporphic stream.
+ /// Optional. Whether to require a non anamorphic stream.
/// Optional. The maximum number of audio channels to transcode.
/// Optional. The limit of how many cpu cores to use.
/// The live stream id.
@@ -583,8 +571,8 @@ namespace Jellyfin.Api.Controllers
/// Optional. The streaming options.
/// Video stream returned.
/// A containing the audio file.
- [HttpGet("{itemId}/{stream=stream}.{container}")]
- [HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
+ [HttpGet("{itemId}/stream.{container}")]
+ [HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesVideoFile]
public Task GetVideoStreamByContainer(
@@ -620,7 +608,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -635,7 +623,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions)
{
return GetVideoStream(
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index f2abd515d..06173315a 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
@@ -113,14 +112,5 @@ namespace Jellyfin.Api.Extensions
return dtoOptions;
}
-
- ///
- /// Check if DtoOptions contains field.
- ///
- /// DtoOptions object.
- /// Field to check.
- /// Field existence.
- internal static bool ContainsField(this DtoOptions dtoOptions, ItemFields field)
- => dtoOptions.Fields != null && dtoOptions.Fields.Contains(field);
}
}
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index 21ec2d32f..264131905 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Net.Http;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
@@ -33,13 +32,11 @@ namespace Jellyfin.Api.Helpers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly EncodingHelper _encodingHelper;
///
/// Initializes a new instance of the