Merge branch 'master' into ProfileMatch

pull/5861/head
BaronGreenback 3 years ago committed by GitHub
commit f8b717e7c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

@ -0,0 +1,66 @@
name: Automation
on:
pull_request:
issues:
issue_comment:
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.5.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.5.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.5.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.5.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.5.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 }}

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

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

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

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

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

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

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

@ -1,5 +1,4 @@
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 +6,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 +119,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 +199,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 +303,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 +416,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 +428,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

@ -36,7 +36,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);
@ -126,14 +126,14 @@ namespace Emby.Dlna
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used."); builder.AppendLine("No matching device profile found. The default will need to be used.");
builder.Append("FriendlyName:").AppendLine(profile.FriendlyName); builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
builder.Append("Manufacturer:").AppendLine(profile.Manufacturer); builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl); builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
builder.Append("ModelDescription:").AppendLine(profile.ModelDescription); builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
builder.Append("ModelName:").AppendLine(profile.ModelName); builder.Append("ModelName: ").AppendLine(profile.ModelName);
builder.Append("ModelNumber:").AppendLine(profile.ModelNumber); builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
builder.Append("ModelUrl:").AppendLine(profile.ModelUrl); builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
builder.Append("SerialNumber:").AppendLine(profile.SerialNumber); builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString()); _logger.LogInformation(builder.ToString());
} }
@ -290,7 +290,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);
} }
@ -352,7 +357,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);
} }

@ -25,7 +25,6 @@
<!-- 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" />

@ -5,7 +5,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 +127,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 +316,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";

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

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

@ -219,7 +219,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 +259,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 +290,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 +323,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 +403,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 +417,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 +441,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 +569,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 +619,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 +706,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 +778,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 +964,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 +996,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 +1031,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>();

@ -132,7 +132,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;
} }
@ -943,11 +943,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");

@ -178,6 +178,11 @@ 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;

@ -2,7 +2,6 @@
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 +44,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 +93,17 @@ namespace Emby.Dlna.PlayTo
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8); try
return XDocument.Parse( {
await reader.ReadToEndAsync().ConfigureAwait(false), return await XDocument.LoadAsync(
LoadOptions.PreserveWhitespace); stream,
LoadOptions.PreserveWhitespace,
cancellationToken).ConfigureAwait(false);
}
catch
{
return null;
}
} }
private async Task<HttpResponseMessage> PostSoapDataAsync( private async Task<HttpResponseMessage> PostSoapDataAsync(

@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
public class TransportCommands public class TransportCommands
{ {
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>"; private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
private List<StateVariable> _stateVariables = new List<StateVariable>();
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
public List<StateVariable> StateVariables => _stateVariables; public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
public List<ServiceAction> ServiceActions => _serviceActions; public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
public static TransportCommands Create(XDocument document) public static TransportCommands Create(XDocument document)
{ {

@ -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:*";

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

@ -69,7 +69,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);

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

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

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

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

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

@ -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
if (folderName.Length <= testFilename.Length)
{ {
testFilename = cleanName.ToString(); testFilename = testFilename.Substring(folderName.Length).Trim();
} }
if (folderName.Length <= testFilename.Length) if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{ {
testFilename = testFilename.Substring(folderName.Length).Trim(); 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;

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

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

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

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

@ -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.");
} }
} }
@ -463,7 +444,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 +468,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 +484,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 +522,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 +554,6 @@ namespace Emby.Server.Implementations
}; };
Certificate = GetCertificate(CertificateInfo); Certificate = GetCertificate(CertificateInfo);
DiscoverTypes();
RegisterServices(); RegisterServices();
_pluginManager.RegisterServices(ServiceCollection); _pluginManager.RegisterServices(ServiceCollection);
@ -567,7 +568,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 +596,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>();
@ -685,6 +685,8 @@ namespace Emby.Server.Implementations
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 +784,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 +806,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 +915,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 +924,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 +1143,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 +1169,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 +1189,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 +1227,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.

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

@ -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;
@ -343,11 +340,24 @@ namespace Emby.Server.Implementations.Collections
} }
} }
else else
{
var alreadyInResults = false;
foreach (var child in item.GetMediaSources(true))
{
if (Guid.TryParse(child.Id, out var id) && results.ContainsKey(id))
{
alreadyInResults = true;
break;
}
}
if (!alreadyInResults)
{ {
results[item.Id] = item; results[item.Id] = item;
} }
} }
} }
}
return results.Values; return results.Values;
} }

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

@ -88,7 +88,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");
} }
@ -5415,7 +5415,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 +5440,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,

@ -27,6 +27,7 @@
<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="3.4.1" />
<PackageReference Include="sharpcompress" Version="0.28.1" /> <PackageReference Include="sharpcompress" Version="0.28.1" />
@ -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" />

@ -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()
{ {

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

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

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

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

@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.IO
} }
var extension = Path.GetExtension(filename); var extension = Path.GetExtension(filename);
return _shortcutHandlers.Any(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); return _shortcutHandlers.Any(i => string.Equals(extension, i.Extension, _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal));
} }
/// <summary> /// <summary>
@ -248,12 +248,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)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
}
result.DirectoryName = fileInfo.DirectoryName; result.DirectoryName = fileInfo.DirectoryName;
} }
@ -487,26 +496,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 +513,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 +528,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)

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

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

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

@ -196,33 +196,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; }
@ -1247,7 +1247,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 +1914,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 +1937,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 +1949,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 +1959,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);
} }
} }
@ -2776,6 +2781,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 +2789,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;
}
var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
if (substitutionResult.Item2)
{ {
return substitutionResult.Item1; return newPath;
} }
} }
} }
@ -2800,24 +2800,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 +2818,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))
}
private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
{ {
if (string.IsNullOrWhiteSpace(path)) return newPath;
{
throw new ArgumentNullException(nameof(path));
} }
if (string.IsNullOrWhiteSpace(from)) return path;
{
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)
@ -3001,7 +2958,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>());
} }

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

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

@ -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,72 @@ 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)
|| (!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;
}
} }
} }

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

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

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

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

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

@ -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;
@ -34,11 +44,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
season = parent.GetParents().OfType<Season>().FirstOrDefault(); season = parent.GetParents().OfType<Season>().FirstOrDefault();
} }
// If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something // 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);
@ -74,14 +85,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)
{
}
} }
} }

@ -127,7 +127,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 +160,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;

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

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

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

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

@ -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;
@ -1856,7 +1855,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 +1920,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
{ {

@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IServerApplicationPaths _appPaths; private readonly IServerApplicationPaths _appPaths;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private bool _hasExited; private bool _hasExited;
private Stream _logFileStream; private Stream _logFileStream;
private string _targetPath; private string _targetPath;

@ -9,55 +9,44 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
internal class EpgChannelData internal class EpgChannelData
{ {
private readonly Dictionary<string, ChannelInfo> _channelsById;
private readonly Dictionary<string, ChannelInfo> _channelsByNumber;
private readonly Dictionary<string, ChannelInfo> _channelsByName;
public EpgChannelData(IEnumerable<ChannelInfo> channels) public EpgChannelData(IEnumerable<ChannelInfo> channels)
{ {
ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); _channelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); _channelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); _channelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var channel in channels) foreach (var channel in channels)
{ {
ChannelsById[channel.Id] = channel; _channelsById[channel.Id] = channel;
if (!string.IsNullOrEmpty(channel.Number)) if (!string.IsNullOrEmpty(channel.Number))
{ {
ChannelsByNumber[channel.Number] = channel; _channelsByNumber[channel.Number] = channel;
} }
var normalizedName = NormalizeName(channel.Name ?? string.Empty); var normalizedName = NormalizeName(channel.Name ?? string.Empty);
if (!string.IsNullOrWhiteSpace(normalizedName)) if (!string.IsNullOrWhiteSpace(normalizedName))
{ {
ChannelsByName[normalizedName] = channel; _channelsByName[normalizedName] = channel;
} }
} }
} }
private Dictionary<string, ChannelInfo> ChannelsById { get; set; }
private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; }
private Dictionary<string, ChannelInfo> ChannelsByName { get; set; }
public ChannelInfo GetChannelById(string id) public ChannelInfo GetChannelById(string id)
{ => _channelsById.GetValueOrDefault(id);
ChannelsById.TryGetValue(id, out var result);
return result;
}
public ChannelInfo GetChannelByNumber(string number) public ChannelInfo GetChannelByNumber(string number)
{ => _channelsByNumber.GetValueOrDefault(number);
ChannelsByNumber.TryGetValue(number, out var result);
return result;
}
public ChannelInfo GetChannelByName(string name) public ChannelInfo GetChannelByName(string name)
{ => _channelsByName.GetValueOrDefault(name);
ChannelsByName.TryGetValue(name, out var result);
return result;
}
public static string NormalizeName(string value) public static string NormalizeName(string value)
{ {

@ -4,9 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
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.Tasks;
using MediaBrowser.Common.Json; using MediaBrowser.Common.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -17,7 +15,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
private readonly string _dataPath; private readonly string _dataPath;
private readonly object _fileDataLock = new object(); private readonly object _fileDataLock = new object();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private T[] _items; private T[] _items;
public ItemDataProvider( public ItemDataProvider(

@ -2,7 +2,6 @@
using System; using System;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV namespace Emby.Server.Implementations.LiveTv.EmbyTV

@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ICryptoProvider _cryptoProvider; private readonly ICryptoProvider _cryptoProvider;
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>(); private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private DateTime _lastErrorResponse; private DateTime _lastErrorResponse;
public SchedulesDirect( public SchedulesDirect(

@ -4,7 +4,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;

@ -8,10 +8,8 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json; using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
@ -19,7 +17,6 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -61,7 +58,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_networkManager = networkManager; _networkManager = networkManager;
_streamHelper = streamHelper; _streamHelper = streamHelper;
_jsonOptions = JsonDefaults.GetOptions(); _jsonOptions = JsonDefaults.Options;
} }
public string Name => "HD Homerun"; public string Name => "HD Homerun";

@ -2,6 +2,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Net; using System.Net;
@ -10,6 +11,7 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@ -120,17 +122,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken) private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
{ {
var lockkeyMsg = CreateGetMessage(tuner, "lockkey");
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try try
{ {
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); var msgLen = WriteGetMessage(buffer, tuner, "lockkey");
await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false);
ParseReturnMessage(buffer, receivedBytes, out string returnVal); int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase); return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
} }
finally finally
{ {
@ -166,24 +166,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_activeTuner = i; _activeTuner = i;
var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue); var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null);
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked // parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _)) if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{ {
continue; continue;
} }
foreach (var command in commands.GetCommands()) foreach (var command in commands.GetCommands())
{ {
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); var channelMsgLen = WriteSetMessage(buffer, i, command.Item1, command.Item2, lockKeyValue);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked // parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _)) if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{ {
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue; continue;
@ -191,13 +191,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort); var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false); await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked // parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _)) if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{ {
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue; continue;
@ -232,12 +232,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
foreach (var command in commandList) foreach (var command in commandList)
{ {
var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey); var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.Item1, command.Item2, _lockkey);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked // parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _)) if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{ {
return; return;
} }
@ -265,17 +265,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
var stream = client.GetStream(); var stream = client.GetStream();
var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue);
await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false);
var buffer = ArrayPool<byte>.Shared.Rent(8192); var buffer = ArrayPool<byte>.Shared.Rent(8192);
try try
{ {
await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue);
var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue); await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false);
await stream.ReadAsync(buffer).ConfigureAwait(false);
var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue);
_lockkey = null; _lockkey = null;
await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false); await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false);
await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); await stream.ReadAsync(buffer).ConfigureAwait(false);
} }
finally finally
{ {
@ -283,249 +283,136 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
} }
private static byte[] CreateGetMessage(int tuner, string name) internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name)
{ {
var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name)); var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length int offset = WriteHeaderAndPayload(buffer, byteName);
return FinishPacket(buffer, offset);
var message = new byte[messageLength]; }
int offset = InsertHeaderAndName(byteName, messageLength, message); internal static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey)
{
var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
int offset = WriteHeaderAndPayload(buffer, byteName);
bool flipEndian = BitConverter.IsLittleEndian; buffer[offset++] = GetSetValue;
offset += WriteNullTerminatedString(buffer.Slice(offset), value);
// calculate crc and insert at the end of the message if (lockkey.HasValue)
var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
if (flipEndian)
{ {
Array.Reverse(crcBytes); buffer[offset++] = GetSetLockkey;
buffer[offset++] = 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value);
offset += 4;
} }
Buffer.BlockCopy(crcBytes, 0, message, offset, 4); return FinishPacket(buffer, offset);
return message;
} }
private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey) internal static int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> payload)
{ {
var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name)); int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1;
var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value));
int messageLength = byteName.Length + byteValue.Length + 12; // TODO: variable length: this can be 2 bytes if len > 127
if (lockkey.HasValue) // Write length in front of value
{ buffer[0] = Convert.ToByte(len);
messageLength += 6;
}
var message = new byte[messageLength]; // null-terminate
buffer[len++] = 0;
int offset = InsertHeaderAndName(byteName, messageLength, message); return len;
}
bool flipEndian = BitConverter.IsLittleEndian;
message[offset++] = GetSetValue; private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload)
message[offset++] = Convert.ToByte(byteValue.Length);
Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length);
offset += byteValue.Length;
if (lockkey.HasValue)
{ {
message[offset++] = GetSetLockkey; // Packet type
message[offset++] = 4; BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
var lockKeyBytes = BitConverter.GetBytes(lockkey.Value);
if (flipEndian)
{
Array.Reverse(lockKeyBytes);
}
Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4); // We write the payload length at the end
offset += 4; int offset = 4;
}
// calculate crc and insert at the end of the message // Tag
var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4)); buffer[offset++] = GetSetName;
if (flipEndian)
{
Array.Reverse(crcBytes);
}
Buffer.BlockCopy(crcBytes, 0, message, offset, 4); // Payload length + data
int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload);
offset += strLen;
return message; return offset;
} }
private static int InsertHeaderAndName(byte[] byteName, int messageLength, byte[] message) private static int FinishPacket(Span<byte> buffer, int offset)
{ {
// check to see if we need to flip endiannes // Payload length
bool flipEndian = BitConverter.IsLittleEndian; BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4));
int offset = 0;
// create header bytes // calculate crc and insert at the end of the message
var getSetBytes = BitConverter.GetBytes(GetSetRequest); var crc = Crc32.Compute(buffer.Slice(0, offset));
var msgLenBytes = BitConverter.GetBytes((ushort)(messageLength - 8)); // Subtrace 4 bytes for header and 4 bytes for crc BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc);
if (flipEndian) return offset + 4;
{
Array.Reverse(getSetBytes);
Array.Reverse(msgLenBytes);
} }
// insert header bytes into message internal static bool VerifyReturnValueOfGetSet(ReadOnlySpan<byte> buffer, string expected)
Buffer.BlockCopy(getSetBytes, 0, message, offset, 2); {
offset += 2; return TryGetReturnValueOfGetSet(buffer, out var value)
Buffer.BlockCopy(msgLenBytes, 0, message, offset, 2); && string.Equals(Encoding.UTF8.GetString(value), expected, StringComparison.OrdinalIgnoreCase);
offset += 2;
// insert tag name and length
message[offset++] = GetSetName;
message[offset++] = Convert.ToByte(byteName.Length);
// insert name string
Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length);
offset += byteName.Length;
return offset;
} }
private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal) internal static bool TryGetReturnValueOfGetSet(ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> value)
{ {
returnVal = string.Empty; value = ReadOnlySpan<byte>.Empty;
if (numBytes < 4) if (buffer.Length < 8)
{ {
return false; return false;
} }
var flipEndian = BitConverter.IsLittleEndian; uint crc = BinaryPrimitives.ReadUInt32LittleEndian(buffer[^4..]);
int offset = 0; if (crc != Crc32.Compute(buffer[..^4]))
byte[] msgTypeBytes = new byte[2];
Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length);
if (flipEndian)
{ {
Array.Reverse(msgTypeBytes); return false;
} }
var msgType = BitConverter.ToUInt16(msgTypeBytes, 0); if (BinaryPrimitives.ReadUInt16BigEndian(buffer) != GetSetReply)
offset += 2;
if (msgType != GetSetReply)
{ {
return false; return false;
} }
byte[] msgLengthBytes = new byte[2]; var msgLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2));
Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length); if (buffer.Length != 2 + 2 + 4 + msgLength)
if (flipEndian)
{ {
Array.Reverse(msgLengthBytes); return false;
} }
var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0); var offset = 4;
offset += 2; if (buffer[offset++] != GetSetName)
if (numBytes < msgLength + 8)
{ {
return false; return false;
} }
offset++; // Name Tag var nameLength = buffer[offset++];
if (buffer.Length < 4 + 1 + offset + nameLength)
var nameLength = buf[offset++]; {
return false;
}
// skip the name field to get to value for return
offset += nameLength; offset += nameLength;
offset++; // Value Tag if (buffer[offset++] != GetSetValue)
{
var valueLength = buf[offset++]; return false;
returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator
return true;
} }
private static class HdHomerunCrc var valueLength = buffer[offset++];
{ if (buffer.Length < 4 + offset + valueLength)
private static uint[] crc_table = { {
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, return false;
0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d };
public static uint GetCrc32(byte[] bytes, int numBytes)
{
var hash = 0xffffffff;
for (var i = 0; i < numBytes; i++)
{
hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff];
}
var tmp = ~hash & 0xffffffff;
var b0 = tmp & 0xff;
var b1 = (tmp >> 8) & 0xff;
var b2 = (tmp >> 16) & 0xff;
var b3 = (tmp >> 24) & 0xff;
return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
} }
// remove null terminator
value = buffer.Slice(offset, valueLength - 1);
return true;
} }
} }
} }

@ -4,7 +4,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
@ -133,6 +132,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
channel.ImageUrl = value; channel.ImageUrl = value;
} }
if (attributes.TryGetValue("group-title", out string groupTitle))
{
channel.ChannelGroup = groupTitle;
}
channel.Name = GetChannelName(extInf, attributes); channel.Name = GetChannelName(extInf, attributes);
channel.Number = GetChannelNumber(extInf, attributes, mediaUrl); channel.Number = GetChannelNumber(extInf, attributes, mediaUrl);

@ -1,5 +1,5 @@
{ {
"Albums": "Albums", "Albums": "Albummer",
"AppDeviceValues": "App: {0}, Enhed: {1}", "AppDeviceValues": "App: {0}, Enhed: {1}",
"Application": "Applikation", "Application": "Applikation",
"Artists": "Kunstnere", "Artists": "Kunstnere",

@ -22,5 +22,26 @@
"Artists": "Artistoj", "Artists": "Artistoj",
"Application": "Aplikaĵo", "Application": "Aplikaĵo",
"AppDeviceValues": "Aplikaĵo: {0}, Aparato: {1}", "AppDeviceValues": "Aplikaĵo: {0}, Aparato: {1}",
"Albums": "Albumoj" "Albums": "Albumoj",
"TasksLibraryCategory": "Libraro",
"VersionNumber": "Versio {0}",
"UserDownloadingItemWithValues": "{0} elŝutas {1}",
"UserCreatedWithName": "Uzanto {0} kreiĝis",
"User": "Uzanto",
"System": "Sistemo",
"Songs": "Kantoj",
"ScheduledTaskStartedWithName": "{0} komencis",
"ScheduledTaskFailedWithName": "{0} malsukcesis",
"PluginUninstalledWithName": "{0} malinstaliĝis",
"PluginInstalledWithName": "{0} instaliĝis",
"Plugin": "Kromprogramo",
"Playlists": "Ludlistoj",
"Photos": "Fotoj",
"NotificationOptionPluginUninstalled": "Kromprogramo malinstaliĝis",
"NotificationOptionNewLibraryContent": "Nova enhavo aldoniĝis",
"NotificationOptionPluginInstalled": "Kromprogramo instaliĝis",
"MusicVideos": "Muzikvideoj",
"LabelIpAddressValue": "IP-adreso: {0}",
"Genres": "Ĝenroj",
"DeviceOfflineWithName": "{0} malkonektis"
} }

@ -57,5 +57,27 @@
"DeviceOnlineWithName": "{0} conectouse", "DeviceOnlineWithName": "{0} conectouse",
"DeviceOfflineWithName": "{0} desconectouse", "DeviceOfflineWithName": "{0} desconectouse",
"Default": "Por defecto", "Default": "Por defecto",
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}" "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"TaskCleanLogs": "Limpar Carpeta de Rexistros",
"TaskCleanActivityLog": "Limpar Rexistro de Actividade",
"TasksChannelsCategory": "Canáis de Internet",
"TaskUpdatePlugins": "Actualizar Plugins",
"User": "Usuario",
"Undefined": "Sen definir",
"TvShows": "Programas de TV",
"System": "Sistema",
"Sync": "Sincronizar",
"SubtitleDownloadFailureFromForItem": "Fallou a descarga de subtítulos para {1} dende {0}",
"StartupEmbyServerIsLoading": "O Servidor Jellyfin está cargando. Por favor, reinténteo en breve.",
"Songs": "Cancións",
"Shows": "Programas",
"ServerNameNeedsToBeRestarted": "{0} precisa ser reiniciado",
"ScheduledTaskStartedWithName": "{0} comezou",
"ScheduledTaskFailedWithName": "{0} fallou",
"ProviderValue": "Provedor: {0}",
"PluginUpdatedWithName": "{0} foi actualizado",
"PluginUninstalledWithName": "{0} foi desinstalado",
"PluginInstalledWithName": "{0} foi instalado",
"Playlists": "Listas de reproducción",
"Photos": "Fotos"
} }

@ -109,14 +109,14 @@
"TasksMaintenanceCategory": "Qyzmet körsetu", "TasksMaintenanceCategory": "Qyzmet körsetu",
"Undefined": "Anyqtalmağan", "Undefined": "Anyqtalmağan",
"Forced": "Mäjbürlı", "Forced": "Mäjbürlı",
"TaskDownloadMissingSubtitlesDescription": "Metaderekter teŋşelımderı negızınde joq subtitrlerdı Internetten ızdeidı.", "TaskDownloadMissingSubtitlesDescription": "Metaderekter teŋşelımderı negızınde joq subtitrlerdı İnternetten ızdeidı.",
"TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jaŋğyrtady.", "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jaŋğyrtady.",
"TaskCleanTranscodeDescription": "Bіr künnen asqan qaita kodtau faildaryn joiady.", "TaskCleanTranscodeDescription": "Bіr künnen asqan qaita kodtau faildaryn joiady.",
"TaskUpdatePluginsDescription": "Avtomatty türde jaŋartuğa teŋşelgen plaginder üşın jaŋartulardy jüktep alady jäne ornatady.", "TaskUpdatePluginsDescription": "Avtomatty türde jaŋartuğa teŋşelgen plaginder üşın jaŋartulardy jüktep alady jäne ornatady.",
"TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jaŋartady.", "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jaŋartady.",
"TaskCleanLogsDescription": "{0} künnen asqan jūrnal faildaryn joiady.", "TaskCleanLogsDescription": "{0} künnen asqan jūrnal faildaryn joiady.",
"TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaŋa faildardy skanerleidі jäne metaderekterdı jaŋğyrtady.", "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaŋa faildardy skanerleidі jäne metaderekterdı jaŋğyrtady.",
"TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşіn nobailar jasaidy.", "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.",
"TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.", "TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.",
"TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teŋşelgen jasynan asqan jazbalary joiady." "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teŋşelgen jasynan asqan jazbalary joiady."
} }

@ -113,5 +113,9 @@
"TasksChannelsCategory": "Internetiniai Kanalai", "TasksChannelsCategory": "Internetiniai Kanalai",
"TasksApplicationCategory": "Programa", "TasksApplicationCategory": "Programa",
"TasksLibraryCategory": "Mediateka", "TasksLibraryCategory": "Mediateka",
"TasksMaintenanceCategory": "Priežiūra" "TasksMaintenanceCategory": "Priežiūra",
"TaskCleanActivityLog": "Švarus veiklos žurnalas",
"Undefined": "Neapibrėžtas",
"Forced": "Priverstas",
"Default": "Numatytas"
} }

@ -16,7 +16,7 @@
"Folders": "Pastas", "Folders": "Pastas",
"Genres": "Gêneros", "Genres": "Gêneros",
"HeaderAlbumArtists": "Artistas do Álbum", "HeaderAlbumArtists": "Artistas do Álbum",
"HeaderContinueWatching": "Continuar Assistindo", "HeaderContinueWatching": "Continuar assistindo",
"HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episódios favoritos", "HeaderFavoriteEpisodes": "Episódios favoritos",

@ -50,7 +50,7 @@
"HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ", "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
"HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ", "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
"HeaderContinueWatching": "ดูต่อ", "HeaderContinueWatching": "ดูต่อ",
"HeaderAlbumArtists": "อัลบั้มศิลปิน", "HeaderAlbumArtists": "ศิลปินอัลบั้ม",
"Genres": "ประเภท", "Genres": "ประเภท",
"Folders": "โฟลเดอร์", "Folders": "โฟลเดอร์",
"Favorites": "รายการโปรด", "Favorites": "รายการโปรด",
@ -112,5 +112,6 @@
"System": "ระบบ", "System": "ระบบ",
"Sync": "ซิงค์", "Sync": "ซิงค์",
"SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้", "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
"StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่" "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่",
"Default": "ค่าเริ่มต้น"
} }

@ -11,7 +11,6 @@ using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Localization namespace Emby.Server.Implementations.Localization
@ -36,7 +35,7 @@ namespace Emby.Server.Implementations.Localization
private List<CultureDto> _cultures; private List<CultureDto> _cultures;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LocalizationManager" /> class. /// Initializes a new instance of the <see cref="LocalizationManager" /> class.

@ -15,7 +15,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.MediaEncoder namespace Emby.Server.Implementations.MediaEncoder
@ -82,11 +81,6 @@ namespace Emby.Server.Implementations.MediaEncoder
return false; return false;
} }
if (video.VideoType == VideoType.Dvd)
{
return false;
}
if (video.IsShortcut) if (video.IsShortcut)
{ {
return false; return false;

@ -2,6 +2,7 @@
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.Net.Http; using System.Net.Http;
@ -34,7 +35,7 @@ namespace Emby.Server.Implementations.Plugins
private readonly ILogger<PluginManager> _logger; private readonly ILogger<PluginManager> _logger;
private readonly IApplicationHost _appHost; private readonly IApplicationHost _appHost;
private readonly ServerConfiguration _config; private readonly ServerConfiguration _config;
private readonly IList<LocalPlugin> _plugins; private readonly List<LocalPlugin> _plugins;
private readonly Version _minimumVersion; private readonly Version _minimumVersion;
private IHttpClientFactory? _httpClientFactory; private IHttpClientFactory? _httpClientFactory;
@ -70,7 +71,7 @@ namespace Emby.Server.Implementations.Plugins
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_pluginsPath = pluginsPath; _pluginsPath = pluginsPath;
_appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion)); _appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
_jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions()) _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options)
{ {
WriteIndented = true WriteIndented = true
}; };
@ -94,7 +95,7 @@ namespace Emby.Server.Implementations.Plugins
/// <summary> /// <summary>
/// Gets the Plugins. /// Gets the Plugins.
/// </summary> /// </summary>
public IList<LocalPlugin> Plugins => _plugins; public IReadOnlyList<LocalPlugin> Plugins => _plugins;
/// <summary> /// <summary>
/// Returns all the assemblies. /// Returns all the assemblies.
@ -368,7 +369,7 @@ namespace Emby.Server.Implementations.Plugins
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path) public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
{ {
if (packageInfo == null) if (packageInfo == null)
{ {
@ -411,9 +412,9 @@ namespace Emby.Server.Implementations.Plugins
Overview = packageInfo.Overview, Overview = packageInfo.Overview,
Owner = packageInfo.Owner, Owner = packageInfo.Owner,
TargetAbi = versionInfo.TargetAbi ?? string.Empty, TargetAbi = versionInfo.TargetAbi ?? string.Empty,
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp), Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
Version = versionInfo.Version, Version = versionInfo.Version,
Status = PluginStatus.Active, Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
AutoUpdate = true, AutoUpdate = true,
ImagePath = imagePath ImagePath = imagePath
}; };
@ -678,7 +679,7 @@ namespace Emby.Server.Implementations.Plugins
var entry = versions[x]; var entry = versions[x];
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
{ {
entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories)); entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories);
if (entry.IsEnabledAndSupported) if (entry.IsEnabledAndSupported)
{ {
lastName = entry.Name; lastName = entry.Name;

@ -3,7 +3,6 @@ using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;

@ -4,7 +4,6 @@ using System;
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;
@ -69,7 +68,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary> /// <summary>
/// The options for the json Serializer. /// The options for the json Serializer.
/// </summary> /// </summary>
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class. /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
@ -178,7 +177,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
lock (_lastExecutionResultSyncLock) lock (_lastExecutionResultSyncLock)
{ {
using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
JsonSerializer.SerializeAsync(createStream, value, _jsonOptions); using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream);
JsonSerializer.Serialize(jsonStream, value, _jsonOptions);
} }
} }
} }
@ -578,7 +578,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path));
using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
JsonSerializer.SerializeAsync(createStream, triggers, _jsonOptions); using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream);
JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions);
} }
/// <summary> /// <summary>

@ -12,9 +12,9 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks namespace Emby.Server.Implementations.ScheduledTasks
{ {

@ -5,8 +5,8 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.ScheduledTasks namespace Emby.Server.Implementations.ScheduledTasks
{ {

@ -9,7 +9,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

@ -6,8 +6,8 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Library;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.ScheduledTasks namespace Emby.Server.Implementations.ScheduledTasks
{ {

@ -131,11 +131,11 @@ namespace Emby.Server.Implementations.Sorting
return GetSpecialCompareValue(x).CompareTo(GetSpecialCompareValue(y)); return GetSpecialCompareValue(x).CompareTo(GetSpecialCompareValue(y));
} }
private static int GetSpecialCompareValue(Episode item) private static long GetSpecialCompareValue(Episode item)
{ {
// First sort by season number // First sort by season number
// Since there are three sort orders, pad with 9 digits (3 for each, figure 1000 episode buffer should be enough) // Since there are three sort orders, pad with 9 digits (3 for each, figure 1000 episode buffer should be enough)
var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000; var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000L;
// Second sort order is if it airs after the season // Second sort order is if it airs after the season
if (item.AirsAfterSeasonNumber.HasValue) if (item.AirsAfterSeasonNumber.HasValue)

@ -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 Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@ -11,7 +10,6 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV; using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Series = MediaBrowser.Controller.Entities.TV.Series; using Series = MediaBrowser.Controller.Entities.TV.Series;
@ -23,12 +21,14 @@ namespace Emby.Server.Implementations.TV
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager; private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager) public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager)
{ {
_userManager = userManager; _userManager = userManager;
_userDataManager = userDataManager; _userDataManager = userDataManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_configurationManager = configurationManager;
} }
public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions) public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions)
@ -200,13 +200,10 @@ namespace Emby.Server.Implementations.TV
ParentIndexNumberNotEquals = 0, ParentIndexNumberNotEquals = 0,
DtoOptions = new DtoOptions DtoOptions = new DtoOptions
{ {
Fields = new ItemFields[] Fields = new[] { ItemFields.SortName },
{
ItemFields.SortName
},
EnableImages = false EnableImages = false
} }
}).FirstOrDefault(); }).Cast<Episode>().FirstOrDefault();
Func<Episode> getEpisode = () => Func<Episode> getEpisode = () =>
{ {
@ -224,6 +221,43 @@ namespace Emby.Server.Implementations.TV
DtoOptions = dtoOptions DtoOptions = dtoOptions
}).Cast<Episode>().FirstOrDefault(); }).Cast<Episode>().FirstOrDefault();
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
{
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0,
IncludeItemTypes = new[] { nameof(Episode) },
IsPlayed = false,
IsVirtualItem = false,
DtoOptions = dtoOptions
})
.Cast<Episode>()
.Where(episode => episode.AirsBeforeSeasonNumber != null || episode.AirsAfterSeasonNumber != null)
.ToList();
if (lastWatchedEpisode != null)
{
// Last watched episode is added, because there could be specials that aired before the last watched episode
consideredEpisodes.Add(lastWatchedEpisode);
}
if (nextEpisode != null)
{
consideredEpisodes.Add(nextEpisode);
}
var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
.Cast<Episode>();
if (lastWatchedEpisode != null)
{
sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => episode.Id != lastWatchedEpisode.Id).Skip(1);
}
nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
}
if (nextEpisode != null) if (nextEpisode != null)
{ {
var userData = _userDataManager.GetUserData(user, nextEpisode); var userData = _userDataManager.GetUserData(user, nextEpisode);

@ -53,12 +53,7 @@ namespace Emby.Server.Implementations.Udp
if (!string.IsNullOrEmpty(localUrl)) if (!string.IsNullOrEmpty(localUrl))
{ {
var response = new ServerDiscoveryInfo var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
{
Address = localUrl,
Id = _appHost.SystemId,
Name = _appHost.FriendlyName
};
try try
{ {
@ -79,7 +74,7 @@ namespace Emby.Server.Implementations.Udp
/// Starts the specified port. /// Starts the specified port.
/// </summary> /// </summary>
/// <param name="port">The port.</param> /// <param name="port">The port.</param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
public void Start(int port, CancellationToken cancellationToken) public void Start(int port, CancellationToken cancellationToken)
{ {
_endpoint = new IPEndPoint(IPAddress.Any, port); _endpoint = new IPEndPoint(IPAddress.Any, port);

@ -22,6 +22,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates; using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -92,7 +93,7 @@ namespace Emby.Server.Implementations.Updates
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_config = config; _config = config;
_zipClient = zipClient; _zipClient = zipClient;
_jsonSerializerOptions = JsonDefaults.GetOptions(); _jsonSerializerOptions = JsonDefaults.Options;
_pluginManager = pluginManager; _pluginManager = pluginManager;
} }
@ -194,7 +195,7 @@ namespace Emby.Server.Implementations.Updates
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber); var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
if (plugin != null) if (plugin != null)
{ {
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path); await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
} }
// Remove versions with a target ABI greater then the current application version. // Remove versions with a target ABI greater then the current application version.
@ -500,7 +501,8 @@ namespace Emby.Server.Implementations.Updates
var plugins = _pluginManager.Plugins; var plugins = _pluginManager.Plugins;
foreach (var plugin in plugins) foreach (var plugin in plugins)
{ {
if (plugin.Manifest?.AutoUpdate == false) // Don't auto update when plugin marked not to, or when it's disabled.
if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled)
{ {
continue; continue;
} }
@ -515,7 +517,7 @@ namespace Emby.Server.Implementations.Updates
} }
} }
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken) private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
{ {
var extension = Path.GetExtension(package.SourceUrl); var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase)) if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
@ -567,7 +569,7 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0; stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true); _zipClient.ExtractAllFromZip(stream, targetDir, true);
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir); await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
_pluginManager.ImportPluginFrom(targetDir); _pluginManager.ImportPluginFrom(targetDir);
} }
@ -576,7 +578,7 @@ namespace Emby.Server.Implementations.Updates
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version)) LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version)); ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false); await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false);
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version); _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
return plugin != null; return plugin != null;

@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
{ {
Id = itemId, Id = itemId,
Container = container, Container = container,
Static = @static ?? true, Static = @static ?? false,
Params = @params, Params = @params,
Tag = tag, Tag = tag,
DeviceProfileId = deviceProfileId, DeviceProfileId = deviceProfileId,
@ -168,22 +168,22 @@ namespace Jellyfin.Api.Controllers
Level = level, Level = level,
Framerate = framerate, Framerate = framerate,
MaxFramerate = maxFramerate, MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true, CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks,
Width = width, Width = width,
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? true, DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? true, RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels, TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit, CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId, LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@ -309,7 +309,7 @@ namespace Jellyfin.Api.Controllers
{ {
Id = itemId, Id = itemId,
Container = container, Container = container,
Static = @static ?? true, Static = @static ?? false,
Params = @params, Params = @params,
Tag = tag, Tag = tag,
DeviceProfileId = deviceProfileId, DeviceProfileId = deviceProfileId,
@ -333,22 +333,22 @@ namespace Jellyfin.Api.Controllers
Level = level, Level = level,
Framerate = framerate, Framerate = framerate,
MaxFramerate = maxFramerate, MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true, CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks,
Width = width, Width = width,
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? true, DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? true, RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels, TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit, CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId, LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,

@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ConfigurationController"/> class. /// Initializes a new instance of the <see cref="ConfigurationController"/> class.

@ -95,9 +95,9 @@ namespace Jellyfin.Api.Controllers
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1)); return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
} }
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin? plugin) private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin)
{ {
if (plugin?.Instance is not IHasWebPages hasWebPages) if (plugin.Instance is not IHasWebPages hasWebPages)
{ {
return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>(); return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>();
} }

@ -203,7 +203,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@ -218,14 +218,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions, [FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true) [FromQuery] bool enableAdaptiveBitrateStreaming = true)
{ {
var streamingRequest = new HlsVideoRequestDto var streamingRequest = new HlsVideoRequestDto
{ {
Id = itemId, Id = itemId,
Static = @static ?? true, Static = @static ?? false,
Params = @params, Params = @params,
Tag = tag, Tag = tag,
DeviceProfileId = deviceProfileId, DeviceProfileId = deviceProfileId,
@ -249,28 +249,28 @@ namespace Jellyfin.Api.Controllers
Level = level, Level = level,
Framerate = framerate, Framerate = framerate,
MaxFramerate = maxFramerate, MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true, CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks,
Width = width, Width = width,
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? true, DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? true, RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels, TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit, CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId, LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions, StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
}; };
@ -370,7 +370,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@ -385,14 +385,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions, [FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true) [FromQuery] bool enableAdaptiveBitrateStreaming = true)
{ {
var streamingRequest = new HlsAudioRequestDto var streamingRequest = new HlsAudioRequestDto
{ {
Id = itemId, Id = itemId,
Static = @static ?? true, Static = @static ?? false,
Params = @params, Params = @params,
Tag = tag, Tag = tag,
DeviceProfileId = deviceProfileId, DeviceProfileId = deviceProfileId,
@ -416,28 +416,28 @@ namespace Jellyfin.Api.Controllers
Level = level, Level = level,
Framerate = framerate, Framerate = framerate,
MaxFramerate = maxFramerate, MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true, CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks,
Width = width, Width = width,
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? true, DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? true, RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels, TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit, CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId, LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions, StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
}; };
@ -533,7 +533,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@ -548,14 +548,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions) [FromQuery] Dictionary<string, string> streamOptions)
{ {
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto var streamingRequest = new VideoRequestDto
{ {
Id = itemId, Id = itemId,
Static = @static ?? true, Static = @static ?? false,
Params = @params, Params = @params,
Tag = tag, Tag = tag,
DeviceProfileId = deviceProfileId, DeviceProfileId = deviceProfileId,
@ -579,28 +579,28 @@ namespace Jellyfin.Api.Controllers
Level = level, Level = level,
Framerate = framerate, Framerate = framerate,
MaxFramerate = maxFramerate, MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true, CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks,
Width = width, Width = width,
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? true, DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? true, RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels, TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit, CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId, LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions StreamOptions = streamOptions
}; };
@ -698,7 +698,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@ -713,14 +713,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions) [FromQuery] Dictionary<string, string> streamOptions)
{ {
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new StreamingRequestDto var streamingRequest = new StreamingRequestDto
{ {
Id = itemId, Id = itemId,
Static = @static ?? true, Static = @static ?? false,
Params = @params, Params = @params,
Tag = tag, Tag = tag,
DeviceProfileId = deviceProfileId, DeviceProfileId = deviceProfileId,
@ -744,28 +744,28 @@ namespace Jellyfin.Api.Controllers
Level = level, Level = level,
Framerate = framerate, Framerate = framerate,
MaxFramerate = maxFramerate, MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true, CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks,
Width = width, Width = width,
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? true, DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? true, RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels, TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit, CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId, LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions StreamOptions = streamOptions
}; };
@ -868,7 +868,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@ -883,14 +883,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions) [FromQuery] Dictionary<string, string> streamOptions)
{ {
var streamingRequest = new VideoRequestDto var streamingRequest = new VideoRequestDto
{ {
Id = itemId, Id = itemId,
Container = container, Container = container,
Static = @static ?? true, Static = @static ?? false,
Params = @params, Params = @params,
Tag = tag, Tag = tag,
DeviceProfileId = deviceProfileId, DeviceProfileId = deviceProfileId,
@ -914,28 +914,28 @@ namespace Jellyfin.Api.Controllers
Level = level, Level = level,
Framerate = framerate, Framerate = framerate,
MaxFramerate = maxFramerate, MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true, CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks,
Width = width, Width = width,
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? true, DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? true, RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels, TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit, CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId, LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions StreamOptions = streamOptions
}; };
@ -1040,7 +1040,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@ -1055,14 +1055,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions) [FromQuery] Dictionary<string, string> streamOptions)
{ {
var streamingRequest = new StreamingRequestDto var streamingRequest = new StreamingRequestDto
{ {
Id = itemId, Id = itemId,
Container = container, Container = container,
Static = @static ?? true, Static = @static ?? false,
Params = @params, Params = @params,
Tag = tag, Tag = tag,
DeviceProfileId = deviceProfileId, DeviceProfileId = deviceProfileId,
@ -1086,28 +1086,28 @@ namespace Jellyfin.Api.Controllers
Level = level, Level = level,
Framerate = framerate, Framerate = framerate,
MaxFramerate = maxFramerate, MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true, CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks,
Width = width, Width = width,
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? true, DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? true, RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels, TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit, CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId, LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions StreamOptions = streamOptions
}; };

@ -61,7 +61,13 @@ namespace Jellyfin.Api.Controllers
{ {
// TODO: Deprecate with new iOS app // TODO: Deprecate with new iOS app
var file = segmentId + Path.GetExtension(Request.Path); var file = segmentId + Path.GetExtension(Request.Path);
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath))
{
return BadRequest("Invalid segment.");
}
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext); return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
} }
@ -81,7 +87,13 @@ namespace Jellyfin.Api.Controllers
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{ {
var file = playlistId + Path.GetExtension(Request.Path); var file = playlistId + Path.GetExtension(Request.Path);
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8")
{
return BadRequest("Invalid segment.");
}
return GetFileResult(file, file); return GetFileResult(file, file);
} }
@ -96,7 +108,9 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Videos/ActiveEncodings")] [HttpDelete("Videos/ActiveEncodings")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId) public ActionResult StopEncodingProcess(
[FromQuery, Required] string deviceId,
[FromQuery, Required] string playSessionId)
{ {
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true); _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
return NoContent(); return NoContent();
@ -128,7 +142,12 @@ namespace Jellyfin.Api.Controllers
var file = segmentId + Path.GetExtension(Request.Path); var file = segmentId + Path.GetExtension(Request.Path);
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
file = Path.Combine(transcodeFolderPath, file); file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath))
{
return BadRequest("Invalid segment.");
}
var normalizedPlaylistId = playlistId; var normalizedPlaylistId = playlistId;

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

Loading…
Cancel
Save