Merge remote-tracking branch 'upstream/master' into UrlDecoding

This commit is contained in:
BaronGreenback 2021-05-08 12:22:09 +01:00
commit 7185de970c
739 changed files with 10333 additions and 5367 deletions

View File

@ -1,59 +0,0 @@
parameters:
- name: LinuxImage
type: string
default: "ubuntu-latest"
- name: GeneratorVersion
type: string
default: "5.0.1"
jobs:
- job: GenerateApiClients
displayName: 'Generate Api Clients'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
dependsOn: Test
pool:
vmImage: "${{ parameters.LinuxImage }}"
steps:
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec Artifact'
inputs:
source: 'current'
artifact: "OpenAPI Spec"
path: "$(System.ArtifactsDirectory)/openapispec"
runVersion: "latest"
- task: CmdLine@2
displayName: 'Download OpenApi Generator'
inputs:
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
## Authenticate with npm registry
- task: npmAuthenticate@0
inputs:
workingFile: ./.npmrc
customEndpoint: 'jellyfin-bot for NPM'
## Generate npm api client
- task: CmdLine@2
displayName: 'Build stable typescript axios client'
inputs:
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
## Run npm install
- task: Npm@1
displayName: 'Install npm dependencies'
inputs:
command: install
workingDir: ./apiclient/generated/typescript/axios
## Publish npm packages
- task: Npm@1
displayName: 'Publish stable typescript axios client'
inputs:
command: custom
customCommand: publish --access public
publishRegistry: useExternalRegistry
publishEndpoint: 'jellyfin-bot for NPM'
workingDir: ./apiclient/generated/typescript/axios

View File

@ -160,7 +160,6 @@ jobs:
dependsOn: dependsOn:
- BuildPackage - BuildPackage
- BuildDocker - BuildDocker
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
@ -186,9 +185,6 @@ jobs:
- job: PublishNuget - job: PublishNuget
displayName: 'Publish NuGet packages' displayName: 'Publish NuGet packages'
dependsOn:
- BuildPackage
condition: succeeded('BuildPackage')
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'

View File

@ -94,5 +94,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact' displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs: inputs:
targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json" targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
artifactName: 'OpenAPI Spec' artifactName: 'OpenAPI Spec'

View File

@ -61,6 +61,3 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml - template: azure-pipelines-package.yml
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-api-client.yml

View File

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

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

@ -0,0 +1,64 @@
name: Automation
on:
pull_request:
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Does PR has the stable backport label?
uses: Dreamcodeio/does-pr-has-label@v1.2
id: checkLabel
with:
label: stable backport
- 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) && !steps.checkLabel.outputs.hasLabel
continue-on-error: true
with:
project: Current Release
action: delete
repo-token: ${{ secrets.GH_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.GH_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) && steps.checkLabel.outputs.hasLabel
continue-on-error: true
with:
project: Current Release
column: In progress
repo-token: ${{ secrets.GH_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.GH_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.GH_TOKEN }}

View File

@ -0,0 +1,43 @@
comment:
header: Hello @{{ issue.user.login }}
footer: "\
---\n\n
> This is an automated comment created by the [peaceiris/actions-label-commenter]. \
Responding to the bot or mentioning it won't have any effect.\n\n
[peaceiris/actions-label-commenter]: https://github.com/peaceiris/actions-label-commenter
"
labels:
- name: stable backport
labeled:
pr:
body: |
This pull request has been tagged as a stable backport. It will be cherry-picked into the next stable point release.
Please observe the following:
* Any dependent PRs that this PR requires **must** be tagged for stable backporting as well.
* Any issue(s) this PR fixes or closes **should** target the current stable release or a previous stable release to which a fix has not yet entered the current stable release.
* This PR **must** be test cherry-picked against the current release branch (`release-X.Y.z` where X and Y are numbers). It must apply cleanly, or a diff of the expected change must be provided.
To do this, run the following commands from your local copy of the Jellyfin repository:
1. `git checkout master`
1. `git merge --no-ff <myPullRequestBranch>`
1. `git log` -> `commit xxxxxxxxx`, grab hash
1. `git checkout release-X.Y.z` replacing X and Y with the *current* stable version (e.g. `release-10.7.z`)
1. `git cherry-pick -sx -m1 <hash>`
Ensure the `cherry-pick` applies cleanly. If it does not, fix any merge conflicts *preserving as much of the original code as possible*, and make note of the resulting diff.
Test your changes with a build to ensure they are successful. If not, adjust the diff accordingly.
**Do not** push your merges to either branch. Use `git reset --hard HEAD~1` to revert both branches to their original state.
Reply to this PR with a comment beginning "Cherry-pick test completed." and including the merge-conflict-fixing diff(s) if applicable.

22
.github/workflows/label-commenter.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Label Commenter
on:
issues:
types:
- labeled
- unlabeled
pull_request_target:
types:
- labeled
- unlabeled
jobs:
comment:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
ref: master
- name: Label Commenter
uses: peaceiris/actions-label-commenter@v1

17
.github/workflows/merge-conflicts.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: 'Merge Conflicts'
on:
push:
branches:
- master
pull_request_target:
types:
- synchronize
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: eps1lon/actions-label-merge-conflict@v2.0.1
with:
dirtyLabel: 'merge conflict'
repoToken: ${{ secrets.GH_TOKEN }}

27
.github/workflows/rebase.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Automatic Rebase
on:
issue_comment:
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@v1.4.5
with:
token: ${{ secrets.GH_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.GH_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.4
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

View File

@ -17,6 +17,7 @@
- [bugfixin](https://github.com/bugfixin) - [bugfixin](https://github.com/bugfixin)
- [chaosinnovator](https://github.com/chaosinnovator) - [chaosinnovator](https://github.com/chaosinnovator)
- [ckcr4lyf](https://github.com/ckcr4lyf) - [ckcr4lyf](https://github.com/ckcr4lyf)
- [cocool97](https://github.com/cocool97)
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear) - [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
- [crankdoofus](https://github.com/crankdoofus) - [crankdoofus](https://github.com/crankdoofus)
- [crobibero](https://github.com/crobibero) - [crobibero](https://github.com/crobibero)
@ -49,6 +50,7 @@
- [h1nk](https://github.com/h1nk) - [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93) - [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017) - [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga) - [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h) - [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface) - [joshuaboniface](https://github.com/joshuaboniface)
@ -104,6 +106,7 @@
- [shemanaev](https://github.com/shemanaev) - [shemanaev](https://github.com/shemanaev)
- [skaro13](https://github.com/skaro13) - [skaro13](https://github.com/skaro13)
- [sl1288](https://github.com/sl1288) - [sl1288](https://github.com/sl1288)
- [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004) - [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251) - [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits) - [spookbits](https://github.com/spookbits)

View File

@ -5,10 +5,10 @@ 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 python \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& yarn install \ && npm ci --no-audit \
&& mv dist /dist && mv dist /dist
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

View File

@ -10,7 +10,7 @@ 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 python \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& yarn install \ && npm ci --no-audit \
&& mv dist /dist && mv dist /dist

View File

@ -10,7 +10,7 @@ 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 python \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& yarn install \ && npm ci --no-audit \
&& mv dist /dist && mv dist /dist

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
namespace Emby.Dlna.Configuration namespace Emby.Dlna.Configuration

View File

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

View File

@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
{ {
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase)) if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
{ {

View File

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

View File

@ -1,5 +1,6 @@
#nullable disable
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -7,7 +8,6 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Xml; using System.Xml;
using Emby.Dlna.Configuration;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
@ -121,7 +121,7 @@ namespace Emby.Dlna.ContentDirectory
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
{ {
if (xmlWriter == null) if (xmlWriter == null)
{ {
@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory
/// <summary> /// <summary>
/// Adds a "XSetBookmark" element to the xml document. /// Adds a "XSetBookmark" element to the xml document.
/// </summary> /// </summary>
/// <param name="sparams">The <see cref="IDictionary"/>.</param> /// <param name="sparams">The method parameters.</param>
private void HandleXSetBookmark(IDictionary<string, string> sparams) private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams)
{ {
var id = sparams["ObjectID"]; var id = sparams["ObjectID"];
@ -305,35 +305,18 @@ namespace Emby.Dlna.ContentDirectory
return builder.ToString(); return builder.ToString();
} }
/// <summary>
/// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist.
/// </summary>
/// <param name="sparams">The <see cref="IDictionary"/>.</param>
/// <param name="key">The key.</param>
/// <param name="defaultValue">The defaultValue.</param>
/// <returns>The <see cref="string"/>.</returns>
public static string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue)
{
if (sparams != null && sparams.TryGetValue(key, out string val))
{
return val;
}
return defaultValue;
}
/// <summary> /// <summary>
/// Builds the "Browse" xml response. /// Builds the "Browse" xml response.
/// </summary> /// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
/// <param name="sparams">The <see cref="IDictionary"/>.</param> /// <param name="sparams">The method parameters.</param>
/// <param name="deviceId">The device Id to use.</param> /// <param name="deviceId">The device Id to use.</param>
private void HandleBrowse(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
{ {
var id = sparams["ObjectID"]; var id = sparams["ObjectID"];
var flag = sparams["BrowseFlag"]; var flag = sparams["BrowseFlag"];
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty)); var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
var provided = 0; var provided = 0;
@ -435,9 +418,9 @@ namespace Emby.Dlna.ContentDirectory
/// Builds the response to the "X_BrowseByLetter request. /// Builds the response to the "X_BrowseByLetter request.
/// </summary> /// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
/// <param name="sparams">The <see cref="IDictionary"/>.</param> /// <param name="sparams">The method parameters.</param>
/// <param name="deviceId">The device id.</param> /// <param name="deviceId">The device id.</param>
private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
{ {
// TODO: Implement this method // TODO: Implement this method
HandleSearch(xmlWriter, sparams, deviceId); HandleSearch(xmlWriter, sparams, deviceId);
@ -447,13 +430,13 @@ namespace Emby.Dlna.ContentDirectory
/// Builds a response to the "Search" request. /// Builds a response to the "Search" request.
/// </summary> /// </summary>
/// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param> /// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param>
/// <param name="sparams">The sparams<see cref="IDictionary"/>.</param> /// <param name="sparams">The method parameters.</param>
/// <param name="deviceId">The deviceId<see cref="string"/>.</param> /// <param name="deviceId">The deviceId<see cref="string"/>.</param>
private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
{ {
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty)); var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty)); var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
// sort example: dc:title, dc:date // sort example: dc:title, dc:date

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -36,7 +38,7 @@ namespace Emby.Dlna
private readonly ILogger<DlnaManager> _logger; private readonly ILogger<DlnaManager> _logger;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal); private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
@ -111,7 +113,7 @@ namespace Emby.Dlna
if (profile != null) if (profile != null)
{ {
_logger.LogDebug("Found matching device profile: {0}", profile.Name); _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
} }
else else
{ {
@ -138,80 +140,45 @@ namespace Emby.Dlna
_logger.LogInformation(builder.ToString()); _logger.LogInformation(builder.ToString());
} }
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo) /// <summary>
/// Attempts to match a device with a profile.
/// Rules:
/// - If the profile field has no value, the field matches irregardless of its contents.
/// - the profile field can be an exact match, or a reg exp.
/// </summary>
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
/// <returns><b>True</b> if they match.</returns>
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
{ {
if (!string.IsNullOrEmpty(profileInfo.FriendlyName)) return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
{ && IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)) && IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
{ && IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
return false; && IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
} && IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
} && IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
{
if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
{
if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
{
if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelName))
{
if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
{
if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
{
if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
{
if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
{
return false;
}
}
return true;
} }
private bool IsRegexOrSubstringMatch(string input, string pattern) private bool IsRegexOrSubstringMatch(string input, string pattern)
{ {
if (string.IsNullOrEmpty(pattern))
{
// In profile identification: An empty pattern matches anything.
return true;
}
if (string.IsNullOrEmpty(input))
{
// The profile contains a value, and the device doesn't.
return false;
}
try try
{ {
return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
@ -333,7 +300,12 @@ namespace Emby.Dlna
throw new ArgumentNullException(nameof(id)); throw new ArgumentNullException(nameof(id));
} }
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
if (info == null)
{
return null;
}
return ParseProfileFile(info.Path, info.Info.Type); return ParseProfileFile(info.Path, info.Info.Type);
} }
@ -395,7 +367,8 @@ namespace Emby.Dlna
{ {
Directory.CreateDirectory(systemProfilesPath); Directory.CreateDirectory(systemProfilesPath);
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{ {
await stream.CopyToAsync(fileStream).ConfigureAwait(false); await stream.CopyToAsync(fileStream).ConfigureAwait(false);
} }

View File

@ -21,11 +21,11 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -5,7 +7,6 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.PlayTo; using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
@ -128,7 +129,8 @@ namespace Emby.Dlna.Main
_netConfig = config.GetConfiguration<NetworkConfiguration>("network"); _netConfig = config.GetConfiguration<NetworkConfiguration>("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps; _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
if (_disabled)
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{ {
_logger.LogError("The DLNA specification does not support HTTPS."); _logger.LogError("The DLNA specification does not support HTTPS.");
} }
@ -316,7 +318,7 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri); var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
if (_appHost.PublishedServerUrl == null) if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
{ {
// DLNA will only work over http, so we must reset to http:// : {port}. // DLNA will only work over http, so we must reset to http:// : {port}.
uri.Scheme = "http"; uri.Scheme = "http";

View File

@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
{ {
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
{ {

View File

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

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -219,7 +221,7 @@ namespace Emby.Dlna.PlayTo
{ {
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
if (command == null) if (command == null)
{ {
return false; return false;
@ -259,7 +261,7 @@ namespace Emby.Dlna.PlayTo
{ {
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
if (command == null) if (command == null)
{ {
return; return;
@ -290,7 +292,7 @@ namespace Emby.Dlna.PlayTo
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
if (command == null) if (command == null)
{ {
return; return;
@ -323,7 +325,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header); _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
if (command == null) if (command == null)
{ {
return; return;
@ -403,6 +405,10 @@ namespace Emby.Dlna.PlayTo
public async Task SetPlay(CancellationToken cancellationToken) public async Task SetPlay(CancellationToken cancellationToken)
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
if (avCommands == null)
{
return;
}
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
@ -413,7 +419,7 @@ namespace Emby.Dlna.PlayTo
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
if (command == null) if (command == null)
{ {
return; return;
@ -437,7 +443,7 @@ namespace Emby.Dlna.PlayTo
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
if (command == null) if (command == null)
{ {
return; return;
@ -565,7 +571,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
if (command == null) if (command == null)
{ {
return; return;
@ -615,7 +621,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
if (command == null) if (command == null)
{ {
return; return;
@ -702,6 +708,10 @@ namespace Emby.Dlna.PlayTo
} }
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
if (rendererCommands == null)
{
return null;
}
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
@ -770,6 +780,11 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
if (rendererCommands == null)
{
return (false, null);
}
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
@ -951,6 +966,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory); var httpClient = new SsdpHttpClient(_httpClientFactory);
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
AvCommands = TransportCommands.Create(document); AvCommands = TransportCommands.Create(document);
return AvCommands; return AvCommands;
@ -979,6 +998,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory); var httpClient = new SsdpHttpClient(_httpClientFactory);
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
RendererCommands = TransportCommands.Create(document); RendererCommands = TransportCommands.Create(document);
return RendererCommands; return RendererCommands;
@ -1010,6 +1033,10 @@ namespace Emby.Dlna.PlayTo
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory); var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
var friendlyNames = new List<string>(); var friendlyNames = new List<string>();

View File

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

View File

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

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -132,7 +134,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e) private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{ {
if (_disposed) if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{ {
return; return;
} }
@ -499,8 +501,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Audio) if (streamInfo.MediaType == DlnaProfileType.Audio)
{ {
return new ContentFeatureBuilder(profile) return ContentFeatureBuilder.BuildAudioHeader(
.BuildAudioHeader( profile,
streamInfo.Container, streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate, streamInfo.TargetAudioBitrate,
@ -514,8 +516,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Video) if (streamInfo.MediaType == DlnaProfileType.Video)
{ {
var list = new ContentFeatureBuilder(profile) var list = ContentFeatureBuilder.BuildVideoHeader(
.BuildVideoHeader( profile,
streamInfo.Container, streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(),
@ -943,11 +945,7 @@ namespace Emby.Dlna.PlayTo
request.DeviceId = values.GetValueOrDefault("DeviceId"); request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId"); request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId"); request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
// Be careful, IsDirectStream==true by default (Static != false or not in query).
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks"); request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -178,12 +180,17 @@ namespace Emby.Dlna.PlayTo
if (controller == null) if (controller == null)
{ {
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false); var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
if (device == null)
{
_logger.LogError("Ignoring device as xml response is invalid.");
return;
}
string deviceName = device.Properties.Name; string deviceName = device.Properties.Name;
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress); string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
controller = new PlayToController( controller = new PlayToController(
sessionInfo, sessionInfo,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
@ -45,10 +46,10 @@ namespace Emby.Dlna.PlayTo
cancellationToken) cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8); return await XDocument.LoadAsync(
return XDocument.Parse( stream,
await reader.ReadToEndAsync().ConfigureAwait(false), LoadOptions.PreserveWhitespace,
LoadOptions.PreserveWhitespace); cancellationToken).ConfigureAwait(false);
} }
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@ -94,10 +95,17 @@ namespace Emby.Dlna.PlayTo
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8); try
return XDocument.Parse( {
await reader.ReadToEndAsync().ConfigureAwait(false), return await XDocument.LoadAsync(
LoadOptions.PreserveWhitespace); stream,
LoadOptions.PreserveWhitespace,
cancellationToken).ConfigureAwait(false);
}
catch
{
return null;
}
} }
private async Task<HttpResponseMessage> PostSoapDataAsync( private async Task<HttpResponseMessage> PostSoapDataAsync(

View File

@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
public class TransportCommands public class TransportCommands
{ {
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>"; private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
private List<StateVariable> _stateVariables = new List<StateVariable>();
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
public List<StateVariable> StateVariables => _stateVariables; public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
public List<ServiceAction> ServiceActions => _serviceActions; public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
public static TransportCommands Create(XDocument document) public static TransportCommands Create(XDocument document)
{ {
@ -48,7 +46,7 @@ namespace Emby.Dlna.PlayTo
{ {
var serviceAction = new ServiceAction var serviceAction = new ServiceAction
{ {
Name = container.GetValue(UPnpNamespaces.Svc + "name"), Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
}; };
var argumentList = serviceAction.ArgumentList; var argumentList = serviceAction.ArgumentList;
@ -70,9 +68,9 @@ namespace Emby.Dlna.PlayTo
return new Argument return new Argument
{ {
Name = container.GetValue(UPnpNamespaces.Svc + "name"), Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"), Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
}; };
} }
@ -91,8 +89,8 @@ namespace Emby.Dlna.PlayTo
return new StateVariable return new StateVariable
{ {
Name = container.GetValue(UPnpNamespaces.Svc + "name"), Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"), DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
AllowedValues = allowedValues AllowedValues = allowedValues
}; };
} }
@ -168,7 +166,7 @@ namespace Emby.Dlna.PlayTo
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString); return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
} }
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "") private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
{ {
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase)); var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));

View File

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

View File

@ -1,5 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using System.Globalization;
using System.Linq; using System.Linq;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
{ {
public DefaultProfile() public DefaultProfile()
{ {
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
Name = "Generic Device"; Name = "Generic Device";
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*"; ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";

View File

@ -250,7 +250,8 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/'); url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
return SecurityElement.Escape(url); // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
return SecurityElement.Escape(url) ?? string.Empty;
} }
private IEnumerable<DeviceIcon> GetIcons() private IEnumerable<DeviceIcon> GetIcons()

View File

@ -47,7 +47,7 @@ namespace Emby.Dlna.Service
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request) private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
{ {
ControlRequestInfo requestInfo = null; ControlRequestInfo? requestInfo = null;
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8)) using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
{ {
@ -151,7 +151,7 @@ namespace Emby.Dlna.Service
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader) private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
{ {
string namespaceURI = null, localName = null; string? namespaceURI = null, localName = null;
await reader.MoveToContentAsync().ConfigureAwait(false); await reader.MoveToContentAsync().ConfigureAwait(false);
await reader.ReadAsync().ConfigureAwait(false); await reader.ReadAsync().ConfigureAwait(false);
@ -210,7 +210,7 @@ namespace Emby.Dlna.Service
} }
} }
protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter); protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter);
private void LogRequest(ControlRequest request) private void LogRequest(ControlRequest request)
{ {

View File

@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -69,7 +71,7 @@ namespace Emby.Dlna.Ssdp
{ {
lock (_syncLock) lock (_syncLock)
{ {
if (_listenerCount > 0 && _deviceLocator == null) if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
{ {
_deviceLocator = new SsdpDeviceLocator(_commsServer); _deviceLocator = new SsdpDeviceLocator(_commsServer);
@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp
{ {
Location = e.DiscoveredDevice.DescriptionLocation, Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers, Headers = headers,
LocalIpAddress = e.LocalIpAddress RemoteIpAddress = e.RemoteIpAddress
}); });
DeviceDiscoveredInternal?.Invoke(this, args); DeviceDiscoveredInternal?.Invoke(this, args);

View File

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

View File

@ -25,7 +25,6 @@
<!-- Code analysers--> <!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@ -171,21 +172,31 @@ namespace Emby.Drawing
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
} }
ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
int quality = options.Quality; int quality = options.Quality;
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency); ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer); string cacheFilePath = GetCacheFilePath(
originalImagePath,
options.Width,
options.Height,
options.MaxWidth,
options.MaxHeight,
options.FillWidth,
options.FillHeight,
quality,
dateModified,
outputFormat,
options.AddPlayedIndicator,
options.PercentPlayed,
options.UnplayedCount,
options.Blur,
options.BackgroundColor,
options.ForegroundLayer);
try try
{ {
if (!File.Exists(cacheFilePath)) if (!File.Exists(cacheFilePath))
{ {
if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
{
options.CropWhiteSpace = false;
}
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat); string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase)) if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
@ -246,48 +257,111 @@ namespace Emby.Drawing
/// <summary> /// <summary>
/// Gets the cache file path based on a set of parameters. /// Gets the cache file path based on a set of parameters.
/// </summary> /// </summary>
private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer) private string GetCacheFilePath(
string originalPath,
int? width,
int? height,
int? maxWidth,
int? maxHeight,
int? fillWidth,
int? fillHeight,
int quality,
DateTime dateModified,
ImageFormat format,
bool addPlayedIndicator,
double percentPlayed,
int? unwatchedCount,
int? blur,
string backgroundColor,
string foregroundLayer)
{ {
var filename = originalPath var filename = new StringBuilder(256);
+ "width=" + outputSize.Width filename.Append(originalPath);
+ "height=" + outputSize.Height
+ "quality=" + quality filename.Append(",quality=");
+ "datemodified=" + dateModified.Ticks filename.Append(quality);
+ "f=" + format;
filename.Append(",datemodified=");
filename.Append(dateModified.Ticks);
filename.Append(",f=");
filename.Append(format);
if (width.HasValue)
{
filename.Append(",width=");
filename.Append(width.Value);
}
if (height.HasValue)
{
filename.Append(",height=");
filename.Append(height.Value);
}
if (maxWidth.HasValue)
{
filename.Append(",maxwidth=");
filename.Append(maxWidth.Value);
}
if (maxHeight.HasValue)
{
filename.Append(",maxheight=");
filename.Append(maxHeight.Value);
}
if (fillWidth.HasValue)
{
filename.Append(",fillwidth=");
filename.Append(fillWidth.Value);
}
if (fillHeight.HasValue)
{
filename.Append(",fillheight=");
filename.Append(fillHeight.Value);
}
if (addPlayedIndicator) if (addPlayedIndicator)
{ {
filename += "pl=true"; filename.Append(",pl=true");
} }
if (percentPlayed > 0) if (percentPlayed > 0)
{ {
filename += "p=" + percentPlayed; filename.Append(",p=");
filename.Append(percentPlayed);
} }
if (unwatchedCount.HasValue) if (unwatchedCount.HasValue)
{ {
filename += "p=" + unwatchedCount.Value; filename.Append(",p=");
filename.Append(unwatchedCount.Value);
} }
if (blur.HasValue) if (blur.HasValue)
{ {
filename += "blur=" + blur.Value; filename.Append(",blur=");
filename.Append(blur.Value);
} }
if (!string.IsNullOrEmpty(backgroundColor)) if (!string.IsNullOrEmpty(backgroundColor))
{ {
filename += "b=" + backgroundColor; filename.Append(",b=");
filename.Append(backgroundColor);
} }
if (!string.IsNullOrEmpty(foregroundLayer)) if (!string.IsNullOrEmpty(foregroundLayer))
{ {
filename += "fl=" + foregroundLayer; filename.Append(",fl=");
filename.Append(foregroundLayer);
} }
filename += "v=" + Version; filename.Append(",v=");
filename.Append(Version);
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant()); return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
} }
/// <inheritdoc /> /// <inheritdoc />
@ -352,8 +426,13 @@ namespace Emby.Drawing
} }
/// <inheritdoc /> /// <inheritdoc />
public string GetImageCacheTag(User user) public string? GetImageCacheTag(User user)
{ {
if (user.ProfileImage == null)
{
return null;
}
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5() return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
.ToString("N", CultureInfo.InvariantCulture); .ToString("N", CultureInfo.InvariantCulture);
} }

View File

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

View File

@ -44,7 +44,6 @@
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

View File

@ -68,6 +68,11 @@ namespace Emby.Naming.TV
var parsingResult = new EpisodePathParser(_options) var parsingResult = new EpisodePathParser(_options)
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo); .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
if (!parsingResult.Success && !isStub)
{
return null;
}
return new EpisodeInfo(path) return new EpisodeInfo(path)
{ {
Container = container, Container = container,

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Emby.Naming.Video namespace Emby.Naming.Video
@ -16,8 +17,14 @@ namespace Emby.Naming.Video
/// <param name="expressions">List of regex to parse name and year from.</param> /// <param name="expressions">List of regex to parse name and year from.</param>
/// <param name="newName">Parsing result string.</param> /// <param name="newName">Parsing result string.</param>
/// <returns>True if parsing was successful.</returns> /// <returns>True if parsing was successful.</returns>
public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName) public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
{ {
if (string.IsNullOrEmpty(name))
{
newName = ReadOnlySpan<char>.Empty;
return false;
}
var len = expressions.Count; var len = expressions.Count;
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
{ {
@ -41,7 +48,7 @@ namespace Emby.Naming.Video
return true; return true;
} }
newName = string.Empty; newName = ReadOnlySpan<char>.Empty;
return false; return false;
} }
} }

View File

@ -221,20 +221,21 @@ namespace Emby.Naming.Video
string testFilename = Path.GetFileNameWithoutExtension(testFilePath); string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{ {
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName)) // Remove the folder name before cleaning as we don't care about cleaning that part
{
testFilename = cleanName.ToString();
}
if (folderName.Length <= testFilename.Length) if (folderName.Length <= testFilename.Length)
{ {
testFilename = testFilename.Substring(folderName.Length).Trim(); testFilename = testFilename.Substring(folderName.Length).Trim();
} }
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.Trim().ToString();
}
// The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(testFilename) return string.IsNullOrEmpty(testFilename)
|| testFilename[0] == '-' || testFilename[0] == '-'
|| testFilename[0] == '_' || Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
} }
return false; return false;

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Emby.Naming.Common; using Emby.Naming.Common;
@ -146,7 +147,7 @@ namespace Emby.Naming.Video
/// <param name="name">Raw name.</param> /// <param name="name">Raw name.</param>
/// <param name="newName">Clean name.</param> /// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns> /// <returns>True if cleaning of name was successful.</returns>
public bool TryCleanString(string name, out ReadOnlySpan<char> newName) public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
{ {
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName); return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
} }

View File

@ -11,6 +11,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -25,14 +27,9 @@
<!-- Code analyzers--> <!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project> </Project>

View File

@ -24,18 +24,15 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project> </Project>

View File

@ -3,7 +3,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase namespace Emby.Server.Implementations.AppBase
@ -53,7 +52,8 @@ namespace Emby.Server.Implementations.AppBase
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
// Save it after load in case we got new items // Save it after load in case we got new items
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{ {
fs.Write(newBytes, 0, newBytesLen); fs.Write(newBytes, 0, newBytesLen);
} }

View File

@ -10,8 +10,6 @@ using System.Net;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna; using Emby.Dlna;
@ -43,6 +41,7 @@ using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session; using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV; using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Configuration;
@ -50,7 +49,6 @@ using Jellyfin.Networking.Manager;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events; using MediaBrowser.Common.Events;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
@ -99,6 +97,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime; using Prometheus.DotNetRuntime;
@ -118,6 +117,7 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager; private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions; private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager; private readonly IPluginManager _pluginManager;
@ -126,7 +126,6 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder; private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
private string[] _urlPrefixes; private string[] _urlPrefixes;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
/// <summary> /// <summary>
/// Gets a value indicating whether this instance can self restart. /// Gets a value indicating whether this instance can self restart.
@ -135,9 +134,6 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; } public bool CoreStartupHasCompleted { get; private set; }
/// <inheritdoc />
public Uri PublishedServerUrl => _startupOptions.PublishedServerUrl;
public virtual bool CanLaunchWebBrowser public virtual bool CanLaunchWebBrowser
{ {
get get
@ -214,7 +210,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the configuration manager. /// Gets or sets the configuration manager.
/// </summary> /// </summary>
/// <value>The configuration manager.</value> /// <value>The configuration manager.</value>
protected IConfigurationManager ConfigurationManager { get; set; } public ServerConfigurationManager ConfigurationManager { get; set; }
/// <summary> /// <summary>
/// Gets or sets the service provider. /// Gets or sets the service provider.
@ -232,10 +228,9 @@ namespace Emby.Server.Implementations
public int HttpsPort { get; private set; } public int HttpsPort { get; private set; }
/// <summary> /// <summary>
/// Gets the server configuration manager. /// Gets the value of the PublishedServerUrl setting.
/// </summary> /// </summary>
/// <value>The server configuration manager.</value> public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class. /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@ -243,51 +238,37 @@ namespace Emby.Server.Implementations
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param> /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param> /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost( public ApplicationHost(
IServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStartupOptions options, IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem, IFileSystem fileSystem,
IServiceCollection serviceCollection) IServiceCollection serviceCollection)
{ {
_xmlSerializer = new MyXmlSerializer();
ServiceCollection = serviceCollection;
ApplicationPaths = applicationPaths; ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
_fileSystemManager = fileSystem; _fileSystemManager = fileSystem;
ServiceCollection = serviceCollection;
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
// Have to migrate settings here as migration subsystem not yet initialised.
MigrateNetworkConfiguration();
// Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
Logger = LoggerFactory.CreateLogger<ApplicationHost>(); Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_startupOptions = options;
// Initialize runtime stat collection
if (ServerConfigurationManager.Configuration.EnableMetrics)
{
DotNetRuntimeStatsBuilder.Default().StartCollecting();
}
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3); ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer();
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
_pluginManager = new PluginManager( _pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(), LoggerFactory.CreateLogger<PluginManager>(),
this, this,
ServerConfigurationManager.Configuration, ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath, ApplicationPaths.PluginsPath,
ApplicationVersion); ApplicationVersion);
} }
@ -302,9 +283,9 @@ namespace Emby.Server.Implementations
if (!File.Exists(path)) if (!File.Exists(path))
{ {
var networkSettings = new NetworkConfiguration(); var networkSettings = new NetworkConfiguration();
ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings); ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
_xmlSerializer.SerializeToFile(networkSettings, path); _xmlSerializer.SerializeToFile(networkSettings, path);
Logger?.LogDebug("Successfully migrated network settings."); Logger.LogDebug("Successfully migrated network settings.");
} }
} }
@ -354,10 +335,7 @@ namespace Emby.Server.Implementations
{ {
get get
{ {
if (_deviceId == null) _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
{
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
}
return _deviceId.Value; return _deviceId.Value;
} }
@ -389,10 +367,7 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns> /// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type) protected object CreateInstanceSafe(Type type)
{ {
if (_creatingInstances == null) _creatingInstances ??= new List<Type>();
{
_creatingInstances = new List<Type>();
}
if (_creatingInstances.IndexOf(type) != -1) if (_creatingInstances.IndexOf(type) != -1)
{ {
@ -463,7 +438,7 @@ namespace Emby.Server.Implementations
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true) public IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
{ {
// Convert to list so this isn't executed for each iteration // Convert to list so this isn't executed for each iteration
var parts = GetExportTypes<T>() var parts = GetExportTypes<T>()
@ -487,8 +462,9 @@ namespace Emby.Server.Implementations
/// Runs the startup tasks. /// Runs the startup tasks.
/// </summary> /// </summary>
/// <returns><see cref="Task" />.</returns> /// <returns><see cref="Task" />.</returns>
public async Task RunStartupTasksAsync() public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
{ {
cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks"); Logger.LogInformation("Running startup tasks");
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
@ -502,14 +478,21 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports<IServerEntryPoint>(); var entryPoints = GetExports<IServerEntryPoint>();
cancellationToken.ThrowIfCancellationRequested();
var stopWatch = new Stopwatch(); var stopWatch = new Stopwatch();
stopWatch.Start(); stopWatch.Start();
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false); await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete"); Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true; CoreStartupHasCompleted = true;
cancellationToken.ThrowIfCancellationRequested();
stopWatch.Restart(); stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
stopWatch.Stop(); stopWatch.Stop();
@ -533,7 +516,21 @@ namespace Emby.Server.Implementations
/// <inheritdoc/> /// <inheritdoc/>
public void Init() public void Init()
{ {
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); DiscoverTypes();
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
// Have to migrate settings here as migration subsystem not yet initialised.
MigrateNetworkConfiguration();
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
{
DotNetRuntimeStatsBuilder.Default().StartCollecting();
}
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
HttpPort = networkConfiguration.HttpServerPortNumber; HttpPort = networkConfiguration.HttpServerPortNumber;
HttpsPort = networkConfiguration.HttpsPortNumber; HttpsPort = networkConfiguration.HttpsPortNumber;
@ -551,8 +548,6 @@ namespace Emby.Server.Implementations
}; };
Certificate = GetCertificate(CertificateInfo); Certificate = GetCertificate(CertificateInfo);
DiscoverTypes();
RegisterServices(); RegisterServices();
_pluginManager.RegisterServices(ServiceCollection); _pluginManager.RegisterServices(ServiceCollection);
@ -567,7 +562,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddMemoryCache(); ServiceCollection.AddMemoryCache();
ServiceCollection.AddSingleton(ConfigurationManager); ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this); ServiceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager); ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
@ -594,8 +590,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IServerApplicationHost>(this); ServiceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton(ServerConfigurationManager);
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
@ -607,12 +601,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>(); ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@ -677,14 +667,14 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
ServiceCollection.AddSingleton<TranscodingJobHelper>(); ServiceCollection.AddSingleton<TranscodingJobHelper>();
ServiceCollection.AddScoped<MediaInfoHelper>(); ServiceCollection.AddScoped<MediaInfoHelper>();
ServiceCollection.AddScoped<AudioHelper>(); ServiceCollection.AddScoped<AudioHelper>();
ServiceCollection.AddScoped<DynamicHlsHelper>(); ServiceCollection.AddScoped<DynamicHlsHelper>();
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
} }
/// <summary> /// <summary>
@ -782,7 +772,7 @@ namespace Emby.Server.Implementations
{ {
// For now there's no real way to inject these properly // For now there's no real way to inject these properly
BaseItem.Logger = Resolve<ILogger<BaseItem>>(); BaseItem.Logger = Resolve<ILogger<BaseItem>>();
BaseItem.ConfigurationManager = ServerConfigurationManager; BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.LibraryManager = Resolve<ILibraryManager>(); BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.ProviderManager = Resolve<IProviderManager>(); BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
@ -804,13 +794,12 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
private void FindParts() private void FindParts()
{ {
if (!ServerConfigurationManager.Configuration.IsPortAuthorized) if (!ConfigurationManager.Configuration.IsPortAuthorized)
{ {
ServerConfigurationManager.Configuration.IsPortAuthorized = true; ConfigurationManager.Configuration.IsPortAuthorized = true;
ConfigurationManager.SaveConfiguration(); ConfigurationManager.SaveConfiguration();
} }
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
_pluginManager.CreatePlugins(); _pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray(); _urlPrefixes = GetUrlPrefixes().ToArray();
@ -914,7 +903,7 @@ namespace Emby.Server.Implementations
protected void OnConfigurationUpdated(object sender, EventArgs e) protected void OnConfigurationUpdated(object sender, EventArgs e)
{ {
var requiresRestart = false; var requiresRestart = false;
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
// Don't do anything if these haven't been set yet // Don't do anything if these haven't been set yet
if (HttpPort != 0 && HttpsPort != 0) if (HttpPort != 0 && HttpsPort != 0)
@ -923,10 +912,10 @@ namespace Emby.Server.Implementations
if (networkConfiguration.HttpServerPortNumber != HttpPort || if (networkConfiguration.HttpServerPortNumber != HttpPort ||
networkConfiguration.HttpsPortNumber != HttpsPort) networkConfiguration.HttpsPortNumber != HttpsPort)
{ {
if (ServerConfigurationManager.Configuration.IsPortAuthorized) if (ConfigurationManager.Configuration.IsPortAuthorized)
{ {
ServerConfigurationManager.Configuration.IsPortAuthorized = false; ConfigurationManager.Configuration.IsPortAuthorized = false;
ServerConfigurationManager.SaveConfiguration(); ConfigurationManager.SaveConfiguration();
requiresRestart = true; requiresRestart = true;
} }
@ -1142,16 +1131,16 @@ namespace Emby.Server.Implementations
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps; public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null) public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
{ {
// Published server ends with a / // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null) if (!string.IsNullOrEmpty(PublishedServerUrl))
{ {
// Published server ends with a '/', so we need to remove it. // Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(ipAddress, out port); string smart = NetManager.GetBindInterface(ipAddress, out port);
@ -1168,10 +1157,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(HttpRequest request, int? port = null) public string GetSmartApiUrl(HttpRequest request, int? port = null)
{ {
// Published server ends with a / // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null) if (!string.IsNullOrEmpty(PublishedServerUrl))
{ {
// Published server ends with a '/', so we need to remove it. // Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(request, out port); string smart = NetManager.GetBindInterface(request, out port);
@ -1188,10 +1177,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(string hostname, int? port = null) public string GetSmartApiUrl(string hostname, int? port = null)
{ {
// Published server ends with a / // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null) if (!string.IsNullOrEmpty(PublishedServerUrl))
{ {
// Published server ends with a '/', so we need to remove it. // Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(hostname, out port); string smart = NetManager.GetBindInterface(hostname, out port);
@ -1226,14 +1215,14 @@ namespace Emby.Server.Implementations
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
Host = host, Host = host,
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
}.ToString().TrimEnd('/'); }.ToString().TrimEnd('/');
} }
public string FriendlyName => public string FriendlyName =>
string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName) string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName ? Environment.MachineName
: ServerConfigurationManager.Configuration.ServerName; : ConfigurationManager.Configuration.ServerName;
/// <summary> /// <summary>
/// Shuts down. /// Shuts down.

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -49,7 +48,7 @@ namespace Emby.Server.Implementations.Channels
private readonly IProviderManager _providerManager; private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ChannelManager"/> class. /// Initializes a new instance of the <see cref="ChannelManager"/> class.

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -8,11 +7,9 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -124,7 +121,7 @@ namespace Emby.Server.Implementations.Collections
private IEnumerable<BoxSet> GetCollections(User user) private IEnumerable<BoxSet> GetCollections(User user)
{ {
var folder = GetCollectionsFolder(false).Result; var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
return folder == null return folder == null
? Enumerable.Empty<BoxSet>() ? Enumerable.Empty<BoxSet>()
@ -319,11 +316,11 @@ namespace Emby.Server.Implementations.Collections
{ {
var results = new Dictionary<Guid, BaseItem>(); var results = new Dictionary<Guid, BaseItem>();
var allBoxsets = GetCollections(user).ToList(); var allBoxSets = GetCollections(user).ToList();
foreach (var item in items) foreach (var item in items)
{ {
if (!(item is ISupportsBoxSetGrouping)) if (item is not ISupportsBoxSetGrouping)
{ {
results[item.Id] = item; results[item.Id] = item;
} }
@ -331,20 +328,44 @@ namespace Emby.Server.Implementations.Collections
{ {
var itemId = item.Id; var itemId = item.Id;
var currentBoxSets = allBoxsets var itemIsInBoxSet = false;
.Where(i => i.ContainsLinkedChildByItemId(itemId)) foreach (var boxSet in allBoxSets)
.ToList(); {
if (!boxSet.ContainsLinkedChildByItemId(itemId))
{
continue;
}
if (currentBoxSets.Count > 0) itemIsInBoxSet = true;
results.TryAdd(boxSet.Id, boxSet);
}
// skip any item that is in a box set
if (itemIsInBoxSet)
{ {
foreach (var boxset in currentBoxSets) continue;
}
var alreadyInResults = false;
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
if (item is Video video)
{ {
results[boxset.Id] = boxset; foreach (var childId in video.GetLocalAlternateVersionIds())
{
if (!results.ContainsKey(childId))
{
continue;
}
alreadyInResults = true;
break;
} }
} }
else
if (!alreadyInResults)
{ {
results[item.Id] = item; results[itemId] = item;
} }
} }
} }

View File

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

View File

@ -1,6 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Buffers.Text;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -88,7 +89,7 @@ namespace Emby.Server.Implementations.Data
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_typeMapper = new TypeMapper(); _typeMapper = new TypeMapper();
_jsonOptions = JsonDefaults.GetOptions(); _jsonOptions = JsonDefaults.Options;
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
} }
@ -502,7 +503,7 @@ namespace Emby.Server.Implementations.Data
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{ {
saveImagesStatement.TryBind("@Id", item.Id.ToByteArray()); saveImagesStatement.TryBind("@Id", item.Id.ToByteArray());
saveImagesStatement.TryBind("@Images", SerializeImages(item)); saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
saveImagesStatement.MoveNext(); saveImagesStatement.MoveNext();
} }
@ -897,8 +898,8 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId);
saveItemStatement.TryBind("@Tagline", item.Tagline); saveItemStatement.TryBind("@Tagline", item.Tagline);
saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item)); saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds));
saveItemStatement.TryBind("@Images", SerializeImages(item)); saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
if (item.ProductionLocations.Length > 0) if (item.ProductionLocations.Length > 0)
{ {
@ -968,10 +969,10 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.MoveNext(); saveItemStatement.MoveNext();
} }
private static string SerializeProviderIds(BaseItem item) internal static string SerializeProviderIds(Dictionary<string, string> providerIds)
{ {
StringBuilder str = new StringBuilder(); StringBuilder str = new StringBuilder();
foreach (var i in item.ProviderIds) foreach (var i in providerIds)
{ {
// Ideally we shouldn't need this IsNullOrWhiteSpace check, // Ideally we shouldn't need this IsNullOrWhiteSpace check,
// but we're seeing some cases of bad data slip through // but we're seeing some cases of bad data slip through
@ -995,35 +996,25 @@ namespace Emby.Server.Implementations.Data
return str.ToString(); return str.ToString();
} }
private static void DeserializeProviderIds(string value, BaseItem item) internal static void DeserializeProviderIds(string value, IHasProviderIds item)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
{ {
return; return;
} }
if (item.ProviderIds.Count > 0) foreach (var part in value.SpanSplit('|'))
{ {
return; var providerDelimiterIndex = part.IndexOf('=');
} if (providerDelimiterIndex != -1 && providerDelimiterIndex == part.LastIndexOf('='))
var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{ {
var idParts = part.Split('='); item.SetProviderId(part.Slice(0, providerDelimiterIndex).ToString(), part.Slice(providerDelimiterIndex + 1).ToString());
if (idParts.Length == 2)
{
item.SetProviderId(idParts[0], idParts[1]);
} }
} }
} }
private string SerializeImages(BaseItem item) internal string SerializeImages(ItemImageInfo[] images)
{ {
var images = item.ImageInfos;
if (images.Length == 0) if (images.Length == 0)
{ {
return null; return null;
@ -1045,21 +1036,15 @@ namespace Emby.Server.Implementations.Data
return str.ToString(); return str.ToString();
} }
private void DeserializeImages(string value, BaseItem item) internal ItemImageInfo[] DeserializeImages(string value)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
{ {
return; return Array.Empty<ItemImageInfo>();
} }
if (item.ImageInfos.Length > 0)
{
return;
}
var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries);
var list = new List<ItemImageInfo>(); var list = new List<ItemImageInfo>();
foreach (var part in parts) foreach (var part in value.SpanSplit('|'))
{ {
var image = ItemImageInfoFromValueString(part); var image = ItemImageInfoFromValueString(part);
@ -1069,15 +1054,14 @@ namespace Emby.Server.Implementations.Data
} }
} }
item.ImageInfos = list.ToArray(); return list.ToArray();
} }
public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
{ {
const char Delimiter = '*'; const char Delimiter = '*';
var path = image.Path ?? string.Empty; var path = image.Path ?? string.Empty;
var hash = image.BlurHash ?? string.Empty;
bldr.Append(GetPathToSave(path)) bldr.Append(GetPathToSave(path))
.Append(Delimiter) .Append(Delimiter)
@ -1087,48 +1071,105 @@ namespace Emby.Server.Implementations.Data
.Append(Delimiter) .Append(Delimiter)
.Append(image.Width) .Append(image.Width)
.Append(Delimiter) .Append(Delimiter)
.Append(image.Height) .Append(image.Height);
.Append(Delimiter)
var hash = image.BlurHash;
if (!string.IsNullOrEmpty(hash))
{
bldr.Append(Delimiter)
// Replace delimiters with other characters. // Replace delimiters with other characters.
// This can be removed when we migrate to a proper DB. // This can be removed when we migrate to a proper DB.
.Append(hash.Replace('*', '/').Replace('|', '\\')); .Append(hash.Replace('*', '/').Replace('|', '\\'));
} }
}
public ItemImageInfo ItemImageInfoFromValueString(string value) internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan<char> value)
{ {
var parts = value.Split('*', StringSplitOptions.None); var nextSegment = value.IndexOf('*');
if (nextSegment == -1)
if (parts.Length < 3)
{ {
return null; return null;
} }
var image = new ItemImageInfo(); ReadOnlySpan<char> path = value[..nextSegment];
value = value[(nextSegment + 1)..];
nextSegment = value.IndexOf('*');
if (nextSegment == -1)
{
return null;
}
image.Path = RestorePath(parts[0]); ReadOnlySpan<char> 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<char> 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); 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; 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) value = value[(nextSegment + 1)..];
&& int.TryParse(parts[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) nextSegment = value.IndexOf('*');
if (nextSegment == -1 || nextSegment == value.Length)
{
return image;
}
ReadOnlySpan<char> widthSpan = value[..nextSegment];
value = value[(nextSegment + 1)..];
nextSegment = value.IndexOf('*');
if (nextSegment == -1)
{
nextSegment = value.Length;
}
ReadOnlySpan<char> 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.Width = width;
image.Height = height; 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<char> 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);
} }
} }
@ -1311,7 +1352,14 @@ namespace Emby.Server.Implementations.Data
if (!reader.IsDBNull(index)) if (!reader.IsDBNull(index))
{ {
item.ChannelId = new Guid(reader.GetString(index)); if (!Utf8Parser.TryParse(reader[index].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++; index++;
@ -1790,7 +1838,7 @@ namespace Emby.Server.Implementations.Data
index++; index++;
} }
if (!reader.IsDBNull(index)) if (item.ProviderIds.Count == 0 && !reader.IsDBNull(index))
{ {
DeserializeProviderIds(reader.GetString(index), item); DeserializeProviderIds(reader.GetString(index), item);
} }
@ -1799,9 +1847,9 @@ namespace Emby.Server.Implementations.Data
if (query.DtoOptions.EnableImages) if (query.DtoOptions.EnableImages)
{ {
if (!reader.IsDBNull(index)) if (item.ImageInfos.Length == 0 && !reader.IsDBNull(index))
{ {
DeserializeImages(reader.GetString(index), item); item.ImageInfos = DeserializeImages(reader.GetString(index));
} }
index++; index++;
@ -2116,30 +2164,7 @@ namespace Emby.Server.Implementations.Data
|| query.IsLiked.HasValue; || query.IsLiked.HasValue;
} }
private readonly ItemFields[] _allFields = Enum.GetNames(typeof(ItemFields)) private readonly ItemFields[] _allFields = Enum.GetValues<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<string>();
default:
return new[] { field.ToString() };
}
}
private bool HasField(InternalItemsQuery query, ItemFields name) private bool HasField(InternalItemsQuery query, ItemFields name)
{ {
@ -2329,9 +2354,32 @@ namespace Emby.Server.Implementations.Data
{ {
if (!HasField(query, field)) if (!HasField(query, field))
{ {
foreach (var fieldToRemove in GetColumnNamesFromField(field)) switch (field)
{ {
list.Remove(fieldToRemove); case ItemFields.Settings:
list.Remove("IsLocked");
list.Remove("PreferredMetadataCountryCode");
list.Remove("PreferredMetadataLanguage");
list.Remove("LockedFields");
break;
case ItemFields.ServiceName:
list.Remove("ExternalServiceId");
break;
case ItemFields.SortName:
list.Remove("ForcedSortName");
break;
case ItemFields.Taglines:
list.Remove("Tagline");
break;
case ItemFields.Tags:
list.Remove("Tags");
break;
case ItemFields.IsHD:
// do nothing
break;
default:
list.Remove(field.ToString());
break;
} }
} }
} }
@ -2720,88 +2768,23 @@ namespace Emby.Server.Implementations.Data
} }
private string FixUnicodeChars(string buffer) private string FixUnicodeChars(string buffer)
{
if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
{ {
buffer = buffer.Replace('\u2013', '-'); // en dash buffer = buffer.Replace('\u2013', '-'); // en dash
}
if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2014', '-'); // em dash buffer = buffer.Replace('\u2014', '-'); // em dash
}
if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2015', '-'); // horizontal bar buffer = buffer.Replace('\u2015', '-'); // horizontal bar
}
if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2017', '_'); // double low line buffer = buffer.Replace('\u2017', '_'); // double low line
}
if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2018', '\''); // left single quotation mark buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
}
if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2019', '\''); // right single quotation mark buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
}
if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark 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 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 buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
}
if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
}
if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
}
if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
}
if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2032', '\''); // prime buffer = buffer.Replace('\u2032', '\''); // prime
}
if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2033', '\"'); // double prime buffer = buffer.Replace('\u2033', '\"'); // double prime
}
if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u0060', '\''); // grave accent buffer = buffer.Replace('\u0060', '\''); // grave accent
} return buffer.Replace('\u00B4', '\''); // acute accent
if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u00B4', '\''); // acute accent
}
return buffer;
} }
private void AddItem(List<BaseItem> items, BaseItem newItem) private void AddItem(List<BaseItem> items, BaseItem newItem)
@ -3584,11 +3567,11 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind("@IsFolder", query.IsFolder); 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 // Only specify excluded types if no included types are specified
if (includeTypes.Length == 0) 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) if (excludeTypes.Length == 1)
{ {
whereClauses.Add("type<>@type"); whereClauses.Add("type<>@type");
@ -4532,7 +4515,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb")); whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb"));
} }
var includedItemByNameTypes = GetItemByNameTypesInQuery(query).SelectMany(MapIncludeItemTypes).ToList(); var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
var queryTopParentIds = query.TopParentIds; var queryTopParentIds = query.TopParentIds;
@ -4790,27 +4773,27 @@ namespace Emby.Server.Implementations.Data
if (IsTypeInQuery(nameof(Person), query)) if (IsTypeInQuery(nameof(Person), query))
{ {
list.Add(nameof(Person)); list.Add(typeof(Person).FullName);
} }
if (IsTypeInQuery(nameof(Genre), query)) if (IsTypeInQuery(nameof(Genre), query))
{ {
list.Add(nameof(Genre)); list.Add(typeof(Genre).FullName);
} }
if (IsTypeInQuery(nameof(MusicGenre), query)) if (IsTypeInQuery(nameof(MusicGenre), query))
{ {
list.Add(nameof(MusicGenre)); list.Add(typeof(MusicGenre).FullName);
} }
if (IsTypeInQuery(nameof(MusicArtist), query)) if (IsTypeInQuery(nameof(MusicArtist), query))
{ {
list.Add(nameof(MusicArtist)); list.Add(typeof(MusicArtist).FullName);
} }
if (IsTypeInQuery(nameof(Studio), query)) if (IsTypeInQuery(nameof(Studio), query))
{ {
list.Add(nameof(Studio)); list.Add(typeof(Studio).FullName);
} }
return list; return list;
@ -4915,15 +4898,10 @@ namespace Emby.Server.Implementations.Data
typeof(AggregateFolder) typeof(AggregateFolder)
}; };
public void UpdateInheritedValues(CancellationToken cancellationToken) public void UpdateInheritedValues()
{
UpdateInheritedTags(cancellationToken);
}
private void UpdateInheritedTags(CancellationToken cancellationToken)
{ {
string sql = string.Join( string sql = string.Join(
";", ';',
new string[] new string[]
{ {
"delete from itemvalues where type = 6", "delete from itemvalues where type = 6",
@ -4946,37 +4924,38 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
} }
} }
private static Dictionary<string, string[]> GetTypeMapDictionary() private static Dictionary<string, string> GetTypeMapDictionary()
{ {
var dict = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase); var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var t in _knownTypes) foreach (var t in _knownTypes)
{ {
dict[t.Name] = new[] { t.FullName }; dict[t.Name] = t.FullName ;
} }
dict["Program"] = new[] { typeof(LiveTvProgram).FullName }; dict["Program"] = typeof(LiveTvProgram).FullName;
dict["TvChannel"] = new[] { typeof(LiveTvChannel).FullName }; dict["TvChannel"] = typeof(LiveTvChannel).FullName;
return dict; return dict;
} }
// Not crazy about having this all the way down here, but at least it's in one place // Not crazy about having this all the way down here, but at least it's in one place
private readonly Dictionary<string, string[]> _types = GetTypeMapDictionary(); private readonly Dictionary<string, string> _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; return result;
} }
if (IsValidType(value)) if (IsValidType(value))
{ {
return new[] { value }; return value;
} }
return Array.Empty<string>(); Logger.LogWarning("Unknown item type: {ItemType}", value);
return null;
} }
public void DeleteItem(Guid id) public void DeleteItem(Guid id)
@ -5279,31 +5258,46 @@ AND Type = @InternalPersonType)");
public List<string> GetStudioNames() public List<string> GetStudioNames()
{ {
return GetItemValueNames(new[] { 3 }, new List<string>(), new List<string>()); return GetItemValueNames(new[] { 3 }, Array.Empty<string>(), Array.Empty<string>());
} }
public List<string> GetAllArtistNames() public List<string> GetAllArtistNames()
{ {
return GetItemValueNames(new[] { 0, 1 }, new List<string>(), new List<string>()); return GetItemValueNames(new[] { 0, 1 }, Array.Empty<string>(), Array.Empty<string>());
} }
public List<string> GetMusicGenreNames() public List<string> GetMusicGenreNames()
{ {
return GetItemValueNames(new[] { 2 }, new List<string> { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" }, new List<string>()); return GetItemValueNames(
new[] { 2 },
new string[]
{
typeof(Audio).FullName,
typeof(MusicVideo).FullName,
typeof(MusicAlbum).FullName,
typeof(MusicArtist).FullName
},
Array.Empty<string>());
} }
public List<string> GetGenreNames() public List<string> GetGenreNames()
{ {
return GetItemValueNames(new[] { 2 }, new List<string>(), new List<string> { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" }); return GetItemValueNames(
new[] { 2 },
Array.Empty<string>(),
new string[]
{
typeof(Audio).FullName,
typeof(MusicVideo).FullName,
typeof(MusicAlbum).FullName,
typeof(MusicArtist).FullName
});
} }
private List<string> GetItemValueNames(int[] itemValueTypes, List<string> withItemTypes, List<string> excludeItemTypes) private List<string> GetItemValueNames(int[] itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes)
{ {
CheckDisposed(); CheckDisposed();
withItemTypes = withItemTypes.SelectMany(MapIncludeItemTypes).ToList();
excludeItemTypes = excludeItemTypes.SelectMany(MapIncludeItemTypes).ToList();
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var typeClause = itemValueTypes.Length == 1 ? var typeClause = itemValueTypes.Length == 1 ?
@ -5415,7 +5409,6 @@ AND Type = @InternalPersonType)");
ItemIds = query.ItemIds, ItemIds = query.ItemIds,
TopParentIds = query.TopParentIds, TopParentIds = query.TopParentIds,
ParentId = query.ParentId, ParentId = query.ParentId,
IsPlayed = query.IsPlayed,
IsAiring = query.IsAiring, IsAiring = query.IsAiring,
IsMovie = query.IsMovie, IsMovie = query.IsMovie,
IsSports = query.IsSports, IsSports = query.IsSports,
@ -5441,6 +5434,7 @@ AND Type = @InternalPersonType)");
var outerQuery = new InternalItemsQuery(query.User) var outerQuery = new InternalItemsQuery(query.User)
{ {
IsPlayed = query.IsPlayed,
IsFavorite = query.IsFavorite, IsFavorite = query.IsFavorite,
IsFavoriteOrLiked = query.IsFavoriteOrLiked, IsFavoriteOrLiked = query.IsFavoriteOrLiked,
IsLiked = query.IsLiked, IsLiked = query.IsLiked,
@ -5809,7 +5803,10 @@ AND Type = @InternalPersonType)");
var endIndex = Math.Min(people.Count, startIndex + Limit); var endIndex = Math.Min(people.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++) 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 // Remove last comma
@ -6261,7 +6258,7 @@ AND Type = @InternalPersonType)");
CheckDisposed(); CheckDisposed();
if (id == Guid.Empty) if (id == Guid.Empty)
{ {
throw new ArgumentException(nameof(id)); throw new ArgumentException("Guid can't be empty.", nameof(id));
} }
if (attachments == null) if (attachments == null)

View File

@ -665,10 +665,7 @@ namespace Emby.Server.Implementations.Dto
var tag = GetImageCacheTag(item, image); var tag = GetImageCacheTag(item, image);
if (!string.IsNullOrEmpty(image.BlurHash)) if (!string.IsNullOrEmpty(image.BlurHash))
{ {
if (dto.ImageBlurHashes == null) dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
{
dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
}
if (!dto.ImageBlurHashes.ContainsKey(image.Type)) if (!dto.ImageBlurHashes.ContainsKey(image.Type))
{ {
@ -702,10 +699,7 @@ namespace Emby.Server.Implementations.Dto
if (hashes.Count > 0) if (hashes.Count > 0)
{ {
if (dto.ImageBlurHashes == null) dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
{
dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
}
dto.ImageBlurHashes[imageType] = hashes; dto.ImageBlurHashes[imageType] = hashes;
} }
@ -898,10 +892,7 @@ namespace Emby.Server.Implementations.Dto
dto.Taglines = new string[] { item.Tagline }; dto.Taglines = new string[] { item.Tagline };
} }
if (dto.Taglines == null) dto.Taglines ??= Array.Empty<string>();
{
dto.Taglines = Array.Empty<string>();
}
} }
dto.Type = item.GetBaseItemKind(); dto.Type = item.GetBaseItemKind();

View File

@ -27,10 +27,11 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
<PackageReference Include="Mono.Nat" Version="3.0.1" /> <PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.0.0" />
<PackageReference Include="sharpcompress" Version="0.28.1" /> <PackageReference Include="sharpcompress" Version="0.28.2" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.2.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" /> <PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup> </ItemGroup>
@ -45,20 +46,17 @@
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn> <NoWarn>AD0001</NoWarn>
<AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Localization\iso6392.txt" /> <EmbeddedResource Include="Localization\iso6392.txt" />
<EmbeddedResource Include="Localization\countries.json" /> <EmbeddedResource Include="Localization\countries.json" />

View File

@ -1,5 +1,6 @@
#nullable enable #nullable enable
using System;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -51,6 +52,8 @@ namespace Emby.Server.Implementations.EntryPoints
/// <inheritdoc /> /// <inheritdoc />
public Task RunAsync() public Task RunAsync()
{ {
CheckDisposed();
try try
{ {
_udpServer = new UdpServer(_logger, _appHost, _config); _udpServer = new UdpServer(_logger, _appHost, _config);
@ -64,6 +67,14 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask; return Task.CompletedTask;
} }
private void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(this.GetType().Name);
}
}
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {

View File

@ -1,6 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;

View File

@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
var authorization = _authContext.GetAuthorizationInfo(requestContext); var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User; var user = authorization.User;
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user); return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
} }
public SessionInfo GetSession(object requestContext) public SessionInfo GetSession(object requestContext)

View File

@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.HttpServer
RemoteEndPoint = remoteEndPoint; RemoteEndPoint = remoteEndPoint;
QueryString = query; QueryString = query;
_jsonOptions = JsonDefaults.GetOptions(); _jsonOptions = JsonDefaults.Options;
LastActivityDate = DateTime.Now; LastActivityDate = DateTime.Now;
} }

View File

@ -14,15 +14,18 @@ namespace Emby.Server.Implementations.HttpServer
public class WebSocketManager : IWebSocketManager public class WebSocketManager : IWebSocketManager
{ {
private readonly IWebSocketListener[] _webSocketListeners; private readonly IWebSocketListener[] _webSocketListeners;
private readonly IAuthService _authService;
private readonly ILogger<WebSocketManager> _logger; private readonly ILogger<WebSocketManager> _logger;
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
public WebSocketManager( public WebSocketManager(
IAuthService authService,
IEnumerable<IWebSocketListener> webSocketListeners, IEnumerable<IWebSocketListener> webSocketListeners,
ILogger<WebSocketManager> logger, ILogger<WebSocketManager> logger,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
{ {
_webSocketListeners = webSocketListeners.ToArray(); _webSocketListeners = webSocketListeners.ToArray();
_authService = authService;
_logger = logger; _logger = logger;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
} }
@ -30,6 +33,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc /> /// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context) public async Task WebSocketRequestHandler(HttpContext context)
{ {
_ = _authService.Authenticate(context.Request);
try try
{ {
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);

View File

@ -2,11 +2,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
@ -24,7 +23,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath; private readonly string _tempPath;
private readonly bool _isEnvironmentCaseInsensitive; private static readonly bool _isEnvironmentCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public ManagedFileSystem( public ManagedFileSystem(
ILogger<ManagedFileSystem> logger, ILogger<ManagedFileSystem> logger,
@ -32,8 +31,6 @@ namespace Emby.Server.Implementations.IO
{ {
Logger = logger; Logger = logger;
_tempPath = applicationPaths.TempDirectory; _tempPath = applicationPaths.TempDirectory;
_isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
} }
public virtual void AddShortcutHandler(IShortcutHandler handler) public virtual void AddShortcutHandler(IShortcutHandler handler)
@ -72,7 +69,7 @@ namespace Emby.Server.Implementations.IO
} }
var extension = Path.GetExtension(filename); 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); return handler?.Resolve(filename);
} }
@ -248,14 +245,21 @@ namespace Emby.Server.Implementations.IO
// Issue #2354 get the size of files behind symbolic links // Issue #2354 get the size of files behind symbolic links
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
{
try
{ {
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName)) using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
{ {
result.Length = thisFileStream.Length; result.Length = thisFileStream.Length;
} }
} }
catch (FileNotFoundException ex)
result.DirectoryName = fileInfo.DirectoryName; {
// 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.CreationTimeUtc = GetCreationTimeUtc(info); result.CreationTimeUtc = GetCreationTimeUtc(info);
@ -294,16 +298,37 @@ namespace Emby.Server.Implementations.IO
/// <param name="filename">The filename.</param> /// <param name="filename">The filename.</param>
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">The filename is null.</exception> /// <exception cref="ArgumentNullException">The filename is null.</exception>
public virtual string GetValidFilename(string filename) public string GetValidFilename(string filename)
{ {
var builder = new StringBuilder(filename); var invalid = Path.GetInvalidFileNameChars();
var first = filename.IndexOfAny(invalid);
foreach (var c in Path.GetInvalidFileNameChars()) 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] = ' ';
}
}
}
});
} }
/// <summary> /// <summary>
@ -487,26 +512,9 @@ namespace Emby.Server.Implementations.IO
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
} }
var separatorChar = Path.DirectorySeparatorChar; return path.Contains(
Path.TrimEndingDirectorySeparator(parentPath) + Path.DirectorySeparatorChar,
return path.IndexOf(parentPath.TrimEnd(separatorChar) + separatorChar, StringComparison.OrdinalIgnoreCase) != -1; _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
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;
} }
public virtual string NormalizePath(string path) public virtual string NormalizePath(string path)
@ -521,7 +529,7 @@ namespace Emby.Server.Implementations.IO
return path; return path;
} }
return path.TrimEnd(Path.DirectorySeparatorChar); return Path.TrimEndingDirectorySeparator(path);
} }
public virtual bool AreEqual(string path1, string path2) public virtual bool AreEqual(string path1, string path2)
@ -536,7 +544,10 @@ namespace Emby.Server.Implementations.IO
return false; 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) public virtual string GetFileNameWithoutExtension(FileSystemMetadata info)
@ -689,20 +700,5 @@ namespace Emby.Server.Implementations.IO
AttributesToSkip = 0 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();
}
}
} }
} }

View File

@ -1,6 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
#nullable enable
using System;
namespace Emby.Server.Implementations namespace Emby.Server.Implementations
{ {
@ -9,7 +8,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Gets the value of the --ffmpeg command line option. /// Gets the value of the --ffmpeg command line option.
/// </summary> /// </summary>
string FFmpegPath { get; } string? FFmpegPath { get; }
/// <summary> /// <summary>
/// Gets the value of the --service command line option. /// Gets the value of the --service command line option.
@ -19,21 +18,21 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Gets the value of the --package-name command line option. /// Gets the value of the --package-name command line option.
/// </summary> /// </summary>
string PackageName { get; } string? PackageName { get; }
/// <summary> /// <summary>
/// Gets the value of the --restartpath command line option. /// Gets the value of the --restartpath command line option.
/// </summary> /// </summary>
string RestartPath { get; } string? RestartPath { get; }
/// <summary> /// <summary>
/// Gets the value of the --restartargs command line option. /// Gets the value of the --restartargs command line option.
/// </summary> /// </summary>
string RestartArgs { get; } string? RestartArgs { get; }
/// <summary> /// <summary>
/// Gets the value of the --published-server-url command line option. /// Gets the value of the --published-server-url command line option.
/// </summary> /// </summary>
Uri PublishedServerUrl { get; } string? PublishedServerUrl { get; }
} }
} }

View File

@ -2,20 +2,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Emby.Server.Implementations.Images;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; 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.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images namespace Emby.Server.Implementations.Images
{ {

View File

@ -4,7 +4,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Emby.Server.Implementations.Images;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;

View File

@ -29,9 +29,7 @@ namespace Emby.Server.Implementations.Images
{ {
var subItem = i.Item2; var subItem = i.Item2;
var episode = subItem as Episode; if (subItem is Episode episode)
if (episode != null)
{ {
var series = episode.Series; var series = episode.Series;
if (series != null && series.HasImage(ImageType.Primary)) if (series != null && series.HasImage(ImageType.Primary))

View File

@ -48,6 +48,7 @@ using MediaBrowser.Providers.MediaInfo;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
using Genre = MediaBrowser.Controller.Entities.Genre; using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person; using Person = MediaBrowser.Controller.Entities.Person;
using VideoResolver = Emby.Naming.Video.VideoResolver; using VideoResolver = Emby.Naming.Video.VideoResolver;
@ -175,10 +176,7 @@ namespace Emby.Server.Implementations.Library
{ {
lock (_rootFolderSyncLock) lock (_rootFolderSyncLock)
{ {
if (_rootFolder == null) _rootFolder ??= CreateRootFolder();
{
_rootFolder = CreateRootFolder();
}
} }
} }
@ -196,33 +194,33 @@ namespace Emby.Server.Implementations.Library
/// Gets or sets the postscan tasks. /// Gets or sets the postscan tasks.
/// </summary> /// </summary>
/// <value>The postscan tasks.</value> /// <value>The postscan tasks.</value>
private ILibraryPostScanTask[] PostscanTasks { get; set; } private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>();
/// <summary> /// <summary>
/// Gets or sets the intro providers. /// Gets or sets the intro providers.
/// </summary> /// </summary>
/// <value>The intro providers.</value> /// <value>The intro providers.</value>
private IIntroProvider[] IntroProviders { get; set; } private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>();
/// <summary> /// <summary>
/// Gets or sets the list of entity resolution ignore rules. /// Gets or sets the list of entity resolution ignore rules.
/// </summary> /// </summary>
/// <value>The entity resolution ignore rules.</value> /// <value>The entity resolution ignore rules.</value>
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>();
/// <summary> /// <summary>
/// Gets or sets the list of currently registered entity resolvers. /// Gets or sets the list of currently registered entity resolvers.
/// </summary> /// </summary>
/// <value>The entity resolvers enumerable.</value> /// <value>The entity resolvers enumerable.</value>
private IItemResolver[] EntityResolvers { get; set; } private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>();
private IMultiItemResolver[] MultiItemResolvers { get; set; } private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>();
/// <summary> /// <summary>
/// Gets or sets the comparers. /// Gets or sets the comparers.
/// </summary> /// </summary>
/// <value>The comparers.</value> /// <value>The comparers.</value>
private IBaseItemComparer[] Comparers { get; set; } private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>();
public bool IsScanRunning { get; private set; } public bool IsScanRunning { get; private set; }
@ -558,7 +556,6 @@ namespace Emby.Server.Implementations.Library
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService) var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
{ {
Parent = parent, Parent = parent,
Path = fullPath,
FileInfo = fileInfo, FileInfo = fileInfo,
CollectionType = collectionType, CollectionType = collectionType,
LibraryOptions = libraryOptions LibraryOptions = libraryOptions
@ -684,7 +681,7 @@ namespace Emby.Server.Implementations.Library
foreach (var item in items) 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)); items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions));
@ -1163,7 +1160,7 @@ namespace Emby.Server.Implementations.Library
progress.Report(percent * 100); progress.Report(percent * 100);
} }
_itemRepository.UpdateInheritedValues(cancellationToken); _itemRepository.UpdateInheritedValues();
progress.Report(100); progress.Report(100);
} }
@ -1247,7 +1244,7 @@ namespace Emby.Server.Implementations.Library
{ {
// TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it // TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
// https://github.com/dotnet/runtime/issues/20008 // https://github.com/dotnet/runtime/issues/20008
if (Enum.TryParse<CollectionTypeOptions>(Path.GetExtension(file), true, out var res)) if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
{ {
return res; return res;
} }
@ -1914,12 +1911,17 @@ namespace Emby.Server.Implementations.Library
} }
catch (ArgumentException) catch (ArgumentException)
{ {
_logger.LogWarning("Cannot get image index for {0}", img.Path); _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
continue; 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; continue;
} }
} }
@ -1932,7 +1934,7 @@ namespace Emby.Server.Implementations.Library
} }
catch (Exception ex) 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.Width = 0;
image.Height = 0; image.Height = 0;
continue; continue;
@ -1944,7 +1946,7 @@ namespace Emby.Server.Implementations.Library
} }
catch (Exception ex) 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; image.BlurHash = string.Empty;
} }
@ -1954,7 +1956,7 @@ namespace Emby.Server.Implementations.Library
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Cannot update DateModified for {0}", image.Path); _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
} }
} }
@ -2512,7 +2514,7 @@ namespace Emby.Server.Implementations.Library
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
{ {
var series = episode.Series; 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) if (!isAbsoluteNaming.Value)
{ {
// In other words, no filter applied // In other words, no filter applied
@ -2524,9 +2526,23 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
// TODO nullable - what are we trying to do there with empty episodeInfo? // TODO nullable - what are we trying to do there with empty episodeInfo?
var episodeInfo = episode.IsFileProtocol EpisodeInfo episodeInfo = null;
? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path) if (episode.IsFileProtocol)
: new Naming.TV.EpisodeInfo(episode.Path); {
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 try
{ {
@ -2776,6 +2792,7 @@ namespace Emby.Server.Implementations.Library
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem) public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
{ {
string newPath;
if (ownerItem != null) if (ownerItem != null)
{ {
var libraryOptions = GetLibraryOptions(ownerItem); var libraryOptions = GetLibraryOptions(ownerItem);
@ -2783,15 +2800,9 @@ namespace Emby.Server.Implementations.Library
{ {
foreach (var pathInfo in libraryOptions.PathInfos) 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; return newPath;
}
var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
if (substitutionResult.Item2)
{
return substitutionResult.Item1;
} }
} }
} }
@ -2800,24 +2811,16 @@ namespace Emby.Server.Implementations.Library
var metadataPath = _configurationManager.Configuration.MetadataPath; var metadataPath = _configurationManager.Configuration.MetadataPath;
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath; 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); return newPath;
if (metadataSubstitutionResult.Item2)
{
return metadataSubstitutionResult.Item1;
}
} }
foreach (var map in _configurationManager.Configuration.PathSubstitutions) 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); return newPath;
if (substitutionResult.Item2)
{
return substitutionResult.Item1;
}
} }
} }
@ -2826,47 +2829,12 @@ namespace Emby.Server.Implementations.Library
public string SubstitutePath(string path, string from, string to) public string SubstitutePath(string path, string from, string to)
{ {
return SubstitutePathInternal(path, from, to).Item1; if (path.TryReplaceSubPath(from, to, out var newPath))
{
return newPath;
} }
private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to) return path;
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentNullException(nameof(path));
}
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<string, bool>(newPath, changed);
} }
private void SetExtraTypeFromFilename(Video item) private void SetExtraTypeFromFilename(Video item)
@ -2923,6 +2891,12 @@ namespace Emby.Server.Implementations.Library
} }
public void UpdatePeople(BaseItem item, List<PersonInfo> people) public void UpdatePeople(BaseItem item, List<PersonInfo> people)
{
UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
}
/// <inheritdoc />
public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
{ {
if (!item.SupportsPeople) if (!item.SupportsPeople)
{ {
@ -2930,6 +2904,8 @@ namespace Emby.Server.Implementations.Library
} }
_itemRepository.UpdatePeople(item.Id, people); _itemRepository.UpdatePeople(item.Id, people);
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
} }
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex) public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
@ -3001,7 +2977,7 @@ namespace Emby.Server.Implementations.Library
if (collectionType != null) if (collectionType != null)
{ {
var path = Path.Combine(virtualFolderPath, collectionType.ToString() + ".collection"); var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
File.WriteAllBytes(path, Array.Empty<byte>()); File.WriteAllBytes(path, Array.Empty<byte>());
} }
@ -3033,6 +3009,58 @@ namespace Emby.Server.Implementations.Library
} }
} }
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{
var personsToSave = new List<BaseItem>();
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() private void StartScanInBackground()
{ {
Task.Run(() => Task.Run(() =>

View File

@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths) public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths)
{ {

View File

@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private IMediaSourceProvider[] _providers; private IMediaSourceProvider[] _providers;
@ -199,10 +199,15 @@ namespace Emby.Server.Implementations.Library
{ {
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); 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) public MediaProtocol GetPathProtocol(string path)
@ -436,7 +441,7 @@ namespace Emby.Server.Implementations.Library
} }
} }
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources) private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{ {
return sources.OrderBy(i => return sources.OrderBy(i =>
{ {
@ -451,8 +456,9 @@ namespace Emby.Server.Implementations.Library
{ {
var stream = i.VideoStream; 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(); .ToList();
} }
@ -584,18 +590,9 @@ namespace Emby.Server.Implementations.Library
public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{ {
var info = _openStreams.Values.FirstOrDefault(i => var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
{
var liveStream = i as ILiveStream;
if (liveStream != null)
{
return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
}
return false; return Task.FromResult(info.Value as IDirectStreamProvider);
});
return Task.FromResult(info as IDirectStreamProvider);
} }
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)

View File

@ -100,8 +100,7 @@ namespace Emby.Server.Implementations.Library
public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions) public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions)
{ {
var genre = item as MusicGenre; if (item is MusicGenre genre)
if (genre != null)
{ {
return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions); return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
} }

View File

@ -1,7 +1,8 @@
#nullable enable #nullable enable
using System; using System;
using System.Text.RegularExpressions; using System.Diagnostics.CodeAnalysis;
using MediaBrowser.Common.Providers;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
{ {
@ -41,11 +42,78 @@ namespace Emby.Server.Implementations.Library
// for imdbid we also accept pattern matching // for imdbid we also accept pattern matching
if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase)) if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase))
{ {
var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId);
return m.Success ? m.Value : null; return match ? imdbId.ToString() : null;
} }
return null; return null;
} }
/// <summary>
/// Replaces a sub path with another sub path and normalizes the final path.
/// </summary>
/// <param name="path">The original path.</param>
/// <param name="subPath">The original sub path.</param>
/// <param name="newSubPath">The new sub path.</param>
/// <param name="newPath">The result of the sub path replacement</param>
/// <returns>The path after replacing the sub path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
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;
}
} }
} }

View File

@ -1,3 +1,5 @@
#nullable enable
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -18,11 +20,10 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <param name="parent">The parent.</param> /// <param name="parent">The parent.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="libraryManager">The library manager.</param> /// <param name="libraryManager">The library manager.</param>
/// <param name="directoryService">The directory service.</param> /// <param name="directoryService">The directory service.</param>
/// <exception cref="ArgumentException">Item must have a path</exception> /// <exception cref="ArgumentException">Item must have a path.</exception>
public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService) 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 // This version of the below method has no ItemResolveArgs, so we have to require the path already being set
if (string.IsNullOrEmpty(item.Path)) if (string.IsNullOrEmpty(item.Path))
@ -43,9 +44,14 @@ namespace Emby.Server.Implementations.Library
// Make sure DateCreated and DateModified have values // Make sure DateCreated and DateModified have values
var fileInfo = directoryService.GetFile(item.Path); 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);
} }
/// <summary> /// <summary>
@ -72,9 +78,9 @@ namespace Emby.Server.Implementations.Library
item.Id = libraryManager.GetNewItemId(item.Path, item.GetType()); item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
// Make sure the item has a name // 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); item.GetParents().Any(i => i.IsLocked);
// Make sure DateCreated and DateModified have values // Make sure DateCreated and DateModified have values
@ -84,28 +90,15 @@ namespace Emby.Server.Implementations.Library
/// <summary> /// <summary>
/// Ensures the name. /// Ensures the name.
/// </summary> /// </summary>
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 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 = fileInfo.IsDirectory ? fileInfo.Name : Path.GetFileNameWithoutExtension(fileInfo.Name);
item.Name = GetDisplayName(fileName, fileInfo != null && fileInfo.IsDirectory);
} }
} }
/// <summary>
/// Gets the display name.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="isDirectory">if set to <c>true</c> [is directory].</param>
/// <returns>System.String.</returns>
private static string GetDisplayName(string path, bool isDirectory)
{
return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path);
}
/// <summary> /// <summary>
/// Ensures DateCreated and DateModified have values. /// Ensures DateCreated and DateModified have values.
/// </summary> /// </summary>
@ -114,21 +107,6 @@ namespace Emby.Server.Implementations.Library
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs 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 // See if a different path came out of the resolver than what went in
if (!fileSystem.AreEqual(args.Path, item.Path)) if (!fileSystem.AreEqual(args.Path, item.Path))
{ {
@ -136,7 +114,7 @@ namespace Emby.Server.Implementations.Library
if (childData != null) if (childData != null)
{ {
SetDateCreated(item, fileSystem, childData); SetDateCreated(item, childData);
} }
else else
{ {
@ -144,17 +122,17 @@ namespace Emby.Server.Implementations.Library
if (fileData.Exists) if (fileData.Exists)
{ {
SetDateCreated(item, fileSystem, fileData); SetDateCreated(item, fileData);
} }
} }
} }
else 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(); var config = BaseItem.ConfigurationManager.GetMetadataConfiguration();
@ -163,7 +141,7 @@ namespace Emby.Server.Implementations.Library
// directoryService.getFile may return null // directoryService.getFile may return null
if (info != null) if (info != null)
{ {
var dateCreated = fileSystem.GetCreationTimeUtc(info); var dateCreated = info.CreationTimeUtc;
if (dateCreated.Equals(DateTime.MinValue)) if (dateCreated.Equals(DateTime.MinValue))
{ {

View File

@ -201,6 +201,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue; continue;
} }
if (resolvedItem.Files.Count == 0)
{
continue;
}
var firstMedia = resolvedItem.Files[0]; var firstMedia = resolvedItem.Files[0];
var libraryItem = new T var libraryItem = new T

View File

@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary> /// </summary>
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <returns>`0.</returns> /// <returns>`0.</returns>
protected override T Resolve(ItemResolveArgs args) public override T Resolve(ItemResolveArgs args)
{ {
return ResolveVideo<T>(args, false); return ResolveVideo<T>(args, false);
} }
@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <param name="parseName">if set to <c>true</c> [parse name].</param> /// <param name="parseName">if set to <c>true</c> [parse name].</param>
/// <returns>``0.</returns> /// <returns>``0.</returns>
protected TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new() where TVideoType : Video, new()
{ {
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();

View File

@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{ {
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" }; 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(); var collectionType = args.GetCollectionType();

View File

@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
public abstract class ItemResolver<T> : IItemResolver public abstract class ItemResolver<T> : IItemResolver
where T : BaseItem, new() where T : BaseItem, new()
{ {
/// <summary>
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public virtual ResolverPriority Priority => ResolverPriority.First;
/// <summary> /// <summary>
/// Resolves the specified args. /// Resolves the specified args.
/// </summary> /// </summary>
@ -21,12 +27,6 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null; return null;
} }
/// <summary>
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public virtual ResolverPriority Priority => ResolverPriority.First;
/// <summary> /// <summary>
/// Sets initial values on the newly resolved item. /// Sets initial values on the newly resolved item.
/// </summary> /// </summary>

View File

@ -69,6 +69,110 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result; return result;
} }
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Video.</returns>
public override Video Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
// Find movies with their own folders
if (args.IsDirectory)
{
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
var files = args.FileSystemChildren
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
.ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
{
// Owned items will be caught by the plain video resolver
if (args.Parent == null)
{
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
return null;
}
if (args.HasParent<Series>())
{
return null;
}
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
return null;
}
// Handle owned items
if (args.Parent == null)
{
return base.Resolve(args);
}
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
Video item = null;
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Movie>(args, true);
}
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Video>(args, false);
}
else if (string.IsNullOrEmpty(collectionType))
{
if (args.HasParent<Series>())
{
return null;
}
item = ResolveVideo<Video>(args, false);
}
if (item != null)
{
item.IsInMixedFolder = true;
}
return item;
}
private MultiItemResolverResult ResolveMultipleInternal( private MultiItemResolverResult ResolveMultipleInternal(
Folder parent, Folder parent,
List<FileSystemMetadata> files, List<FileSystemMetadata> files,
@ -216,110 +320,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase); return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
} }
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Video.</returns>
protected override Video Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
// Find movies with their own folders
if (args.IsDirectory)
{
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
var files = args.FileSystemChildren
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
.ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
{
// Owned items will be caught by the plain video resolver
if (args.Parent == null)
{
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
return null;
}
if (args.HasParent<Series>())
{
return null;
}
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
return null;
}
// Handle owned items
if (args.Parent == null)
{
return base.Resolve(args);
}
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
Video item = null;
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Movie>(args, true);
}
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Video>(args, false);
}
else if (string.IsNullOrEmpty(collectionType))
{
if (args.HasParent<Series>())
{
return null;
}
item = ResolveVideo<Video>(args, false);
}
if (item != null)
{
item.IsInMixedFolder = true;
}
return item;
}
/// <summary> /// <summary>
/// Sets the initial item values. /// Sets the initial item values.
/// </summary> /// </summary>
@ -376,7 +376,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{ {
var multiDiscFolders = new List<FileSystemMetadata>(); var multiDiscFolders = new List<FileSystemMetadata>();
var libraryOptions = args.GetLibraryOptions(); var libraryOptions = args.LibraryOptions;
var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos; var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos;
var photos = new List<FileSystemMetadata>(); var photos = new List<FileSystemMetadata>();

View File

@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
public class PhotoAlbumResolver : FolderResolver<PhotoAlbum> public class PhotoAlbumResolver : FolderResolver<PhotoAlbum>
{ {
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class. /// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
@ -26,6 +26,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
/// <inheritdoc />
public override ResolverPriority Priority => ResolverPriority.Second;
/// <summary> /// <summary>
/// Resolves the specified args. /// Resolves the specified args.
/// </summary> /// </summary>
@ -39,8 +42,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection // Must be an image file within a photo collection
var collectionType = args.GetCollectionType(); var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) || if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
(string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos)) || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
{ {
if (HasPhotos(args)) if (HasPhotos(args))
{ {
@ -84,8 +87,5 @@ namespace Emby.Server.Implementations.Library.Resolvers
return false; return false;
} }
/// <inheritdoc />
public override ResolverPriority Priority => ResolverPriority.Second;
} }
} }

View File

@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
var collectionType = args.CollectionType; var collectionType = args.CollectionType;
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
|| (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos)) || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
{ {
if (IsImageFile(args.Path, _imageProcessor)) if (IsImageFile(args.Path, _imageProcessor))
{ {

View File

@ -63,7 +63,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
{ {
Path = args.Path, Path = args.Path,
Name = Path.GetFileNameWithoutExtension(args.Path), Name = Path.GetFileNameWithoutExtension(args.Path),
IsInMixedFolder = true IsInMixedFolder = true,
PlaylistMediaType = MediaType.Audio
}; };
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -11,12 +12,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary> /// </summary>
public class EpisodeResolver : BaseVideoResolver<Episode> public class EpisodeResolver : BaseVideoResolver<Episode>
{ {
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public EpisodeResolver(ILibraryManager libraryManager)
: base(libraryManager)
{
}
/// <summary> /// <summary>
/// Resolves the specified args. /// Resolves the specified args.
/// </summary> /// </summary>
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <returns>Episode.</returns> /// <returns>Episode.</returns>
protected override Episode Resolve(ItemResolveArgs args) public override Episode Resolve(ItemResolveArgs args)
{ {
var parent = args.Parent; var parent = args.Parent;
@ -25,30 +35,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null; return null;
} }
var season = parent as Season;
// Just in case the user decided to nest episodes. // Just in case the user decided to nest episodes.
// Not officially supported but in some cases we can handle it. // Not officially supported but in some cases we can handle it.
if (season == null)
{
season = parent.GetParents().OfType<Season>().FirstOrDefault();
}
// If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something var season = parent as Season ?? parent.GetParents().OfType<Season>().FirstOrDefault();
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders // Also handle flat tv folders
if (season != null || if ((season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>()) args.HasParent<Series>())
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
{ {
var episode = ResolveVideo<Episode>(args, false); var episode = ResolveVideo<Episode>(args, false);
if (episode != null) if (episode != null)
{ {
var series = parent as Series; var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
if (series == null)
{
series = parent.GetParents().OfType<Series>().FirstOrDefault();
}
if (series != null) if (series != null)
{ {
@ -74,14 +77,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null; return null;
} }
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public EpisodeResolver(ILibraryManager libraryManager)
: base(libraryManager)
{
}
} }
} }

View File

@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameSeasonNumber"), _localization.GetLocalizedString("NameSeasonNumber"),
seasonNumber, seasonNumber,
args.GetLibraryOptions().PreferredMetadataLanguage); args.LibraryOptions.PreferredMetadataLanguage);
} }
return season; return season;

View File

@ -19,19 +19,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary> /// </summary>
public class SeriesResolver : FolderResolver<Series> public class SeriesResolver : FolderResolver<Series>
{ {
private readonly IFileSystem _fileSystem;
private readonly ILogger<SeriesResolver> _logger; private readonly ILogger<SeriesResolver> _logger;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SeriesResolver"/> class. /// Initializes a new instance of the <see cref="SeriesResolver"/> class.
/// </summary> /// </summary>
/// <param name="fileSystem">The file system.</param>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="libraryManager">The library manager.</param> /// <param name="libraryManager">The library manager.</param>
public SeriesResolver(IFileSystem fileSystem, ILogger<SeriesResolver> logger, ILibraryManager libraryManager) public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
{ {
_fileSystem = fileSystem;
_logger = logger; _logger = logger;
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
@ -59,15 +56,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var collectionType = args.GetCollectionType(); var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) 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); var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path);
if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{ {
@ -100,7 +88,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null; 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 return new Series
{ {
@ -117,8 +105,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
public static bool IsSeriesFolder( public static bool IsSeriesFolder(
string path, string path,
IEnumerable<FileSystemMetadata> fileSystemChildren, IEnumerable<FileSystemMetadata> fileSystemChildren,
IDirectoryService directoryService,
IFileSystem fileSystem,
ILogger<SeriesResolver> logger, ILogger<SeriesResolver> logger,
ILibraryManager libraryManager, ILibraryManager libraryManager,
bool isTvContentType) bool isTvContentType)
@ -127,7 +113,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{ {
if (child.IsDirectory) 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); logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true; return true;
@ -160,32 +146,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return false; return false;
} }
/// <summary>
/// Determines whether [is place holder] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if [is place holder] [the specified path]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">path</exception>
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);
}
/// <summary> /// <summary>
/// Determines whether [is season folder] [the specified path]. /// Determines whether [is season folder] [the specified path].
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param> /// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
/// <param name="libraryManager">The library manager.</param>
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
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; var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;

View File

@ -12,7 +12,6 @@ using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search; using MediaBrowser.Model.Search;
using Microsoft.Extensions.Logging;
using Genre = MediaBrowser.Controller.Entities.Genre; using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person; using Person = MediaBrowser.Controller.Entities.Person;

View File

@ -13,8 +13,8 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Book = MediaBrowser.Controller.Entities.Book;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook; using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
{ {
@ -248,15 +248,15 @@ namespace Emby.Server.Implementations.Library
} }
else if (positionTicks > 0 && hasRuntime && item is AudioBook) else if (positionTicks > 0 && hasRuntime && item is AudioBook)
{ {
var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes; var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes; var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
if (minIn > _config.Configuration.MinAudiobookResume) if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
{ {
// ignore progress during the beginning // ignore progress during the beginning
positionTicks = 0; 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 // mark as completed close to the end
positionTicks = 0; positionTicks = 0;

View File

@ -2,7 +2,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;

View File

@ -45,7 +45,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); 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(); onStarted();
@ -70,7 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); 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(); onStarted();

View File

@ -17,7 +17,6 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress; using MediaBrowser.Common.Progress;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -802,22 +801,22 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public ActiveRecordingInfo GetActiveRecordingInfo(string path) public ActiveRecordingInfo GetActiveRecordingInfo(string path)
{ {
if (string.IsNullOrWhiteSpace(path)) if (string.IsNullOrWhiteSpace(path) || _activeRecordings.IsEmpty)
{ {
return null; 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) if (timer.Status != RecordingStatus.InProgress)
{ {
return null; return null;
} }
return recording; return recordingInfo;
} }
} }
@ -1622,9 +1621,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
} }
return _activeRecordings return _activeRecordings
.Values .Any(i => string.Equals(i.Value.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Value.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
.ToList()
.Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
} }
private IRecorder GetRecorder(MediaSourceInfo mediaSource) private IRecorder GetRecorder(MediaSourceInfo mediaSource)
@ -1856,7 +1853,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return; 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 var settings = new XmlWriterSettings
{ {
@ -1920,7 +1918,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return; 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 var settings = new XmlWriterSettings
{ {
@ -2238,14 +2237,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var enabledTimersForSeries = new List<TimerInfo>(); var enabledTimersForSeries = new List<TimerInfo>();
foreach (var timer in allTimers) foreach (var timer in allTimers)
{ {
var existingTimer = _timerProvider.GetTimer(timer.Id); var existingTimer = _timerProvider.GetTimer(timer.Id)
?? (string.IsNullOrWhiteSpace(timer.ProgramId)
if (existingTimer == null)
{
existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId)
? null ? null
: _timerProvider.GetTimerByProgramId(timer.ProgramId); : _timerProvider.GetTimerByProgramId(timer.ProgramId));
}
if (existingTimer == null) if (existingTimer == null)
{ {

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