Merge branch 'master' into displaypreferences-efcore

pull/3578/head
Patrick Barron 4 years ago committed by GitHub
commit 3d69cea1c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -139,3 +139,25 @@ jobs:
sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
rm $0 rm $0
exit exit
- job: PublishNuget
displayName: 'Publish NuGet packages'
dependsOn:
- BuildPackage
condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NuGetCommand@2
inputs:
command: 'pack'
packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
packDestination: '$(Build.ArtifactStagingDirectory)'
- task: NuGetCommand@2
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
includeNugetOrg: 'true'

@ -11,6 +11,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Dlna.Api namespace Emby.Dlna.Api
{ {
@ -108,7 +109,7 @@ namespace Emby.Dlna.Api
public string Filename { get; set; } public string Filename { get; set; }
} }
public class DlnaServerService : IService, IRequiresRequest public class DlnaServerService : IService
{ {
private const string XMLContentType = "text/xml; charset=UTF-8"; private const string XMLContentType = "text/xml; charset=UTF-8";
@ -127,11 +128,13 @@ namespace Emby.Dlna.Api
public DlnaServerService( public DlnaServerService(
IDlnaManager dlnaManager, IDlnaManager dlnaManager,
IHttpResultFactory httpResultFactory, IHttpResultFactory httpResultFactory,
IServerConfigurationManager configurationManager) IServerConfigurationManager configurationManager,
IHttpContextAccessor httpContextAccessor)
{ {
_dlnaManager = dlnaManager; _dlnaManager = dlnaManager;
_resultFactory = httpResultFactory; _resultFactory = httpResultFactory;
_configurationManager = configurationManager; _configurationManager = configurationManager;
Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor));
} }
private string GetHeader(string name) private string GetHeader(string name)

@ -152,11 +152,15 @@ namespace Emby.Dlna.Eventing
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">"); builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
foreach (var key in stateVariables.Keys) foreach (var key in stateVariables.Keys)
{ {
builder.Append("<e:property>"); builder.Append("<e:property>")
builder.Append("<" + key + ">"); .Append('<')
builder.Append(stateVariables[key]); .Append(key)
builder.Append("</" + key + ">"); .Append('>')
builder.Append("</e:property>"); .Append(stateVariables[key])
.Append("</")
.Append(key)
.Append('>')
.Append("</e:property>");
} }
builder.Append("</e:propertyset>"); builder.Append("</e:propertyset>");

@ -4,12 +4,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Security;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Server;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -334,7 +334,7 @@ namespace Emby.Dlna.PlayTo
return string.Empty; return string.Empty;
} }
return DescriptionXmlBuilder.Escape(value); return SecurityElement.Escape(value);
} }
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken) private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Security;
using System.Text; using System.Text;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
foreach (var att in attributes) foreach (var att in attributes)
{ {
builder.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value); builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value);
} }
builder.Append(">"); builder.Append('>');
builder.Append("<specVersion>"); builder.Append("<specVersion>");
builder.Append("<major>1</major>"); builder.Append("<major>1</major>");
@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
if (!EnableAbsoluteUrls) if (!EnableAbsoluteUrls)
{ {
builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>"); builder.Append("<URLBase>")
.Append(SecurityElement.Escape(_serverAddress))
.Append("</URLBase>");
} }
AppendDeviceInfo(builder); AppendDeviceInfo(builder);
@ -93,91 +96,14 @@ namespace Emby.Dlna.Server
AppendIconList(builder); AppendIconList(builder);
builder.Append("<presentationURL>" + Escape(_serverAddress) + "/web/index.html</presentationURL>"); builder.Append("<presentationURL>")
.Append(SecurityElement.Escape(_serverAddress))
.Append("/web/index.html</presentationURL>");
AppendServiceList(builder); AppendServiceList(builder);
builder.Append("</device>"); builder.Append("</device>");
} }
private static readonly char[] s_escapeChars = new char[]
{
'<',
'>',
'"',
'\'',
'&'
};
private static readonly string[] s_escapeStringPairs = new[]
{
"<",
"&lt;",
">",
"&gt;",
"\"",
"&quot;",
"'",
"&apos;",
"&",
"&amp;"
};
private static string GetEscapeSequence(char c)
{
int num = s_escapeStringPairs.Length;
for (int i = 0; i < num; i += 2)
{
string text = s_escapeStringPairs[i];
string result = s_escapeStringPairs[i + 1];
if (text[0] == c)
{
return result;
}
}
return c.ToString(CultureInfo.InvariantCulture);
}
/// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
/// <returns>The input string with invalid characters replaced.</returns>
/// <param name="str">The string within which to escape invalid characters. </param>
public static string Escape(string str)
{
if (str == null)
{
return null;
}
StringBuilder stringBuilder = null;
int length = str.Length;
int num = 0;
while (true)
{
int num2 = str.IndexOfAny(s_escapeChars, num);
if (num2 == -1)
{
break;
}
if (stringBuilder == null)
{
stringBuilder = new StringBuilder();
}
stringBuilder.Append(str, num, num2 - num);
stringBuilder.Append(GetEscapeSequence(str[num2]));
num = num2 + 1;
}
if (stringBuilder == null)
{
return str;
}
stringBuilder.Append(str, num, length - num);
return stringBuilder.ToString();
}
private void AppendDeviceProperties(StringBuilder builder) private void AppendDeviceProperties(StringBuilder builder)
{ {
builder.Append("<dlna:X_DLNACAP/>"); builder.Append("<dlna:X_DLNACAP/>");
@ -187,32 +113,54 @@ namespace Emby.Dlna.Server
builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>"); builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>"); builder.Append("<friendlyName>")
builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>"); .Append(SecurityElement.Escape(GetFriendlyName()))
builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>"); .Append("</friendlyName>");
builder.Append("<manufacturer>")
builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>"); .Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>"); .Append("</manufacturer>");
builder.Append("<manufacturerURL>")
builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>"); .Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>"); .Append("</manufacturerURL>");
builder.Append("<modelDescription>")
.Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
.Append("</modelDescription>");
builder.Append("<modelName>")
.Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
.Append("</modelName>");
builder.Append("<modelNumber>")
.Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
.Append("</modelNumber>");
builder.Append("<modelURL>")
.Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
.Append("</modelURL>");
if (string.IsNullOrEmpty(_profile.SerialNumber)) if (string.IsNullOrEmpty(_profile.SerialNumber))
{ {
builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>"); builder.Append("<serialNumber>")
.Append(SecurityElement.Escape(_serverId))
.Append("</serialNumber>");
} }
else else
{ {
builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>"); builder.Append("<serialNumber>")
.Append(SecurityElement.Escape(_profile.SerialNumber))
.Append("</serialNumber>");
} }
builder.Append("<UPC/>"); builder.Append("<UPC/>");
builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>"); builder.Append("<UDN>uuid:")
.Append(SecurityElement.Escape(_serverUdn))
.Append("</UDN>");
if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags)) if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
{ {
builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>"); builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">")
.Append(SecurityElement.Escape(_profile.SonyAggregationFlags))
.Append("</av:aggregationFlags>");
} }
} }
@ -250,11 +198,21 @@ namespace Emby.Dlna.Server
{ {
builder.Append("<icon>"); builder.Append("<icon>");
builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>"); builder.Append("<mimetype>")
builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>"); .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>"); .Append("</mimetype>");
builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>"); builder.Append("<width>")
builder.Append("<url>" + BuildUrl(icon.Url) + "</url>"); .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
.Append("</width>");
builder.Append("<height>")
.Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
.Append("</height>");
builder.Append("<depth>")
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
.Append("</depth>");
builder.Append("<url>")
.Append(BuildUrl(icon.Url))
.Append("</url>");
builder.Append("</icon>"); builder.Append("</icon>");
} }
@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
{ {
builder.Append("<service>"); builder.Append("<service>");
builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>"); builder.Append("<serviceType>")
builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>"); .Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>"); .Append("</serviceType>");
builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>"); builder.Append("<serviceId>")
builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>"); .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
.Append("</serviceId>");
builder.Append("<SCPDURL>")
.Append(BuildUrl(service.ScpdUrl))
.Append("</SCPDURL>");
builder.Append("<controlURL>")
.Append(BuildUrl(service.ControlUrl))
.Append("</controlURL>");
builder.Append("<eventSubURL>")
.Append(BuildUrl(service.EventSubUrl))
.Append("</eventSubURL>");
builder.Append("</service>"); builder.Append("</service>");
} }
@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + url; url = _serverAddress.TrimEnd('/') + url;
} }
return Escape(url); return SecurityElement.Escape(url);
} }
private IEnumerable<DeviceIcon> GetIcons() private IEnumerable<DeviceIcon> GetIcons()

@ -1,9 +1,9 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using System.Security;
using System.Text; using System.Text;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Server;
namespace Emby.Dlna.Service namespace Emby.Dlna.Service
{ {
@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
{ {
builder.Append("<action>"); builder.Append("<action>");
builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>"); builder.Append("<name>")
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
.Append("</name>");
builder.Append("<argumentList>"); builder.Append("<argumentList>");
@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
{ {
builder.Append("<argument>"); builder.Append("<argument>");
builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>"); builder.Append("<name>")
builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>"); .Append(SecurityElement.Escape(argument.Name ?? string.Empty))
builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>"); .Append("</name>");
builder.Append("<direction>")
.Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
.Append("</direction>");
builder.Append("<relatedStateVariable>")
.Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
.Append("</relatedStateVariable>");
builder.Append("</argument>"); builder.Append("</argument>");
} }
@ -68,17 +76,25 @@ namespace Emby.Dlna.Service
{ {
var sendEvents = item.SendsEvents ? "yes" : "no"; var sendEvents = item.SendsEvents ? "yes" : "no";
builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">"); builder.Append("<stateVariable sendEvents=\"")
.Append(sendEvents)
.Append("\">");
builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>"); builder.Append("<name>")
builder.Append("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>"); .Append(SecurityElement.Escape(item.Name ?? string.Empty))
.Append("</name>");
builder.Append("<dataType>")
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
.Append("</dataType>");
if (item.AllowedValues.Length > 0) if (item.AllowedValues.Length > 0)
{ {
builder.Append("<allowedValueList>"); builder.Append("<allowedValueList>");
foreach (var allowedValue in item.AllowedValues) foreach (var allowedValue in item.AllowedValues)
{ {
builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</allowedValue>"); builder.Append("<allowedValue>")
.Append(SecurityElement.Escape(allowedValue))
.Append("</allowedValue>");
} }
builder.Append("</allowedValueList>"); builder.Append("</allowedValueList>");

@ -136,8 +136,8 @@ namespace Emby.Naming.Common
CleanDateTimes = new[] CleanDateTimes = new[]
{ {
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*", @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*" @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
}; };
CleanStrings = new[] CleanStrings = new[]
@ -277,7 +277,7 @@ namespace Emby.Naming.Common
// This isn't a Kodi naming rule, but the expression below causes false positives, // This isn't a Kodi naming rule, but the expression below causes false positives,
// so we make sure this one gets tested first. // so we make sure this one gets tested first.
// "Foo Bar 889" // "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$") new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
{ {
IsNamed = true IsNamed = true
}, },
@ -300,32 +300,32 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming // *** End Kodi Standard Naming
// [bar] Foo - 1 [baz] // [bar] Foo - 1 [baz]
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$") new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d+))[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
}, },
// "01.avi" // "01.avi"
new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$") new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
@ -335,34 +335,34 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"([0-9]+)-([0-9]+)"), new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi" // "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "01.blah.avi" // "01.blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.[^\\\/]+$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\.[^\\\/]+$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah" // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "01 episode title.avi" // "01 episode title.avi"
new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>\d{1,3})([^\\\/]*)$") new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// "Episode 16", "Episode 16 - Title" // "Episode 16", "Episode 16 - Title"
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{ {
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
@ -625,17 +625,17 @@ namespace Emby.Naming.Common
AudioBookPartsExpressions = new[] AudioBookPartsExpressions = new[]
{ {
// Detect specified chapters, like CH 01 // Detect specified chapters, like CH 01
@"ch(?:apter)?[\s_-]?(?<chapter>\d+)", @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
// Detect specified parts, like Part 02 // Detect specified parts, like Part 02
@"p(?:ar)?t[\s_-]?(?<part>\d+)", @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)",
// Chapter is often beginning of filename // Chapter is often beginning of filename
@"^(?<chapter>\d+)", "^(?<chapter>[0-9]+)",
// Part if often ending of filename // Part if often ending of filename
@"(?<part>\d+)$", "(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part) // Sometimes named as 0001_005 (chapter_part)
@"(?<chapter>\d+)_(?<part>\d+)", "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number. // Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?<chapter>\d+)" @"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
}; };
var extensions = VideoFileExtensions.ToList(); var extensions = VideoFileExtensions.ToList();
@ -675,16 +675,16 @@ namespace Emby.Naming.Common
MultipleEpisodeExpressions = new string[] MultipleEpisodeExpressions = new string[]
{ {
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$" @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$"
}.Select(i => new EpisodeExpression(i) }.Select(i => new EpisodeExpression(i)
{ {
IsNamed = true IsNamed = true

@ -192,7 +192,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the application paths. /// Gets or sets the application paths.
/// </summary> /// </summary>
/// <value>The application paths.</value> /// <value>The application paths.</value>
protected ServerApplicationPaths ApplicationPaths { get; set; } protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary> /// <summary>
/// Gets or sets all concrete types. /// Gets or sets all concrete types.
@ -236,7 +236,7 @@ namespace Emby.Server.Implementations
/// Initializes a new instance of the <see cref="ApplicationHost" /> class. /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
/// </summary> /// </summary>
public ApplicationHost( public ApplicationHost(
ServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStartupOptions options, IStartupOptions options,
IFileSystem fileSystem, IFileSystem fileSystem,
@ -792,7 +792,6 @@ namespace Emby.Server.Implementations
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>()); Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
} }

@ -1111,7 +1111,8 @@ namespace Emby.Server.Implementations.Data
continue; continue;
} }
str.Append(ToValueString(i) + "|"); str.Append(ToValueString(i))
.Append('|');
} }
str.Length -= 1; // Remove last | str.Length -= 1; // Remove last |
@ -2472,7 +2473,7 @@ namespace Emby.Server.Implementations.Data
var item = query.SimilarTo; var item = query.SimilarTo;
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("("); builder.Append('(');
if (string.IsNullOrEmpty(item.OfficialRating)) if (string.IsNullOrEmpty(item.OfficialRating))
{ {
@ -2510,7 +2511,7 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrEmpty(query.SearchTerm)) if (!string.IsNullOrEmpty(query.SearchTerm))
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append("("); builder.Append('(');
builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)"); builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
@ -5239,7 +5240,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{ {
if (i > 0) if (i > 0)
{ {
insertText.Append(","); insertText.Append(',');
} }
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture)); insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
@ -6332,7 +6333,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
foreach (var column in _mediaAttachmentSaveColumns.Skip(1)) foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
{ {
insertText.Append("@" + column + index + ","); insertText.Append('@')
.Append(column)
.Append(index)
.Append(',');
} }
insertText.Length -= 1; insertText.Length -= 1;

@ -34,10 +34,10 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
<PackageReference Include="Mono.Nat" Version="2.0.1" /> <PackageReference Include="Mono.Nat" Version="2.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />

@ -1,3 +1,4 @@
using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Udp; using Emby.Server.Implementations.Udp;
@ -48,8 +49,16 @@ namespace Emby.Server.Implementations.EntryPoints
/// <inheritdoc /> /// <inheritdoc />
public Task RunAsync() public Task RunAsync()
{ {
_udpServer = new UdpServer(_logger, _appHost, _config); try
_udpServer.Start(PortNumber, _cancellationTokenSource.Token); {
_udpServer = new UdpServer(_logger, _appHost, _config);
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
}
catch (SocketException ex)
{
_logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
}
return Task.CompletedTask; return Task.CompletedTask;
} }

@ -245,6 +245,16 @@ namespace Emby.Server.Implementations.IO
if (info is FileInfo fileInfo) if (info is FileInfo fileInfo)
{ {
result.Length = fileInfo.Length; result.Length = fileInfo.Length;
// Issue #2354 get the size of files behind symbolic links
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
{
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
{
result.Length = thisFileStream.Length;
}
}
result.DirectoryName = fileInfo.DirectoryName; result.DirectoryName = fileInfo.DirectoryName;
} }

@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null) if (parent != null)
{ {
// Don't resolve these into audio files // Don't resolve these into audio files
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename) if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename)) && _libraryManager.IsAudioFile(filename))
{ {
return true; return true;

@ -11,6 +11,17 @@ namespace Emby.Server.Implementations.Library
{ {
public class ExclusiveLiveStream : ILiveStream public class ExclusiveLiveStream : ILiveStream
{ {
private readonly Func<Task> _closeFn;
public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
{
MediaSource = mediaSource;
EnableStreamSharing = false;
_closeFn = closeFn;
ConsumerCount = 1;
UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
public int ConsumerCount { get; set; } public int ConsumerCount { get; set; }
public string OriginalStreamId { get; set; } public string OriginalStreamId { get; set; }
@ -21,18 +32,7 @@ namespace Emby.Server.Implementations.Library
public MediaSourceInfo MediaSource { get; set; } public MediaSourceInfo MediaSource { get; set; }
public string UniqueId { get; private set; } public string UniqueId { get; }
private Func<Task> _closeFn;
public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
{
MediaSource = mediaSource;
EnableStreamSharing = false;
_closeFn = closeFn;
ConsumerCount = 1;
UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
public Task Close() public Task Close()
{ {

@ -59,6 +59,8 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
public class LibraryManager : ILibraryManager public class LibraryManager : ILibraryManager
{ {
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger; private readonly ILogger<LibraryManager> _logger;
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
@ -74,63 +76,24 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache; private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private NamingOptions _namingOptions;
private string[] _videoFileExtensions;
private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
private IProviderManager ProviderManager => _providerManagerFactory.Value;
private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
/// <summary>
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
private ILibraryPostScanTask[] PostscanTasks { get; set; }
/// <summary>
/// Gets or sets the intro providers.
/// </summary>
/// <value>The intro providers.</value>
private IIntroProvider[] IntroProviders { get; set; }
/// <summary>
/// Gets or sets the list of entity resolution ignore rules.
/// </summary>
/// <value>The entity resolution ignore rules.</value>
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
/// <summary>
/// Gets or sets the list of currently registered entity resolvers.
/// </summary>
/// <value>The entity resolvers enumerable.</value>
private IItemResolver[] EntityResolvers { get; set; }
private IMultiItemResolver[] MultiItemResolvers { get; set; }
/// <summary> /// <summary>
/// Gets or sets the comparers. /// The _root folder sync lock.
/// </summary> /// </summary>
/// <value>The comparers.</value> private readonly object _rootFolderSyncLock = new object();
private IBaseItemComparer[] Comparers { get; set; } private readonly object _userRootFolderSyncLock = new object();
/// <summary> private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
/// Occurs when [item added].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemAdded;
/// <summary> private NamingOptions _namingOptions;
/// Occurs when [item updated]. private string[] _videoFileExtensions;
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemUpdated;
/// <summary> /// <summary>
/// Occurs when [item removed]. /// The _root folder.
/// </summary> /// </summary>
public event EventHandler<ItemChangeEventArgs> ItemRemoved; private volatile AggregateFolder _rootFolder;
private volatile UserRootFolder _userRootFolder;
public bool IsScanRunning { get; private set; } private bool _wizardCompleted;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LibraryManager" /> class. /// Initializes a new instance of the <see cref="LibraryManager" /> class.
@ -185,37 +148,19 @@ namespace Emby.Server.Implementations.Library
} }
/// <summary> /// <summary>
/// Adds the parts. /// Occurs when [item added].
/// </summary> /// </summary>
/// <param name="rules">The rules.</param> public event EventHandler<ItemChangeEventArgs> ItemAdded;
/// <param name="resolvers">The resolvers.</param>
/// <param name="introProviders">The intro providers.</param>
/// <param name="itemComparers">The item comparers.</param>
/// <param name="postscanTasks">The post scan tasks.</param>
public void AddParts(
IEnumerable<IResolverIgnoreRule> rules,
IEnumerable<IItemResolver> resolvers,
IEnumerable<IIntroProvider> introProviders,
IEnumerable<IBaseItemComparer> itemComparers,
IEnumerable<ILibraryPostScanTask> postscanTasks)
{
EntityResolutionIgnoreRules = rules.ToArray();
EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
IntroProviders = introProviders.ToArray();
Comparers = itemComparers.ToArray();
PostscanTasks = postscanTasks.ToArray();
}
/// <summary> /// <summary>
/// The _root folder. /// Occurs when [item updated].
/// </summary> /// </summary>
private volatile AggregateFolder _rootFolder; public event EventHandler<ItemChangeEventArgs> ItemUpdated;
/// <summary> /// <summary>
/// The _root folder sync lock. /// Occurs when [item removed].
/// </summary> /// </summary>
private readonly object _rootFolderSyncLock = new object(); public event EventHandler<ItemChangeEventArgs> ItemRemoved;
/// <summary> /// <summary>
/// Gets the root folder. /// Gets the root folder.
@ -240,7 +185,68 @@ namespace Emby.Server.Implementations.Library
} }
} }
private bool _wizardCompleted; private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
private IProviderManager ProviderManager => _providerManagerFactory.Value;
private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
/// <summary>
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
private ILibraryPostScanTask[] PostscanTasks { get; set; }
/// <summary>
/// Gets or sets the intro providers.
/// </summary>
/// <value>The intro providers.</value>
private IIntroProvider[] IntroProviders { get; set; }
/// <summary>
/// Gets or sets the list of entity resolution ignore rules.
/// </summary>
/// <value>The entity resolution ignore rules.</value>
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
/// <summary>
/// Gets or sets the list of currently registered entity resolvers.
/// </summary>
/// <value>The entity resolvers enumerable.</value>
private IItemResolver[] EntityResolvers { get; set; }
private IMultiItemResolver[] MultiItemResolvers { get; set; }
/// <summary>
/// Gets or sets the comparers.
/// </summary>
/// <value>The comparers.</value>
private IBaseItemComparer[] Comparers { get; set; }
public bool IsScanRunning { get; private set; }
/// <summary>
/// Adds the parts.
/// </summary>
/// <param name="rules">The rules.</param>
/// <param name="resolvers">The resolvers.</param>
/// <param name="introProviders">The intro providers.</param>
/// <param name="itemComparers">The item comparers.</param>
/// <param name="postscanTasks">The post scan tasks.</param>
public void AddParts(
IEnumerable<IResolverIgnoreRule> rules,
IEnumerable<IItemResolver> resolvers,
IEnumerable<IIntroProvider> introProviders,
IEnumerable<IBaseItemComparer> itemComparers,
IEnumerable<ILibraryPostScanTask> postscanTasks)
{
EntityResolutionIgnoreRules = rules.ToArray();
EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
IntroProviders = introProviders.ToArray();
Comparers = itemComparers.ToArray();
PostscanTasks = postscanTasks.ToArray();
}
/// <summary> /// <summary>
/// Records the configuration values. /// Records the configuration values.
@ -340,7 +346,7 @@ namespace Emby.Server.Implementations.Library
if (item is LiveTvProgram) if (item is LiveTvProgram)
{ {
_logger.LogDebug( _logger.LogDebug(
"Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name, item.GetType().Name,
item.Name ?? "Unknown name", item.Name ?? "Unknown name",
item.Path ?? string.Empty, item.Path ?? string.Empty,
@ -349,7 +355,7 @@ namespace Emby.Server.Implementations.Library
else else
{ {
_logger.LogInformation( _logger.LogInformation(
"Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name, item.GetType().Name,
item.Name ?? "Unknown name", item.Name ?? "Unknown name",
item.Path ?? string.Empty, item.Path ?? string.Empty,
@ -367,7 +373,12 @@ namespace Emby.Server.Implementations.Library
continue; continue;
} }
_logger.LogDebug("Deleting path {MetadataPath}", metadataPath); _logger.LogDebug(
"Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
metadataPath,
item.Id);
try try
{ {
@ -391,7 +402,13 @@ namespace Emby.Server.Implementations.Library
{ {
try try
{ {
_logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName); _logger.LogInformation(
"Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
if (fileSystemInfo.IsDirectory) if (fileSystemInfo.IsDirectory)
{ {
Directory.Delete(fileSystemInfo.FullName, true); Directory.Delete(fileSystemInfo.FullName, true);
@ -500,7 +517,7 @@ namespace Emby.Server.Implementations.Library
// Try to normalize paths located underneath program-data in an attempt to make them more portable // Try to normalize paths located underneath program-data in an attempt to make them more portable
key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length) key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
.TrimStart(new[] { '/', '\\' }) .TrimStart(new[] { '/', '\\' })
.Replace("/", "\\"); .Replace('/', '\\');
} }
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds) if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@ -763,14 +780,11 @@ namespace Emby.Server.Implementations.Library
return rootFolder; return rootFolder;
} }
private volatile UserRootFolder _userRootFolder;
private readonly object _syncLock = new object();
public Folder GetUserRootFolder() public Folder GetUserRootFolder()
{ {
if (_userRootFolder == null) if (_userRootFolder == null)
{ {
lock (_syncLock) lock (_userRootFolderSyncLock)
{ {
if (_userRootFolder == null) if (_userRootFolder == null)
{ {
@ -1320,7 +1334,7 @@ namespace Emby.Server.Implementations.Library
return new QueryResult<BaseItem> return new QueryResult<BaseItem>
{ {
Items = _itemRepository.GetItemList(query).ToArray() Items = _itemRepository.GetItemList(query)
}; };
} }
@ -1451,11 +1465,9 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItems(query); return _itemRepository.GetItems(query);
} }
var list = _itemRepository.GetItemList(query);
return new QueryResult<BaseItem> return new QueryResult<BaseItem>
{ {
Items = list Items = _itemRepository.GetItemList(query)
}; };
} }
@ -1864,7 +1876,8 @@ namespace Emby.Server.Implementations.Library
} }
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray(); var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
if (outdated.Length == 0) // Skip image processing if current or live tv source
if (outdated.Length == 0 || item.SourceType != SourceType.Library)
{ {
RegisterItem(item); RegisterItem(item);
return; return;
@ -1933,12 +1946,9 @@ namespace Emby.Server.Implementations.Library
/// <summary> /// <summary>
/// Updates the item. /// Updates the item.
/// </summary> /// </summary>
public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{ {
// Don't iterate multiple times foreach (var item in items)
var itemsList = items.ToList();
foreach (var item in itemsList)
{ {
if (item.IsFileProtocol) if (item.IsFileProtocol)
{ {
@ -1950,11 +1960,11 @@ namespace Emby.Server.Implementations.Library
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate); UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
} }
_itemRepository.SaveItems(itemsList, cancellationToken); _itemRepository.SaveItems(items, cancellationToken);
if (ItemUpdated != null) if (ItemUpdated != null)
{ {
foreach (var item in itemsList) foreach (var item in items)
{ {
// With the live tv guide this just creates too much noise // With the live tv guide this just creates too much noise
if (item.SourceType != SourceType.Library) if (item.SourceType != SourceType.Library)
@ -2177,8 +2187,6 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault(i => !string.IsNullOrEmpty(i)); .FirstOrDefault(i => !string.IsNullOrEmpty(i));
} }
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
public UserView GetNamedView( public UserView GetNamedView(
User user, User user,
string name, string name,
@ -2476,14 +2484,9 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
var episodeInfo = episode.IsFileProtocol ? var episodeInfo = episode.IsFileProtocol
resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) : ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
new Naming.TV.EpisodeInfo(); : new Naming.TV.EpisodeInfo();
if (episodeInfo == null)
{
episodeInfo = new Naming.TV.EpisodeInfo();
}
try try
{ {
@ -2491,11 +2494,13 @@ namespace Emby.Server.Implementations.Library
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase)) if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
{ {
// Read from metadata // Read from metadata
var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest var mediaInfo = _mediaEncoder.GetMediaInfo(
{ new MediaInfoRequest
MediaSource = episode.GetMediaSources(false)[0], {
MediaType = DlnaProfileType.Video MediaSource = episode.GetMediaSources(false)[0],
}, CancellationToken.None).GetAwaiter().GetResult(); MediaType = DlnaProfileType.Video
},
CancellationToken.None).GetAwaiter().GetResult();
if (mediaInfo.ParentIndexNumber > 0) if (mediaInfo.ParentIndexNumber > 0)
{ {
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber; episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@ -2653,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
var videos = videoListResolver.Resolve(fileSystemChildren); var videos = videoListResolver.Resolve(fileSystemChildren);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase)); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
if (currentVideo != null) if (currentVideo != null)
{ {
@ -2670,9 +2675,7 @@ namespace Emby.Server.Implementations.Library
.Select(video => .Select(video =>
{ {
// Try to retrieve it from the db. If we don't find it, use the resolved version // Try to retrieve it from the db. If we don't find it, use the resolved version
var dbItem = GetItemById(video.Id) as Trailer; if (GetItemById(video.Id) is Trailer dbItem)
if (dbItem != null)
{ {
video = dbItem; video = dbItem;
} }
@ -2999,23 +3002,6 @@ namespace Emby.Server.Implementations.Library
}); });
} }
private static bool ValidateNetworkPath(string path)
{
// if (Environment.OSVersion.Platform == PlatformID.Win32NT)
//{
// // We can't validate protocol-based paths, so just allow them
// if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
// {
// return Directory.Exists(path);
// }
//}
// Without native support for unc, we cannot validate this when running under mono
return true;
}
private const string ShortcutFileExtension = ".mblink";
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
{ {
AddMediaPathInternal(virtualFolderName, pathInfo, true); AddMediaPathInternal(virtualFolderName, pathInfo, true);
@ -3040,11 +3026,6 @@ namespace Emby.Server.Implementations.Library
throw new FileNotFoundException("The path does not exist."); throw new FileNotFoundException("The path does not exist.");
} }
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new FileNotFoundException("The network path does not exist.");
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@ -3083,11 +3064,6 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(pathInfo)); throw new ArgumentNullException(nameof(pathInfo));
} }
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new FileNotFoundException("The network path does not exist.");
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@ -3219,7 +3195,8 @@ namespace Emby.Server.Implementations.Library
if (!Directory.Exists(virtualFolderPath)) if (!Directory.Exists(virtualFolderPath))
{ {
throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName)); throw new FileNotFoundException(
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
} }
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)

@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.Library
{ {
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IJsonSerializer _json;
private IJsonSerializer _json; private readonly IApplicationPaths _appPaths;
private IApplicationPaths _appPaths;
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths) public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
{ {
@ -72,13 +71,14 @@ namespace Emby.Server.Implementations.Library
mediaSource.AnalyzeDurationMs = 3000; mediaSource.AnalyzeDurationMs = 3000;
mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest mediaInfo = await _mediaEncoder.GetMediaInfo(
{ new MediaInfoRequest
MediaSource = mediaSource, {
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, MediaSource = mediaSource,
ExtractChapters = false MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
ExtractChapters = false
}, cancellationToken).ConfigureAwait(false); },
cancellationToken).ConfigureAwait(false);
if (cacheFilePath != null) if (cacheFilePath != null)
{ {
@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.RunTimeTicks = null; mediaSource.RunTimeTicks = null;
} }
var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio); var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
if (audioStream == null || audioStream.Index == -1) if (audioStream == null || audioStream.Index == -1)
{ {
@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.DefaultAudioStreamIndex = audioStream.Index; mediaSource.DefaultAudioStreamIndex = audioStream.Index;
} }
var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video); var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
if (videoStream != null) if (videoStream != null)
{ {
if (!videoStream.BitRate.HasValue) if (!videoStream.BitRate.HasValue)

@ -29,6 +29,9 @@ namespace Emby.Server.Implementations.Library
{ {
public class MediaSourceManager : IMediaSourceManager, IDisposable public class MediaSourceManager : IMediaSourceManager, IDisposable
{ {
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char LiveStreamIdDelimeter = '_';
private readonly IItemRepository _itemRepo; private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -40,6 +43,11 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager; private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
private readonly object _disposeLock = new object();
private IMediaSourceProvider[] _providers; private IMediaSourceProvider[] _providers;
public MediaSourceManager( public MediaSourceManager(
@ -368,7 +376,6 @@ namespace Emby.Server.Implementations.Library
} }
} }
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference) var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference); ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
@ -451,9 +458,6 @@ namespace Emby.Server.Implementations.Library
.ToList(); .ToList();
} }
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken) public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
{ {
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
@ -855,9 +859,6 @@ namespace Emby.Server.Implementations.Library
} }
} }
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char LiveStreamIdDelimeter = '_';
private Tuple<IMediaSourceProvider, string> GetProvider(string key) private Tuple<IMediaSourceProvider, string> GetProvider(string key)
{ {
if (string.IsNullOrEmpty(key)) if (string.IsNullOrEmpty(key))
@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);
GC.SuppressFinalize(this);
} }
private readonly object _disposeLock = new object();
/// <summary> /// <summary>
/// Releases unmanaged and - optionally - managed resources. /// Releases unmanaged and - optionally - managed resources.
/// </summary> /// </summary>

@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
} }
// load forced subs if we have found no suitable full subtitles // load forced subs if we have found no suitable full subtitles
stream = stream ?? streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)); stream ??= streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
if (stream != null) if (stream != null)
{ {

@ -20,13 +20,11 @@ namespace Emby.Server.Implementations.Library
{ {
public class SearchEngine : ISearchEngine public class SearchEngine : ISearchEngine
{ {
private readonly ILogger<SearchEngine> _logger;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager) public SearchEngine(ILibraryManager libraryManager, IUserManager userManager)
{ {
_logger = logger;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userManager = userManager; _userManager = userManager;
} }
@ -34,11 +32,7 @@ namespace Emby.Server.Implementations.Library
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query) public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
{ {
User user = null; User user = null;
if (query.UserId != Guid.Empty)
if (query.UserId.Equals(Guid.Empty))
{
}
else
{ {
user = _userManager.GetUserById(query.UserId); user = _userManager.GetUserById(query.UserId);
} }
@ -48,19 +42,19 @@ namespace Emby.Server.Implementations.Library
if (query.StartIndex.HasValue) if (query.StartIndex.HasValue)
{ {
results = results.Skip(query.StartIndex.Value).ToList(); results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
} }
if (query.Limit.HasValue) if (query.Limit.HasValue)
{ {
results = results.Take(query.Limit.Value).ToList(); results = results.GetRange(0, query.Limit.Value);
} }
return new QueryResult<SearchHintInfo> return new QueryResult<SearchHintInfo>
{ {
TotalRecordCount = totalRecordCount, TotalRecordCount = totalRecordCount,
Items = results.ToArray() Items = results
}; };
} }
@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
if (string.IsNullOrEmpty(searchTerm)) if (string.IsNullOrEmpty(searchTerm))
{ {
throw new ArgumentNullException("SearchTerm can't be empty.", nameof(searchTerm)); throw new ArgumentException("SearchTerm can't be empty.", nameof(query));
} }
searchTerm = searchTerm.Trim().RemoveDiacritics(); searchTerm = searchTerm.Trim().RemoveDiacritics();

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Library;
@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@ -54,7 +54,6 @@ namespace Emby.Server.Implementations.LiveTv
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IChannelManager _channelManager; private readonly IChannelManager _channelManager;
private readonly LiveTvDtoService _tvDtoService; private readonly LiveTvDtoService _tvDtoService;
@ -73,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv
ILibraryManager libraryManager, ILibraryManager libraryManager,
ITaskManager taskManager, ITaskManager taskManager,
ILocalizationManager localization, ILocalizationManager localization,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem, IFileSystem fileSystem,
IChannelManager channelManager, IChannelManager channelManager,
LiveTvDtoService liveTvDtoService) LiveTvDtoService liveTvDtoService)
@ -85,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv
_libraryManager = libraryManager; _libraryManager = libraryManager;
_taskManager = taskManager; _taskManager = taskManager;
_localization = localization; _localization = localization;
_jsonSerializer = jsonSerializer;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_dtoService = dtoService; _dtoService = dtoService;
_userDataManager = userDataManager; _userDataManager = userDataManager;
@ -2234,7 +2231,7 @@ namespace Emby.Server.Implementations.LiveTv
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
{ {
info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info)); info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info));
var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@ -2278,7 +2275,7 @@ namespace Emby.Server.Implementations.LiveTv
{ {
// Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
// ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
info = _jsonSerializer.DeserializeFromString<ListingsProviderInfo>(_jsonSerializer.SerializeToString(info)); info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info));
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));

@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Gerät: {1}", "AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung", "Application": "Anwendung",
"Artists": "Interpreten", "Artists": "Interpreten",
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert", "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
"Books": "Bücher", "Books": "Bücher",
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen", "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
"Channels": "Kanäle", "Channels": "Kanäle",

@ -5,47 +5,47 @@
"Artists": "Artis", "Artists": "Artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan", "AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku-buku", "Books": "Buku-buku",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", "CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
"Channels": "Saluran", "Channels": "Saluran",
"ChapterNameValue": "Chapter {0}", "ChapterNameValue": "Bab {0}",
"Collections": "Koleksi", "Collections": "Koleksi",
"DeviceOfflineWithName": "{0} has disconnected", "DeviceOfflineWithName": "{0} telah diputuskan sambungan",
"DeviceOnlineWithName": "{0} is connected", "DeviceOnlineWithName": "{0} telah disambung",
"FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}", "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
"Favorites": "Favorites", "Favorites": "Kegemaran",
"Folders": "Folders", "Folders": "Fail-fail",
"Genres": "Genre-genre", "Genres": "Genre-genre",
"HeaderAlbumArtists": "Album Artists", "HeaderAlbumArtists": "Album Artis-artis",
"HeaderCameraUploads": "Muatnaik Kamera", "HeaderCameraUploads": "Muatnaik Kamera",
"HeaderContinueWatching": "Terus Menonton", "HeaderContinueWatching": "Terus Menonton",
"HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteAlbums": "Album-album Kegemaran",
"HeaderFavoriteArtists": "Favorite Artists", "HeaderFavoriteArtists": "Artis-artis Kegemaran",
"HeaderFavoriteEpisodes": "Favorite Episodes", "HeaderFavoriteEpisodes": "Episod-episod Kegemaran",
"HeaderFavoriteShows": "Favorite Shows", "HeaderFavoriteShows": "Rancangan-rancangan Kegemaran",
"HeaderFavoriteSongs": "Favorite Songs", "HeaderFavoriteSongs": "Lagu-lagu Kegemaran",
"HeaderLiveTV": "Live TV", "HeaderLiveTV": "TV Siaran Langsung",
"HeaderNextUp": "Next Up", "HeaderNextUp": "Seterusnya",
"HeaderRecordingGroups": "Recording Groups", "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman",
"HomeVideos": "Home videos", "HomeVideos": "Video Personal",
"Inherit": "Inherit", "Inherit": "Mewarisi",
"ItemAddedWithName": "{0} was added to the library", "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka",
"ItemRemovedWithName": "{0} was removed from the library", "ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
"LabelIpAddressValue": "Alamat IP: {0}", "LabelIpAddressValue": "Alamat IP: {0}",
"LabelRunningTimeValue": "Running time: {0}", "LabelRunningTimeValue": "Masa berjalan: {0}",
"Latest": "Latest", "Latest": "Terbaru",
"MessageApplicationUpdated": "Jellyfin Server has been updated", "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
"MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
"MixedContent": "Mixed content", "MixedContent": "Kandungan campuran",
"Movies": "Movies", "Movies": "Filem",
"Music": "Muzik", "Music": "Muzik",
"MusicVideos": "Video muzik", "MusicVideos": "Video muzik",
"NameInstallFailed": "{0} installation failed", "NameInstallFailed": "{0} pemasangan gagal",
"NameSeasonNumber": "Season {0}", "NameSeasonNumber": "Musim {0}",
"NameSeasonUnknown": "Season Unknown", "NameSeasonUnknown": "Musim Tidak Diketahui",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
"NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
"NotificationOptionApplicationUpdateInstalled": "Application update installed", "NotificationOptionApplicationUpdateInstalled": "Application update installed",
"NotificationOptionAudioPlayback": "Audio playback started", "NotificationOptionAudioPlayback": "Audio playback started",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped", "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",

@ -67,5 +67,7 @@
"Artists": "นักแสดง", "Artists": "นักแสดง",
"Application": "แอปพลิเคชั่น", "Application": "แอปพลิเคชั่น",
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}", "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
"Albums": "อัลบั้ม" "Albums": "อัลบั้ม",
"ScheduledTaskStartedWithName": "{0} เริ่มต้น",
"ScheduledTaskFailedWithName": "{0} ล้มเหลว"
} }

@ -152,7 +152,12 @@ namespace Emby.Server.Implementations.Networking
return true; return true;
} }
byte[] octet = IPAddress.Parse(endpoint).GetAddressBytes(); if (!IPAddress.TryParse(endpoint, out var ipAddress))
{
return false;
}
byte[] octet = ipAddress.GetAddressBytes();
if ((octet[0] == 10) || if ((octet[0] == 10) ||
(octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
@ -268,6 +273,12 @@ namespace Emby.Server.Implementations.Networking
string excludeAddress = "[" + addressString + "]"; string excludeAddress = "[" + addressString + "]";
var subnets = LocalSubnetsFn(); var subnets = LocalSubnetsFn();
// Include any address if LAN subnets aren't specified
if (subnets.Length == 0)
{
return true;
}
// Exclude any addresses if they appear in the LAN list in [ ] // Exclude any addresses if they appear in the LAN list in [ ]
if (Array.IndexOf(subnets, excludeAddress) != -1) if (Array.IndexOf(subnets, excludeAddress) != -1)
{ {

@ -189,5 +189,4 @@ namespace Emby.Server.Implementations.Services
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()); return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
} }
} }
} }

@ -6,6 +6,7 @@ using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Services
var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false); var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
httpHost.ApplyRequestFilters(httpReq, httpRes, request); httpHost.ApplyRequestFilters(httpReq, httpRes, request);
httpRes.HttpContext.SetServiceStackRequest(httpReq);
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
// Apply response filters // Apply response filters

@ -488,7 +488,8 @@ namespace Emby.Server.Implementations.Services
sb.Append(value); sb.Append(value);
for (var j = pathIx + 1; j < requestComponents.Length; j++) for (var j = pathIx + 1; j < requestComponents.Length; j++)
{ {
sb.Append(PathSeperatorChar + requestComponents[j]); sb.Append(PathSeperatorChar)
.Append(requestComponents[j]);
} }
value = sb.ToString(); value = sb.ToString();
@ -505,7 +506,8 @@ namespace Emby.Server.Implementations.Services
pathIx++; pathIx++;
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{ {
sb.Append(PathSeperatorChar + requestComponents[pathIx++]); sb.Append(PathSeperatorChar)
.Append(requestComponents[pathIx++]);
} }
value = sb.ToString(); value = sb.ToString();

@ -117,23 +117,20 @@ namespace Emby.Server.Implementations.TV
limit = limit.Value + 10; limit = limit.Value + 10;
} }
var items = _libraryManager.GetItemList(new InternalItemsQuery(user) var items = _libraryManager
{ .GetItemList(
IncludeItemTypes = new[] { typeof(Episode).Name }, new InternalItemsQuery(user)
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
SeriesPresentationUniqueKey = presentationUniqueKey,
Limit = limit,
DtoOptions = new DtoOptions
{
Fields = new ItemFields[]
{ {
ItemFields.SeriesPresentationUniqueKey IncludeItemTypes = new[] { typeof(Episode).Name },
}, OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
EnableImages = false SeriesPresentationUniqueKey = presentationUniqueKey,
}, Limit = limit,
GroupBySeriesPresentationUniqueKey = true DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
GroupBySeriesPresentationUniqueKey = true
}, parentsFolders.ToList()).Cast<Episode>().Select(GetUniqueSeriesKey); }, parentsFolders.ToList())
.Cast<Episode>()
.Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
.Select(GetUniqueSeriesKey);
// Avoid implicitly captured closure // Avoid implicitly captured closure
var episodes = GetNextUpEpisodes(request, user, items, dtoOptions); var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);

@ -148,6 +148,11 @@ namespace Emby.Server.Implementations.Updates
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest); _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
return Array.Empty<PackageInfo>(); return Array.Empty<PackageInfo>();
} }
catch (HttpRequestException ex)
{
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
return Array.Empty<PackageInfo>();
}
} }
/// <inheritdoc /> /// <inheritdoc />

@ -46,14 +46,12 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Configuration")] [HttpGet("Configuration")]
public StartupConfigurationDto GetStartupConfiguration() public StartupConfigurationDto GetStartupConfiguration()
{ {
var result = new StartupConfigurationDto return new StartupConfigurationDto
{ {
UICulture = _config.Configuration.UICulture, UICulture = _config.Configuration.UICulture,
MetadataCountryCode = _config.Configuration.MetadataCountryCode, MetadataCountryCode = _config.Configuration.MetadataCountryCode,
PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
}; };
return result;
} }
/// <summary> /// <summary>
@ -92,10 +90,10 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <returns>The first user.</returns> /// <returns>The first user.</returns>
[HttpGet("User")] [HttpGet("User")]
public StartupUserDto GetFirstUser() public async Task<StartupUserDto> GetFirstUser()
{ {
// TODO: Remove this method when startup wizard no longer requires an existing user. // TODO: Remove this method when startup wizard no longer requires an existing user.
_userManager.Initialize(); await _userManager.InitializeAsync().ConfigureAwait(false);
var user = _userManager.Users.First(); var user = _userManager.Users.First();
return new StartupUserDto return new StartupUserDto
{ {

@ -14,7 +14,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.5" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
</ItemGroup> </ItemGroup>

@ -19,8 +19,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.6" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -24,11 +24,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.6">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using System.Linq; using System.Linq;
using Jellyfin.Data; using Jellyfin.Data;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
@ -135,6 +136,18 @@ namespace Jellyfin.Server.Implementations
return base.SaveChanges(); return base.SaveChanges();
} }
/// <inheritdoc/>
public override void Dispose()
{
foreach (var entry in ChangeTracker.Entries())
{
entry.State = EntityState.Detached;
}
GC.SuppressFinalize(this);
base.Dispose();
}
/// <inheritdoc /> /// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {

@ -6,6 +6,7 @@ using System.IO;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -23,7 +24,7 @@ namespace Jellyfin.Server.Implementations.Users
private const string BaseResetFileName = "passwordreset"; private const string BaseResetFileName = "passwordreset";
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IUserManager _userManager; private readonly IApplicationHost _appHost;
private readonly string _passwordResetFileBase; private readonly string _passwordResetFileBase;
private readonly string _passwordResetFileBaseDir; private readonly string _passwordResetFileBaseDir;
@ -33,16 +34,17 @@ namespace Jellyfin.Server.Implementations.Users
/// </summary> /// </summary>
/// <param name="configurationManager">The configuration manager.</param> /// <param name="configurationManager">The configuration manager.</param>
/// <param name="jsonSerializer">The JSON serializer.</param> /// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="userManager">The user manager.</param> /// <param name="appHost">The application host.</param>
public DefaultPasswordResetProvider( public DefaultPasswordResetProvider(
IServerConfigurationManager configurationManager, IServerConfigurationManager configurationManager,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IUserManager userManager) IApplicationHost appHost)
{ {
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_userManager = userManager; _appHost = appHost;
// TODO: Remove the circular dependency on UserManager
} }
/// <inheritdoc /> /// <inheritdoc />
@ -54,6 +56,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc /> /// <inheritdoc />
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{ {
var userManager = _appHost.Resolve<IUserManager>();
var usersReset = new List<string>(); var usersReset = new List<string>();
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{ {
@ -72,10 +75,10 @@ namespace Jellyfin.Server.Implementations.Users
pin.Replace("-", string.Empty, StringComparison.Ordinal), pin.Replace("-", string.Empty, StringComparison.Ordinal),
StringComparison.InvariantCultureIgnoreCase)) StringComparison.InvariantCultureIgnoreCase))
{ {
var resetUser = _userManager.GetUserByName(spr.UserName) var resetUser = userManager.GetUserByName(spr.UserName)
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); await userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
usersReset.Add(resetUser.Username); usersReset.Add(resetUser.Username);
File.Delete(resetFile); File.Delete(resetFile);
} }
@ -121,7 +124,6 @@ namespace Jellyfin.Server.Implementations.Users
} }
user.EasyPassword = pin; user.EasyPassword = pin;
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
return new ForgotPasswordResult return new ForgotPasswordResult
{ {

@ -39,12 +39,11 @@ namespace Jellyfin.Server.Implementations.Users
private readonly IApplicationHost _appHost; private readonly IApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly ILogger<UserManager> _logger; private readonly ILogger<UserManager> _logger;
private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders;
private IAuthenticationProvider[] _authenticationProviders = null!; private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders;
private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!; private readonly InvalidAuthProvider _invalidAuthProvider;
private InvalidAuthProvider _invalidAuthProvider = null!; private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
private IPasswordResetProvider[] _passwordResetProviders = null!; private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class. /// Initializes a new instance of the <see cref="UserManager"/> class.
@ -69,6 +68,13 @@ namespace Jellyfin.Server.Implementations.Users
_appHost = appHost; _appHost = appHost;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_logger = logger; _logger = logger;
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
_authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -102,7 +108,16 @@ namespace Jellyfin.Server.Implementations.Users
} }
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id); public IEnumerable<Guid> UsersIds
{
get
{
using var dbContext = _dbProvider.CreateContext();
return dbContext.Users
.Select(user => user.Id)
.ToList();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public User? GetUserById(Guid id) public User? GetUserById(Guid id)
@ -152,12 +167,12 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(newName)); throw new ArgumentException("Invalid username", nameof(newName));
} }
if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)) if (user.Username.Equals(newName, StringComparison.Ordinal))
{ {
throw new ArgumentException("The new and old names must be different."); throw new ArgumentException("The new and old names must be different.");
} }
if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))) if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.Ordinal)))
{ {
throw new ArgumentException(string.Format( throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@ -188,8 +203,24 @@ namespace Jellyfin.Server.Implementations.Users
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
{
// TODO: Remove after user item data is migrated.
var max = await dbContext.Users.AnyAsync().ConfigureAwait(false)
? await dbContext.Users.Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
: 0;
return new User(
name,
_defaultAuthenticationProvider.GetType().FullName,
_defaultPasswordResetProvider.GetType().FullName)
{
InternalId = max + 1
};
}
/// <inheritdoc/> /// <inheritdoc/>
public User CreateUser(string name) public async Task<User> CreateUserAsync(string name)
{ {
if (!IsValidUsername(name)) if (!IsValidUsername(name))
{ {
@ -198,18 +229,10 @@ namespace Jellyfin.Server.Implementations.Users
using var dbContext = _dbProvider.CreateContext(); using var dbContext = _dbProvider.CreateContext();
// TODO: Remove after user item data is migrated. var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
var newUser = new User(
name,
_defaultAuthenticationProvider.GetType().FullName,
_defaultPasswordResetProvider.GetType().FullName)
{
InternalId = max + 1
};
dbContext.Users.Add(newUser); dbContext.Users.Add(newUser);
dbContext.SaveChanges(); await dbContext.SaveChangesAsync().ConfigureAwait(false);
OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser)); OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
@ -512,7 +535,7 @@ namespace Jellyfin.Server.Implementations.Users
} }
else else
{ {
IncrementInvalidLoginAttemptCount(user); await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
_logger.LogInformation( _logger.LogInformation(
"Authentication request for {UserName} has been denied (IP: {IP}).", "Authentication request for {UserName} has been denied (IP: {IP}).",
user.Username, user.Username,
@ -530,7 +553,12 @@ namespace Jellyfin.Server.Implementations.Users
if (user != null && isInNetwork) if (user != null && isInNetwork)
{ {
var passwordResetProvider = GetPasswordResetProvider(user); var passwordResetProvider = GetPasswordResetProvider(user);
return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); var result = await passwordResetProvider
.StartForgotPasswordProcess(user, isInNetwork)
.ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
return result;
} }
return new ForgotPasswordResult return new ForgotPasswordResult
@ -560,24 +588,13 @@ namespace Jellyfin.Server.Implementations.Users
}; };
} }
/// <inheritdoc/>
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
{
_authenticationProviders = authenticationProviders.ToArray();
_passwordResetProviders = passwordResetProviders.ToArray();
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
}
/// <inheritdoc /> /// <inheritdoc />
public void Initialize() public async Task InitializeAsync()
{ {
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist. // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
using var dbContext = _dbProvider.CreateContext(); using var dbContext = _dbProvider.CreateContext();
if (dbContext.Users.Any()) if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
{ {
return; return;
} }
@ -595,13 +612,13 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Provided username is not valid!", defaultName); throw new ArgumentException("Provided username is not valid!", defaultName);
} }
var newUser = CreateUser(defaultName); var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true); newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
dbContext.Users.Update(newUser); dbContext.Users.Add(newUser);
dbContext.SaveChanges(); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -637,7 +654,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/> /// <inheritdoc/>
public void UpdateConfiguration(Guid userId, UserConfiguration config) public void UpdateConfiguration(Guid userId, UserConfiguration config)
{ {
var dbContext = _dbProvider.CreateContext(); using var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users var user = dbContext.Users
.Include(u => u.Permissions) .Include(u => u.Permissions)
.Include(u => u.Preferences) .Include(u => u.Preferences)
@ -670,8 +687,14 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/> /// <inheritdoc/>
public void UpdatePolicy(Guid userId, UserPolicy policy) public void UpdatePolicy(Guid userId, UserPolicy policy)
{ {
var dbContext = _dbProvider.CreateContext(); using var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!"); var user = dbContext.Users
.Include(u => u.Permissions)
.Include(u => u.Preferences)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id == userId)
?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
@ -876,7 +899,7 @@ namespace Jellyfin.Server.Implementations.Users
} }
} }
private void IncrementInvalidLoginAttemptCount(User user) private async Task IncrementInvalidLoginAttemptCount(User user)
{ {
user.InvalidLoginAttemptCount++; user.InvalidLoginAttemptCount++;
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout; int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
@ -890,7 +913,7 @@ namespace Jellyfin.Server.Implementations.Users
user.InvalidLoginAttemptCount); user.InvalidLoginAttemptCount);
} }
UpdateUser(user); await UpdateUserAsync(user).ConfigureAwait(false);
} }
} }
} }

@ -34,9 +34,9 @@ namespace Jellyfin.Server
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
public CoreAppHost( public CoreAppHost(
ServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
StartupOptions options, IStartupOptions options,
IFileSystem fileSystem, IFileSystem fileSystem,
INetworkManager networkManager) INetworkManager networkManager)
: base( : base(

@ -41,8 +41,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
<PackageReference Include="prometheus-net" Version="3.6.0" /> <PackageReference Include="prometheus-net" Version="3.6.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />

@ -17,6 +17,11 @@ namespace Jellyfin.Server.Migrations
/// </summary> /// </summary>
public string Name { get; } public string Name { get; }
/// <summary>
/// Gets a value indicating whether to perform migration on a new install.
/// </summary>
public bool PerformOnNewInstall { get; }
/// <summary> /// <summary>
/// Execute the migration routine. /// Execute the migration routine.
/// </summary> /// </summary>

@ -22,6 +22,7 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.RemoveDuplicateExtras), typeof(Routines.RemoveDuplicateExtras),
typeof(Routines.AddDefaultPluginRepository), typeof(Routines.AddDefaultPluginRepository),
typeof(Routines.MigrateUserDb), typeof(Routines.MigrateUserDb),
typeof(Routines.ReaddDefaultPluginRepository),
typeof(Routines.MigrateDisplayPreferencesDb) typeof(Routines.MigrateDisplayPreferencesDb)
}; };
@ -44,9 +45,8 @@ namespace Jellyfin.Server.Migrations
// If startup wizard is not finished, this is a fresh install. // If startup wizard is not finished, this is a fresh install.
// Don't run any migrations, just mark all of them as applied. // Don't run any migrations, just mark all of them as applied.
logger.LogInformation("Marking all known migrations as applied because this is a fresh install"); logger.LogInformation("Marking all known migrations as applied because this is a fresh install");
migrationOptions.Applied.AddRange(migrations.Select(m => (m.Id, m.Name))); migrationOptions.Applied.AddRange(migrations.Where(m => !m.PerformOnNewInstall).Select(m => (m.Id, m.Name)));
host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
return;
} }
var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();

@ -32,6 +32,9 @@ namespace Jellyfin.Server.Migrations.Routines
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "AddDefaultPluginRepository"; public string Name => "AddDefaultPluginRepository";
/// <inheritdoc/>
public bool PerformOnNewInstall => true;
/// <inheritdoc/> /// <inheritdoc/>
public void Perform() public void Perform()
{ {

@ -48,6 +48,9 @@ namespace Jellyfin.Server.Migrations.Routines
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "CreateLoggingConfigHeirarchy"; public string Name => "CreateLoggingConfigHeirarchy";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/> /// <inheritdoc/>
public void Perform() public void Perform()
{ {

@ -25,6 +25,9 @@ namespace Jellyfin.Server.Migrations.Routines
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "DisableTranscodingThrottling"; public string Name => "DisableTranscodingThrottling";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/> /// <inheritdoc/>
public void Perform() public void Perform()
{ {

@ -41,6 +41,9 @@ namespace Jellyfin.Server.Migrations.Routines
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "MigrateActivityLogDatabase"; public string Name => "MigrateActivityLogDatabase";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/> /// <inheritdoc/>
public void Perform() public void Perform()
{ {

@ -54,6 +54,9 @@ namespace Jellyfin.Server.Migrations.Routines
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "MigrateUserDatabase"; public string Name => "MigrateUserDatabase";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/> /// <inheritdoc/>
public void Perform() public void Perform()
{ {

@ -0,0 +1,49 @@
using System;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Updates;
namespace Jellyfin.Server.Migrations.Routines
{
/// <summary>
/// Migration to initialize system configuration with the default plugin repository.
/// </summary>
public class ReaddDefaultPluginRepository : IMigrationRoutine
{
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly RepositoryInfo _defaultRepositoryInfo = new RepositoryInfo
{
Name = "Jellyfin Stable",
Url = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json"
};
/// <summary>
/// Initializes a new instance of the <see cref="ReaddDefaultPluginRepository"/> class.
/// </summary>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public ReaddDefaultPluginRepository(IServerConfigurationManager serverConfigurationManager)
{
_serverConfigurationManager = serverConfigurationManager;
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("5F86E7F6-D966-4C77-849D-7A7B40B68C4E");
/// <inheritdoc/>
public string Name => "ReaddDefaultPluginRepository";
/// <inheritdoc/>
public bool PerformOnNewInstall => true;
/// <inheritdoc/>
public void Perform()
{
// Only add if repository list is empty
if (_serverConfigurationManager.Configuration.PluginRepositories.Count == 0)
{
_serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo);
_serverConfigurationManager.SaveConfiguration();
}
}
}
}

@ -29,6 +29,9 @@ namespace Jellyfin.Server.Migrations.Routines
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "RemoveDuplicateExtras"; public string Name => "RemoveDuplicateExtras";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/> /// <inheritdoc/>
public void Perform() public void Perform()
{ {

@ -343,6 +343,21 @@ namespace Jellyfin.Server
} }
} }
} }
// Bind to unix socket (only on OSX and Linux)
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// TODO: allow configuration of socket path
var socketPath = $"{appPaths.DataPath}/socket.sock";
// Workaround for https://github.com/aspnet/AspNetCore/issues/14134
if (File.Exists(socketPath))
{
File.Delete(socketPath);
}
options.ListenUnixSocket(socketPath);
_logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
}
}) })
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig)) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig))
.UseSerilog() .UseSerilog()

@ -895,6 +895,11 @@ namespace MediaBrowser.Api.Images
// Handle image/png; charset=utf-8 // Handle image/png; charset=utf-8
mimeType = mimeType.Split(';').FirstOrDefault(); mimeType = mimeType.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
_userManager.ClearProfileImage(user);
}
user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
await _providerManager await _providerManager

@ -56,7 +56,10 @@ namespace MediaBrowser.Api.System
DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>( var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
entries => entries.Where(entry => entry.DateCreated >= minDate)); entries => entries.Where(entry => entry.DateCreated >= minDate
&& (!request.HasUserId.HasValue || (request.HasUserId.Value
? entry.UserId != Guid.Empty
: entry.UserId == Guid.Empty))));
var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit); var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit);

@ -525,7 +525,7 @@ namespace MediaBrowser.Api
/// <returns>System.Object.</returns> /// <returns>System.Object.</returns>
public async Task<object> Post(CreateUserByName request) public async Task<object> Post(CreateUserByName request)
{ {
var newUser = _userManager.CreateUser(request.Name); var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
// no need to authenticate password for new user // no need to authenticate password for new user
if (request.Password != null) if (request.Password != null)

@ -0,0 +1,33 @@
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Common.Extensions
{
/// <summary>
/// Static class containing extension methods for <see cref="HttpContext"/>.
/// </summary>
public static class HttpContextExtensions
{
private const string ServiceStackRequest = "ServiceStackRequest";
/// <summary>
/// Set the ServiceStack request.
/// </summary>
/// <param name="httpContext">The HttpContext instance.</param>
/// <param name="request">The service stack request instance.</param>
public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request)
{
httpContext.Items[ServiceStackRequest] = request;
}
/// <summary>
/// Get the ServiceStack request.
/// </summary>
/// <param name="httpContext">The HttpContext instance.</param>
/// <returns>The service stack request instance.</returns>
public static IRequest GetServiceStackRequest(this HttpContext httpContext)
{
return (IRequest)httpContext.Items[ServiceStackRequest];
}
}
}

@ -17,8 +17,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup> </ItemGroup>

@ -200,7 +200,7 @@ namespace MediaBrowser.Controller.Library
/// <summary> /// <summary>
/// Updates the item. /// Updates the item.
/// </summary> /// </summary>
void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
@ -55,7 +54,7 @@ namespace MediaBrowser.Controller.Library
/// <summary> /// <summary>
/// Initializes the user manager and ensures that a user exists. /// Initializes the user manager and ensures that a user exists.
/// </summary> /// </summary>
void Initialize(); Task InitializeAsync();
/// <summary> /// <summary>
/// Gets a user by Id. /// Gets a user by Id.
@ -106,7 +105,7 @@ namespace MediaBrowser.Controller.Library
/// <returns>The created user.</returns> /// <returns>The created user.</returns>
/// <exception cref="ArgumentNullException">name</exception> /// <exception cref="ArgumentNullException">name</exception>
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
User CreateUser(string name); Task<User> CreateUserAsync(string name);
/// <summary> /// <summary>
/// Deletes the specified user. /// Deletes the specified user.
@ -166,8 +165,6 @@ namespace MediaBrowser.Controller.Library
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
Task<PinRedeemResult> RedeemPasswordResetPin(string pin); Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
NameIdPair[] GetAuthenticationProviders(); NameIdPair[] GetAuthenticationProviders();
NameIdPair[] GetPasswordResetProviders(); NameIdPair[] GetPasswordResetProviders();

@ -13,8 +13,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -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.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@ -371,7 +372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public int GetVideoProfileScore(string profile) public int GetVideoProfileScore(string profile)
{ {
// strip spaces because they may be stripped out on the query string // strip spaces because they may be stripped out on the query string
profile = profile.Replace(" ", ""); profile = profile.Replace(" ", string.Empty, StringComparison.Ordinal);
return Array.FindIndex(_videoProfiles, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); return Array.FindIndex(_videoProfiles, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
} }
@ -449,41 +450,59 @@ namespace MediaBrowser.Controller.MediaEncoding
var arg = new StringBuilder(); var arg = new StringBuilder();
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty; var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
bool isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
bool isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
bool isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
bool isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (state.IsVideoRequest if (!IsCopyCodec(outputVideoCodec))
&& string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
{ {
if (isVaapiDecoder) if (state.IsVideoRequest
&& _mediaEncoder.SupportsHwaccel("vaapi")
&& string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
{ {
arg.Append("-hwaccel_output_format vaapi ") if (isVaapiDecoder)
.Append("-vaapi_device ") {
.Append(encodingOptions.VaapiDevice) arg.Append("-hwaccel_output_format vaapi ")
.Append(" "); .Append("-vaapi_device ")
} .Append(encodingOptions.VaapiDevice)
else if (!isVaapiDecoder && isVaapiEncoder) .Append(' ');
{ }
arg.Append("-vaapi_device ") else if (!isVaapiDecoder && isVaapiEncoder)
.Append(encodingOptions.VaapiDevice) {
.Append(" "); arg.Append("-vaapi_device ")
.Append(encodingOptions.VaapiDevice)
.Append(' ');
}
} }
}
if (state.IsVideoRequest
&& string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{
var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
if (!hasTextSubs) if (state.IsVideoRequest
&& string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{ {
if (isQsvEncoder) var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
if (isQsvEncoder)
{ {
if (isQsvDecoder) if (isQsvDecoder)
{ {
arg.Append("-hwaccel qsv -init_hw_device qsv=hw "); if (isLinux)
{
if (hasGraphicalSubs)
{
arg.Append("-init_hw_device qsv=hw -filter_hw_device hw ");
}
else
{
arg.Append("-hwaccel qsv ");
}
}
if (isWindows)
{
arg.Append("-hwaccel qsv ");
}
} }
// While using SW decoder // While using SW decoder
else else
@ -806,6 +825,34 @@ namespace MediaBrowser.Controller.MediaEncoding
break; break;
} }
} }
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
switch (encodingOptions.EncoderPreset)
{
case "veryslow":
case "slow":
case "slower":
param += "-quality quality";
break;
case "medium":
param += "-quality balanced";
break;
case "fast":
case "faster":
case "veryfast":
case "superfast":
case "ultrafast":
param += "-quality speed";
break;
default:
param += "-quality speed";
break;
}
}
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
{ {
// Values 0-3, 0 being highest quality but slower // Values 0-3, 0 being highest quality but slower
@ -1351,7 +1398,7 @@ namespace MediaBrowser.Controller.MediaEncoding
transcoderChannelLimit = 6; transcoderChannelLimit = 6;
} }
var isTranscodingAudio = !EncodingHelper.IsCopyCodec(codec); var isTranscodingAudio = !IsCopyCodec(codec);
int? resultChannels = state.GetRequestedAudioChannels(codec); int? resultChannels = state.GetRequestedAudioChannels(codec);
if (isTranscodingAudio) if (isTranscodingAudio)
@ -1555,28 +1602,44 @@ namespace MediaBrowser.Controller.MediaEncoding
var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase); var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
if (index != -1) if (index != -1)
{ {
outputSizeParam = "," + outputSizeParam.Substring(index); outputSizeParam = outputSizeParam.Substring(index);
} }
else else
{ {
index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
if (index != -1) if (index != -1)
{ {
outputSizeParam = "," + outputSizeParam.Substring(index); outputSizeParam = outputSizeParam.Substring(index);
} }
else else
{ {
index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
if (index != -1) if (index != -1)
{ {
outputSizeParam = "," + outputSizeParam.Substring(index); outputSizeParam = outputSizeParam.Substring(index);
} }
else else
{ {
index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
if (index != -1) if (index != -1)
{ {
outputSizeParam = "," + outputSizeParam.Substring(index); outputSizeParam = outputSizeParam.Substring(index);
}
else
{
index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
outputSizeParam = outputSizeParam.Substring(index);
}
else
{
index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
outputSizeParam = outputSizeParam.Substring(index);
}
}
} }
} }
} }
@ -1585,43 +1648,30 @@ namespace MediaBrowser.Controller.MediaEncoding
var videoSizeParam = string.Empty; var videoSizeParam = string.Empty;
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty; var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
// Setup subtitle scaling // Setup subtitle scaling
if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue) if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
{ {
videoSizeParam = string.Format( // Adjust the size of graphical subtitles to fit the video stream.
CultureInfo.InvariantCulture, var videoStream = state.VideoStream;
"scale={0}:{1}", var inputWidth = videoStream?.Width;
state.VideoStream.Width.Value, var inputHeight = videoStream?.Height;
state.VideoStream.Height.Value); var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
// For QSV, feed it into hardware encoder now
if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{
videoSizeParam += ",hwupload=extra_hw_frames=64";
}
// For VAAPI and CUVID decoder if (width.HasValue && height.HasValue)
// these encoders cannot automatically adjust the size of graphical subtitles to fit the output video,
// thus needs to be manually adjusted.
if (videoDecoder.IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
|| (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
&& (videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
|| outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1)))
{ {
var videoStream = state.VideoStream; videoSizeParam = string.Format(
var inputWidth = videoStream?.Width;
var inputHeight = videoStream?.Height;
var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
if (width.HasValue && height.HasValue)
{
videoSizeParam = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"scale={0}:{1}", "scale={0}x{1}",
width.Value, width.Value,
height.Value); height.Value);
} }
// For QSV, feed it into hardware encoder now
if (isLinux && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{
videoSizeParam += ",hwupload=extra_hw_frames=64";
} }
} }
@ -1634,7 +1684,10 @@ namespace MediaBrowser.Controller.MediaEncoding
: state.SubtitleStream.Index; : state.SubtitleStream.Index;
// Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference) // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference)
var retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\""; // Always put the scaler before the overlay for better performance
var retStr = !string.IsNullOrEmpty(outputSizeParam) ?
" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"" :
" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
// When the input may or may not be hardware VAAPI decodable // When the input may or may not be hardware VAAPI decodable
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
@ -1644,12 +1697,11 @@ namespace MediaBrowser.Controller.MediaEncoding
[sub]: SW scaling subtitle to FixedOutputSize [sub]: SW scaling subtitle to FixedOutputSize
[base][sub]: SW overlay [base][sub]: SW overlay
*/ */
outputSizeParam = outputSizeParam.TrimStart(',');
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""; retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"";
} }
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
else if (IsVaapiSupported(state) && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1 else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
&& string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
{ {
/* /*
@ -1657,7 +1709,6 @@ namespace MediaBrowser.Controller.MediaEncoding
[sub]: SW scaling subtitle to FixedOutputSize [sub]: SW scaling subtitle to FixedOutputSize
[base][sub]: SW overlay [base][sub]: SW overlay
*/ */
outputSizeParam = outputSizeParam.TrimStart(',');
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""; retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
} }
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
@ -1666,14 +1717,13 @@ namespace MediaBrowser.Controller.MediaEncoding
QSV in FFMpeg can now setup hardware overlay for transcodes. QSV in FFMpeg can now setup hardware overlay for transcodes.
For software decoding and hardware encoding option, frames must be hwuploaded into hardware For software decoding and hardware encoding option, frames must be hwuploaded into hardware
with fixed frame size. with fixed frame size.
Currently only supports linux.
*/ */
if (videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1) if (isLinux)
{
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\"";
}
else
{ {
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwupload=extra_hw_frames=64[v];[v][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; retStr = !string.IsNullOrEmpty(outputSizeParam) ?
" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" :
" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
} }
} }
@ -1745,10 +1795,8 @@ namespace MediaBrowser.Controller.MediaEncoding
requestedMaxWidth, requestedMaxWidth,
requestedMaxHeight); requestedMaxHeight);
var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|| (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !hasTextSubs)) || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
&& width.HasValue && width.HasValue
&& height.HasValue) && height.HasValue)
{ {
@ -1758,6 +1806,10 @@ namespace MediaBrowser.Controller.MediaEncoding
var outputWidth = width.Value; var outputWidth = width.Value;
var outputHeight = height.Value; var outputHeight = height.Value;
var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase); var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
var isDeintEnabled = state.DeInterlace("h264", true)
|| state.DeInterlace("avc", true)
|| state.DeInterlace("h265", true)
|| state.DeInterlace("hevc", true);
if (!videoWidth.HasValue if (!videoWidth.HasValue
|| outputWidth != videoWidth.Value || outputWidth != videoWidth.Value
@ -1769,15 +1821,20 @@ namespace MediaBrowser.Controller.MediaEncoding
filters.Add( filters.Add(
string.Format( string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"{0}=w={1}:h={2}:format=nv12", "{0}=w={1}:h={2}:format=nv12{3}",
qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi", qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
outputWidth, outputWidth,
outputHeight)); outputHeight,
(qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
} }
else else
{ {
// set w=0:h=0 for vpp_qsv to keep the original dimensions, otherwise it will fail. filters.Add(
filters.Add(string.Format(CultureInfo.InvariantCulture, "{0}format=nv12", qsv_or_vaapi ? "vpp_qsv=w=0:h=0:" : "scale_vaapi=")); string.Format(
CultureInfo.InvariantCulture,
"{0}=format=nv12{1}",
qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
(qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
} }
} }
else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
@ -1989,7 +2046,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
var request = state.BaseRequest; var request = state.BaseRequest;
var videoStream = state.VideoStream; var videoStream = state.VideoStream;
var filters = new List<string>(); var filters = new List<string>();
@ -1998,32 +2054,34 @@ namespace MediaBrowser.Controller.MediaEncoding
var inputHeight = videoStream?.Height; var inputHeight = videoStream?.Height;
var threeDFormat = state.MediaSource.Video3DFormat; var threeDFormat = state.MediaSource.Video3DFormat;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
// When the input may or may not be hardware VAAPI decodable // When the input may or may not be hardware VAAPI decodable
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) if (isVaapiH264Encoder)
{ {
filters.Add("format=nv12|vaapi"); filters.Add("format=nv12|vaapi");
filters.Add("hwupload"); filters.Add("hwupload");
} }
// When the input may or may not be hardware QSV decodable // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
{ {
if (!hasTextSubs) filters.Add("hwupload=extra_hw_frames=64");
{
filters.Add("format=nv12|qsv");
filters.Add("hwupload=extra_hw_frames=64");
}
} }
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
else if (videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1 else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
&& string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
{ {
var codec = videoStream.Codec.ToLowerInvariant(); var codec = videoStream.Codec.ToLowerInvariant();
var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) var isColorDepth10 = IsColorDepth10(state);
|| videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
// Assert 10-bit hardware VAAPI decodable // Assert 10-bit hardware VAAPI decodable
if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
@ -2048,49 +2106,49 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
// Add hardware deinterlace filter before scaling filter // Add hardware deinterlace filter before scaling filter
if (state.DeInterlace("h264", true)) if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true))
{ {
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) if (isVaapiH264Encoder)
{ {
filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi")); filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi"));
} }
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{
if (!hasTextSubs)
{
filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_qsv"));
}
}
} }
// Add software deinterlace filter before scaling filter // Add software deinterlace filter before scaling filter
if (((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)) if (state.DeInterlace("h264", true)
&& !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || state.DeInterlace("avc", true)
&& !string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) || state.DeInterlace("h265", true)
|| (hasTextSubs && state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))) || state.DeInterlace("hevc", true))
{ {
var deintParam = string.Empty;
var inputFramerate = videoStream?.RealFrameRate; var inputFramerate = videoStream?.RealFrameRate;
// If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle
if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30) if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30)
{ {
filters.Add("yadif=1:-1:0"); deintParam = "yadif=1:-1:0";
} }
else else
{ {
filters.Add("yadif=0:-1:0"); deintParam = "yadif=0:-1:0";
}
if (!string.IsNullOrEmpty(deintParam))
{
if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder)
{
filters.Add(deintParam);
}
} }
} }
// Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr // Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr
filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight)); filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
// Add parameters to use VAAPI with burn-in text subttiles (GH issue #642) // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) if (isVaapiH264Encoder)
{ {
if (state.SubtitleStream != null if (hasTextSubs)
&& state.SubtitleStream.IsTextSubtitleStream
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
{ {
// Test passed on Intel and AMD gfx // Test passed on Intel and AMD gfx
filters.Add("hwmap=mode=read+write"); filters.Add("hwmap=mode=read+write");
@ -2100,9 +2158,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var output = string.Empty; var output = string.Empty;
if (state.SubtitleStream != null if (hasTextSubs)
&& state.SubtitleStream.IsTextSubtitleStream
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
{ {
var subParam = GetTextSubtitleParam(state); var subParam = GetTextSubtitleParam(state);
@ -2110,7 +2166,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API // Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API
// Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI // Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) if (isVaapiH264Encoder)
{ {
filters.Add("hwmap"); filters.Add("hwmap");
} }
@ -2264,7 +2320,7 @@ namespace MediaBrowser.Controller.MediaEncoding
flags.Add("+ignidx"); flags.Add("+ignidx");
} }
if (state.GenPtsInput || EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) if (state.GenPtsInput || IsCopyCodec(state.OutputVideoCodec))
{ {
flags.Add("+genpts"); flags.Add("+genpts");
} }
@ -2290,7 +2346,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
inputModifier += " " + videoDecoder; inputModifier += " " + videoDecoder;
if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) if (!IsCopyCodec(state.OutputVideoCodec)
&& (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1)
{ {
var videoStream = state.VideoStream; var videoStream = state.VideoStream;
var inputWidth = videoStream?.Width; var inputWidth = videoStream?.Width;
@ -2303,11 +2360,19 @@ namespace MediaBrowser.Controller.MediaEncoding
&& width.HasValue && width.HasValue
&& height.HasValue) && height.HasValue)
{ {
inputModifier += string.Format( if (width.HasValue && height.HasValue)
CultureInfo.InvariantCulture, {
" -resize {0}x{1}", inputModifier += string.Format(
width.Value, CultureInfo.InvariantCulture,
height.Value); " -resize {0}x{1}",
width.Value,
height.Value);
}
if (state.DeInterlace("h264", true))
{
inputModifier += " -deint 1";
}
} }
} }
} }
@ -2523,21 +2588,21 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
/// <summary> /// <summary>
/// Gets the name of the output video codec. /// Gets the ffmpeg option string for the hardware accelerated video decoder.
/// </summary> /// </summary>
/// <param name="state">The encoding job info.</param>
/// <param name="encodingOptions">The encoding options.</param>
/// <returns>The option string or null if none available.</returns>
protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions) protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
{ {
var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
var videoStream = state.VideoStream; var videoStream = state.VideoStream;
var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
|| videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) if (videoStream == null)
{ {
return null; return null;
} }
var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
// Only use alternative encoders for video files. // Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
@ -2546,10 +2611,15 @@ namespace MediaBrowser.Controller.MediaEncoding
return null; return null;
} }
if (videoStream != null if (IsCopyCodec(state.OutputVideoCodec))
&& !string.IsNullOrEmpty(videoStream.Codec)
&& !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
{ {
return null;
}
if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
{
var isColorDepth10 = IsColorDepth10(state);
// Only hevc and vp9 formats have 10-bit hardware decoder support now. // Only hevc and vp9 formats have 10-bit hardware decoder support now.
if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
@ -2625,13 +2695,6 @@ namespace MediaBrowser.Controller.MediaEncoding
case "h264": case "h264":
if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
{ {
// cuvid decoder does not support 10-bit input.
if ((videoStream.BitDepth ?? 8) > 8)
{
encodingOptions.HardwareDecodingCodecs = Array.Empty<string>();
return null;
}
return "-c:v h264_cuvid"; return "-c:v h264_cuvid";
} }
@ -2898,21 +2961,24 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary> /// </summary>
public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec) public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec)
{ {
var isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT; var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1); var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1);
var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va"); var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va");
if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase)) if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
{ {
if (!isWindows) if (isLinux)
{ {
return "-hwaccel vaapi"; return "-hwaccel vaapi";
} }
else if (isWindows8orLater)
if (isWindows && isWindows8orLater)
{ {
return "-hwaccel d3d11va"; return "-hwaccel d3d11va";
} }
else
if (isWindows && !isWindows8orLater)
{ {
return "-hwaccel dxva2"; return "-hwaccel dxva2";
} }
@ -3002,7 +3068,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += " -mpegts_m2ts_mode 1"; args += " -mpegts_m2ts_mode 1";
} }
if (EncodingHelper.IsCopyCodec(videoCodec)) if (IsCopyCodec(videoCodec))
{ {
if (state.VideoStream != null if (state.VideoStream != null
&& string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
@ -3104,7 +3170,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var args = "-codec:a:0 " + codec; var args = "-codec:a:0 " + codec;
if (EncodingHelper.IsCopyCodec(codec)) if (IsCopyCodec(codec))
{ {
return args; return args;
} }
@ -3181,5 +3247,42 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);
} }
public static bool IsColorDepth10(EncodingJobInfo state)
{
var result = false;
var videoStream = state.VideoStream;
if (videoStream != null)
{
if (!string.IsNullOrEmpty(videoStream.PixelFormat))
{
result = videoStream.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase);
if (result)
{
return true;
}
}
if (!string.IsNullOrEmpty(videoStream.Profile))
{
result = videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
|| videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)
|| videoStream.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase);
if (result)
{
return true;
}
}
result = (videoStream.BitDepth ?? 8) == 10;
if (result)
{
return true;
}
}
return result;
}
} }
} }

@ -198,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
internal static Version GetFFmpegVersion(string output) internal static Version GetFFmpegVersion(string output)
{ {
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
var match = Regex.Match(output, @"^ffmpeg version n?((?:\d+\.?)+)"); var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
if (match.Success) if (match.Success)
{ {
@ -225,7 +225,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var rc = new StringBuilder(144); var rc = new StringBuilder(144);
foreach (Match m in Regex.Matches( foreach (Match m in Regex.Matches(
output, output,
@"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))", @"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))",
RegexOptions.Multiline)) RegexOptions.Multiline))
{ {
rc.Append(m.Groups["name"]) rc.Append(m.Groups["name"])

@ -172,7 +172,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
inputFiles = new[] { mediaSource.Path }; inputFiles = new[] { mediaSource.Path };
} }
var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false); var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), subtitleStream, cancellationToken).ConfigureAwait(false);
var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);

@ -23,7 +23,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" />
<PackageReference Include="System.Globalization" Version="4.3.0" /> <PackageReference Include="System.Globalization" Version="4.3.0" />
<PackageReference Include="System.Text.Json" Version="4.7.2" /> <PackageReference Include="System.Text.Json" Version="4.7.2" />
</ItemGroup> </ItemGroup>

@ -1,3 +1,4 @@
#pragma warning disable CA1819 // Properties should not return arrays
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -9,21 +10,27 @@ namespace MediaBrowser.Model.Notifications
public NotificationOption(string type) public NotificationOption(string type)
{ {
Type = type; Type = type;
DisabledServices = Array.Empty<string>();
DisabledMonitorUsers = Array.Empty<string>();
SendToUsers = Array.Empty<string>();
}
public NotificationOption()
{
DisabledServices = Array.Empty<string>(); DisabledServices = Array.Empty<string>();
DisabledMonitorUsers = Array.Empty<string>(); DisabledMonitorUsers = Array.Empty<string>();
SendToUsers = Array.Empty<string>(); SendToUsers = Array.Empty<string>();
} }
public string Type { get; set; } public string? Type { get; set; }
/// <summary> /// <summary>
/// User Ids to not monitor (it's opt out). /// Gets or sets user Ids to not monitor (it's opt out).
/// </summary> /// </summary>
public string[] DisabledMonitorUsers { get; set; } public string[] DisabledMonitorUsers { get; set; }
/// <summary> /// <summary>
/// User Ids to send to (if SendToUserMode == Custom) /// Gets or sets user Ids to send to (if SendToUserMode == Custom).
/// </summary> /// </summary>
public string[] SendToUsers { get; set; } public string[] SendToUsers { get; set; }

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -25,7 +23,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Priority_Queue; using Priority_Queue;
using Book = MediaBrowser.Controller.Entities.Book; using Book = MediaBrowser.Controller.Entities.Book;
@ -42,33 +39,38 @@ namespace MediaBrowser.Providers.Manager
/// </summary> /// </summary>
public class ProviderManager : IProviderManager, IDisposable public class ProviderManager : IProviderManager, IDisposable
{ {
private readonly object _refreshQueueLock = new object();
private readonly ILogger<ProviderManager> _logger; private readonly ILogger<ProviderManager> _logger;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ILibraryMonitor _libraryMonitor; private readonly ILibraryMonitor _libraryMonitor;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths; private readonly IServerApplicationPaths _appPaths;
private readonly IJsonSerializer _json;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subtitleManager; private readonly ISubtitleManager _subtitleManager;
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;
private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>();
private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue =
new SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>>();
private IImageProvider[] ImageProviders { get; set; } private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>();
private IMetadataProvider[] _metadataProviders = Array.Empty<IMetadataProvider>();
private IMetadataService[] _metadataServices = { };
private IMetadataProvider[] _metadataProviders = { };
private IEnumerable<IMetadataSaver> _savers; private IEnumerable<IMetadataSaver> _savers;
private IExternalId[] _externalIds; private IExternalId[] _externalIds;
private bool _isProcessingRefreshQueue;
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); private bool _disposed;
public event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
public event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ProviderManager" /> class. /// Initializes a new instance of the <see cref="ProviderManager"/> class.
/// </summary> /// </summary>
/// <param name="httpClient">The Http client.</param>
/// <param name="subtitleManager">The subtitle manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="libraryMonitor">The library monitor.</param>
/// <param name="logger">The logger.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="appPaths">The server application paths.</param>
/// <param name="libraryManager">The library manager.</param>
public ProviderManager( public ProviderManager(
IHttpClient httpClient, IHttpClient httpClient,
ISubtitleManager subtitleManager, ISubtitleManager subtitleManager,
@ -77,8 +79,7 @@ namespace MediaBrowser.Providers.Manager
ILogger<ProviderManager> logger, ILogger<ProviderManager> logger,
IFileSystem fileSystem, IFileSystem fileSystem,
IServerApplicationPaths appPaths, IServerApplicationPaths appPaths,
ILibraryManager libraryManager, ILibraryManager libraryManager)
IJsonSerializer json)
{ {
_logger = logger; _logger = logger;
_httpClient = httpClient; _httpClient = httpClient;
@ -87,16 +88,27 @@ namespace MediaBrowser.Providers.Manager
_fileSystem = fileSystem; _fileSystem = fileSystem;
_appPaths = appPaths; _appPaths = appPaths;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_json = json;
_subtitleManager = subtitleManager; _subtitleManager = subtitleManager;
} }
/// <summary> /// <inheritdoc/>
/// Adds the metadata providers. public event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
/// </summary>
public void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, /// <inheritdoc/>
IEnumerable<IMetadataProvider> metadataProviders, IEnumerable<IMetadataSaver> metadataSavers, public event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
IEnumerable<IExternalId> externalIds)
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
private IImageProvider[] ImageProviders { get; set; }
/// <inheritdoc/>
public void AddParts(
IEnumerable<IImageProvider> imageProviders,
IEnumerable<IMetadataService> metadataServices,
IEnumerable<IMetadataProvider> metadataProviders,
IEnumerable<IMetadataSaver> metadataSavers,
IEnumerable<IExternalId> externalIds)
{ {
ImageProviders = imageProviders.ToArray(); ImageProviders = imageProviders.ToArray();
@ -104,27 +116,17 @@ namespace MediaBrowser.Providers.Manager
_metadataProviders = metadataProviders.ToArray(); _metadataProviders = metadataProviders.ToArray();
_externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray(); _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray();
_savers = metadataSavers.Where(i => _savers = metadataSavers
{ .Where(i => !(i is IConfigurableProvider configurable) || configurable.IsEnabled)
var configurable = i as IConfigurableProvider; .ToArray();
return configurable == null || configurable.IsEnabled;
}).ToArray();
} }
/// <inheritdoc/>
public Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken) public Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
IMetadataService service = null;
var type = item.GetType(); var type = item.GetType();
foreach (var current in _metadataServices) var service = _metadataServices.FirstOrDefault(current => current.CanRefreshPrimary(type));
{
if (current.CanRefreshPrimary(type))
{
service = current;
break;
}
}
if (service == null) if (service == null)
{ {
@ -147,35 +149,36 @@ namespace MediaBrowser.Providers.Manager
return Task.FromResult(ItemUpdateType.None); return Task.FromResult(ItemUpdateType.None);
} }
/// <inheritdoc/>
public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken) public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken)
{ {
using (var response = await _httpClient.GetResponse(new HttpRequestOptions using var response = await _httpClient.GetResponse(new HttpRequestOptions
{ {
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
Url = url, Url = url,
BufferContent = false BufferContent = false
}).ConfigureAwait(false);
}).ConfigureAwait(false)) // Workaround for tvheadend channel icons
// TODO: Isolate this hack into the tvh plugin
if (string.IsNullOrEmpty(response.ContentType))
{ {
// Workaround for tvheadend channel icons if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1)
// TODO: Isolate this hack into the tvh plugin
if (string.IsNullOrEmpty(response.ContentType))
{ {
if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1) response.ContentType = "image/png";
{
response.ContentType = "image/png";
}
} }
await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken).ConfigureAwait(false);
} }
await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken).ConfigureAwait(false);
} }
/// <inheritdoc/>
public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken) public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
{ {
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken); return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken);
} }
/// <inheritdoc/>
public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken) public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
{ {
if (string.IsNullOrWhiteSpace(source)) if (string.IsNullOrWhiteSpace(source))
@ -188,12 +191,14 @@ namespace MediaBrowser.Providers.Manager
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken); return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
} }
/// <inheritdoc/>
public Task SaveImage(User user, Stream source, string mimeType, string path) public Task SaveImage(User user, Stream source, string mimeType, string path)
{ {
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger) return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
.SaveImage(user, source, path); .SaveImage(user, source, path);
} }
/// <inheritdoc/>
public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken) public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken)
{ {
var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders); var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders);
@ -213,7 +218,7 @@ namespace MediaBrowser.Providers.Manager
languages.Add(preferredLanguage); languages.Add(preferredLanguage);
} }
var tasks = providers.Select(i => GetImages(item, cancellationToken, i, languages, query.ImageType)); var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType));
var results = await Task.WhenAll(tasks).ConfigureAwait(false); var results = await Task.WhenAll(tasks).ConfigureAwait(false);
@ -224,12 +229,17 @@ namespace MediaBrowser.Providers.Manager
/// Gets the images. /// Gets the images.
/// </summary> /// </summary>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="provider">The provider.</param> /// <param name="provider">The provider.</param>
/// <param name="preferredLanguages">The preferred languages.</param> /// <param name="preferredLanguages">The preferred languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns> /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
private async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken, IRemoteImageProvider provider, List<string> preferredLanguages, ImageType? type = null) private async Task<IEnumerable<RemoteImageInfo>> GetImages(
BaseItem item,
IRemoteImageProvider provider,
IReadOnlyCollection<string> preferredLanguages,
CancellationToken cancellationToken,
ImageType? type = null)
{ {
try try
{ {
@ -255,21 +265,23 @@ namespace MediaBrowser.Providers.Manager
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "{0} failed in GetImageInfos for type {1}", provider.GetType().Name, item.GetType().Name); _logger.LogError(ex, "{ProviderName} failed in GetImageInfos for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
return new List<RemoteImageInfo>(); return new List<RemoteImageInfo>();
} }
} }
/// <summary> /// <inheritdoc/>
/// Gets the supported image providers.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>IEnumerable{IImageProvider}.</returns>
public IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item) public IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item)
{ {
return GetRemoteImageProviders(item, true).Select(i => new ImageProviderInfo(i.Name, i.GetSupportedImages(item).ToArray())); return GetRemoteImageProviders(item, true).Select(i => new ImageProviderInfo(i.Name, i.GetSupportedImages(item).ToArray()));
} }
/// <summary>
/// Gets the image providers for the provided item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="refreshOptions">The image refresh options.</param>
/// <returns>The image providers for the item.</returns>
public IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions) public IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions)
{ {
return GetImageProviders(item, _libraryManager.GetLibraryOptions(item), GetMetadataOptions(item), refreshOptions, false); return GetImageProviders(item, _libraryManager.GetLibraryOptions(item), GetMetadataOptions(item), refreshOptions, false);
@ -283,7 +295,7 @@ namespace MediaBrowser.Providers.Manager
var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name); var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
var typeFetcherOrder = typeOptions?.ImageFetcherOrder; var typeFetcherOrder = typeOptions?.ImageFetcherOrder;
return ImageProviders.Where(i => CanRefresh(i, item, libraryOptions, options, refreshOptions, includeDisabled)) return ImageProviders.Where(i => CanRefresh(i, item, libraryOptions, refreshOptions, includeDisabled))
.OrderBy(i => .OrderBy(i =>
{ {
// See if there's a user-defined order // See if there's a user-defined order
@ -304,6 +316,13 @@ namespace MediaBrowser.Providers.Manager
.ThenBy(GetOrder); .ThenBy(GetOrder);
} }
/// <summary>
/// Gets the metadata providers for the provided item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="libraryOptions">The library options.</param>
/// <typeparam name="T">The type of metadata provider.</typeparam>
/// <returns>The metadata providers.</returns>
public IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions) public IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
where T : BaseItem where T : BaseItem
{ {
@ -319,7 +338,7 @@ namespace MediaBrowser.Providers.Manager
var currentOptions = globalMetadataOptions; var currentOptions = globalMetadataOptions;
return _metadataProviders.OfType<IMetadataProvider<T>>() return _metadataProviders.OfType<IMetadataProvider<T>>()
.Where(i => CanRefresh(i, item, libraryOptions, currentOptions, includeDisabled, forceEnableInternetMetadata)) .Where(i => CanRefresh(i, item, libraryOptions, includeDisabled, forceEnableInternetMetadata))
.OrderBy(i => GetConfiguredOrder(item, i, libraryOptions, globalMetadataOptions)) .OrderBy(i => GetConfiguredOrder(item, i, libraryOptions, globalMetadataOptions))
.ThenBy(GetDefaultOrder); .ThenBy(GetDefaultOrder);
} }
@ -329,14 +348,20 @@ namespace MediaBrowser.Providers.Manager
var options = GetMetadataOptions(item); var options = GetMetadataOptions(item);
var libraryOptions = _libraryManager.GetLibraryOptions(item); var libraryOptions = _libraryManager.GetLibraryOptions(item);
return GetImageProviders(item, libraryOptions, options, return GetImageProviders(
new ImageRefreshOptions( item,
new DirectoryService(_fileSystem)), libraryOptions,
includeDisabled) options,
.OfType<IRemoteImageProvider>(); new ImageRefreshOptions(new DirectoryService(_fileSystem)),
includeDisabled).OfType<IRemoteImageProvider>();
} }
private bool CanRefresh(IMetadataProvider provider, BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, bool includeDisabled, bool forceEnableInternetMetadata) private bool CanRefresh(
IMetadataProvider provider,
BaseItem item,
LibraryOptions libraryOptions,
bool includeDisabled,
bool forceEnableInternetMetadata)
{ {
if (!includeDisabled) if (!includeDisabled)
{ {
@ -372,7 +397,12 @@ namespace MediaBrowser.Providers.Manager
return true; return true;
} }
private bool CanRefresh(IImageProvider provider, BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, ImageRefreshOptions refreshOptions, bool includeDisabled) private bool CanRefresh(
IImageProvider provider,
BaseItem item,
LibraryOptions libraryOptions,
ImageRefreshOptions refreshOptions,
bool includeDisabled)
{ {
if (!includeDisabled) if (!includeDisabled)
{ {
@ -400,7 +430,7 @@ namespace MediaBrowser.Providers.Manager
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "{0} failed in Supports for type {1}", provider.GetType().Name, item.GetType().Name); _logger.LogError(ex, "{ProviderName} failed in Supports for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
return false; return false;
} }
} }
@ -412,9 +442,7 @@ namespace MediaBrowser.Providers.Manager
/// <returns>System.Int32.</returns> /// <returns>System.Int32.</returns>
private int GetOrder(IImageProvider provider) private int GetOrder(IImageProvider provider)
{ {
var hasOrder = provider as IHasOrder; if (!(provider is IHasOrder hasOrder))
if (hasOrder == null)
{ {
return 0; return 0;
} }
@ -441,7 +469,7 @@ namespace MediaBrowser.Providers.Manager
if (provider is IRemoteMetadataProvider) if (provider is IRemoteMetadataProvider)
{ {
var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name); var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
var typeFetcherOrder = typeOptions == null ? null : typeOptions.MetadataFetcherOrder; var typeFetcherOrder = typeOptions?.MetadataFetcherOrder;
var fetcherOrder = typeFetcherOrder ?? globalMetadataOptions.MetadataFetcherOrder; var fetcherOrder = typeFetcherOrder ?? globalMetadataOptions.MetadataFetcherOrder;
@ -459,9 +487,7 @@ namespace MediaBrowser.Providers.Manager
private int GetDefaultOrder(IMetadataProvider provider) private int GetDefaultOrder(IMetadataProvider provider)
{ {
var hasOrder = provider as IHasOrder; if (provider is IHasOrder hasOrder)
if (hasOrder != null)
{ {
return hasOrder.Order; return hasOrder.Order;
} }
@ -469,9 +495,10 @@ namespace MediaBrowser.Providers.Manager
return 0; return 0;
} }
/// <inheritdoc/>
public MetadataPluginSummary[] GetAllMetadataPlugins() public MetadataPluginSummary[] GetAllMetadataPlugins()
{ {
return new MetadataPluginSummary[] return new[]
{ {
GetPluginSummary<Movie>(), GetPluginSummary<Movie>(),
GetPluginSummary<BoxSet>(), GetPluginSummary<BoxSet>(),
@ -493,7 +520,7 @@ namespace MediaBrowser.Providers.Manager
where T : BaseItem, new() where T : BaseItem, new()
{ {
// Give it a dummy path just so that it looks like a file system item // Give it a dummy path just so that it looks like a file system item
var dummy = new T() var dummy = new T
{ {
Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"), Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"),
ParentId = Guid.NewGuid() ParentId = Guid.NewGuid()
@ -508,11 +535,12 @@ namespace MediaBrowser.Providers.Manager
var libraryOptions = new LibraryOptions(); var libraryOptions = new LibraryOptions();
var imageProviders = GetImageProviders(dummy, libraryOptions, options, var imageProviders = GetImageProviders(
new ImageRefreshOptions( dummy,
new DirectoryService(_fileSystem)), libraryOptions,
true) options,
.ToList(); new ImageRefreshOptions(new DirectoryService(_fileSystem)),
true).ToList();
var pluginList = summary.Plugins.ToList(); var pluginList = summary.Plugins.ToList();
@ -572,7 +600,6 @@ namespace MediaBrowser.Providers.Manager
private void AddImagePlugins<T>(List<MetadataPlugin> list, T item, List<IImageProvider> imageProviders) private void AddImagePlugins<T>(List<MetadataPlugin> list, T item, List<IImageProvider> imageProviders)
where T : BaseItem where T : BaseItem
{ {
// Locals // Locals
list.AddRange(imageProviders.Where(i => (i is ILocalImageProvider)).Select(i => new MetadataPlugin list.AddRange(imageProviders.Where(i => (i is ILocalImageProvider)).Select(i => new MetadataPlugin
{ {
@ -588,6 +615,7 @@ namespace MediaBrowser.Providers.Manager
})); }));
} }
/// <inheritdoc/>
public MetadataOptions GetMetadataOptions(BaseItem item) public MetadataOptions GetMetadataOptions(BaseItem item)
{ {
var type = item.GetType().Name; var type = item.GetType().Name;
@ -597,17 +625,13 @@ namespace MediaBrowser.Providers.Manager
new MetadataOptions(); new MetadataOptions();
} }
/// <summary> /// <inheritdoc/>
/// Saves the metadata.
/// </summary>
public void SaveMetadata(BaseItem item, ItemUpdateType updateType) public void SaveMetadata(BaseItem item, ItemUpdateType updateType)
{ {
SaveMetadata(item, updateType, _savers); SaveMetadata(item, updateType, _savers);
} }
/// <summary> /// <inheritdoc/>
/// Saves the metadata.
/// </summary>
public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers) public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers)
{ {
SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparer.OrdinalIgnoreCase))); SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparer.OrdinalIgnoreCase)));
@ -619,7 +643,6 @@ namespace MediaBrowser.Providers.Manager
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <param name="updateType">Type of the update.</param> /// <param name="updateType">Type of the update.</param>
/// <param name="savers">The savers.</param> /// <param name="savers">The savers.</param>
/// <returns>Task.</returns>
private void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers) private void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers)
{ {
var libraryOptions = _libraryManager.GetLibraryOptions(item); var libraryOptions = _libraryManager.GetLibraryOptions(item);
@ -628,11 +651,9 @@ namespace MediaBrowser.Providers.Manager
{ {
_logger.LogDebug("Saving {0} to {1}.", item.Path ?? item.Name, saver.Name); _logger.LogDebug("Saving {0} to {1}.", item.Path ?? item.Name, saver.Name);
var fileSaver = saver as IMetadataFileSaver; if (saver is IMetadataFileSaver fileSaver)
if (fileSaver != null)
{ {
string path = null; string path;
try try
{ {
@ -699,11 +720,9 @@ namespace MediaBrowser.Providers.Manager
{ {
if (updateType >= ItemUpdateType.MetadataEdit) if (updateType >= ItemUpdateType.MetadataEdit)
{ {
var fileSaver = saver as IMetadataFileSaver;
// Manual edit occurred // Manual edit occurred
// Even if save local is off, save locally anyway if the metadata file already exists // Even if save local is off, save locally anyway if the metadata file already exists
if (fileSaver == null || !File.Exists(fileSaver.GetSavePath(item))) if (!(saver is IMetadataFileSaver fileSaver) || !File.Exists(fileSaver.GetSavePath(item)))
{ {
return false; return false;
} }
@ -734,6 +753,7 @@ namespace MediaBrowser.Providers.Manager
} }
} }
/// <inheritdoc/>
public Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, CancellationToken cancellationToken) public Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, CancellationToken cancellationToken)
where TItemType : BaseItem, new() where TItemType : BaseItem, new()
where TLookupType : ItemLookupInfo where TLookupType : ItemLookupInfo
@ -748,7 +768,7 @@ namespace MediaBrowser.Providers.Manager
return GetRemoteSearchResults<TItemType, TLookupType>(searchInfo, referenceItem, cancellationToken); return GetRemoteSearchResults<TItemType, TLookupType>(searchInfo, referenceItem, cancellationToken);
} }
public async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, BaseItem referenceItem, CancellationToken cancellationToken) private async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, BaseItem referenceItem, CancellationToken cancellationToken)
where TItemType : BaseItem, new() where TItemType : BaseItem, new()
where TLookupType : ItemLookupInfo where TLookupType : ItemLookupInfo
{ {
@ -837,7 +857,9 @@ namespace MediaBrowser.Providers.Manager
return resultList; return resultList;
} }
private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>(IRemoteSearchProvider<TLookupType> provider, TLookupType searchInfo, private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>(
IRemoteSearchProvider<TLookupType> provider,
TLookupType searchInfo,
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TLookupType : ItemLookupInfo where TLookupType : ItemLookupInfo
{ {
@ -853,6 +875,7 @@ namespace MediaBrowser.Providers.Manager
return list; return list;
} }
/// <inheritdoc/>
public Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken) public Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken)
{ {
var provider = _metadataProviders.OfType<IRemoteSearchProvider>().FirstOrDefault(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase)); var provider = _metadataProviders.OfType<IRemoteSearchProvider>().FirstOrDefault(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
@ -865,6 +888,7 @@ namespace MediaBrowser.Providers.Manager
return provider.GetImageResponse(url, cancellationToken); return provider.GetImageResponse(url, cancellationToken);
} }
/// <inheritdoc/>
public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item) public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
{ {
return _externalIds.Where(i => return _externalIds.Where(i =>
@ -881,6 +905,7 @@ namespace MediaBrowser.Providers.Manager
}); });
} }
/// <inheritdoc/>
public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item) public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
{ {
return GetExternalIds(item) return GetExternalIds(item)
@ -909,6 +934,7 @@ namespace MediaBrowser.Providers.Manager
}).Where(i => i != null).Concat(item.GetRelatedUrls()); }).Where(i => i != null).Concat(item.GetRelatedUrls());
} }
/// <inheritdoc/>
public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item) public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item)
{ {
return GetExternalIds(item) return GetExternalIds(item)
@ -921,8 +947,7 @@ namespace MediaBrowser.Providers.Manager
}); });
} }
private ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>(); /// <inheritdoc/>
public Dictionary<Guid, Guid> GetRefreshQueue() public Dictionary<Guid, Guid> GetRefreshQueue()
{ {
lock (_refreshQueueLock) lock (_refreshQueueLock)
@ -938,6 +963,7 @@ namespace MediaBrowser.Providers.Manager
} }
} }
/// <inheritdoc/>
public void OnRefreshStart(BaseItem item) public void OnRefreshStart(BaseItem item)
{ {
_logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); _logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
@ -945,6 +971,7 @@ namespace MediaBrowser.Providers.Manager
RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item)); RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
} }
/// <inheritdoc/>
public void OnRefreshComplete(BaseItem item) public void OnRefreshComplete(BaseItem item)
{ {
_logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); _logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
@ -954,6 +981,7 @@ namespace MediaBrowser.Providers.Manager
RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item)); RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
} }
/// <inheritdoc/>
public double? GetRefreshProgress(Guid id) public double? GetRefreshProgress(Guid id)
{ {
if (_activeRefreshes.TryGetValue(id, out double value)) if (_activeRefreshes.TryGetValue(id, out double value))
@ -964,6 +992,7 @@ namespace MediaBrowser.Providers.Manager
return null; return null;
} }
/// <inheritdoc/>
public void OnRefreshProgress(BaseItem item, double progress) public void OnRefreshProgress(BaseItem item, double progress)
{ {
var id = item.Id; var id = item.Id;
@ -983,12 +1012,7 @@ namespace MediaBrowser.Providers.Manager
RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress))); RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress)));
} }
private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue = /// <inheritdoc/>
new SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>>();
private readonly object _refreshQueueLock = new object();
private bool _isProcessingRefreshQueue;
public void QueueRefresh(Guid id, MetadataRefreshOptions options, RefreshPriority priority) public void QueueRefresh(Guid id, MetadataRefreshOptions options, RefreshPriority priority)
{ {
if (_disposed) if (_disposed)
@ -1032,7 +1056,7 @@ namespace MediaBrowser.Providers.Manager
if (item != null) if (item != null)
{ {
// Try to throttle this a little bit. // Try to throttle this a little bit.
await Task.Delay(100).ConfigureAwait(false); await Task.Delay(100, cancellationToken).ConfigureAwait(false);
var task = item is MusicArtist artist var task = item is MusicArtist artist
? RefreshArtist(artist, refreshItem.Item2, cancellationToken) ? RefreshArtist(artist, refreshItem.Item2, cancellationToken)
@ -1062,17 +1086,14 @@ namespace MediaBrowser.Providers.Manager
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
// Collection folders don't validate their children so we'll have to simulate that here // Collection folders don't validate their children so we'll have to simulate that here
switch (item)
if (item is CollectionFolder collectionFolder)
{ {
await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false); case CollectionFolder collectionFolder:
} await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
else break;
{ case Folder folder:
if (item is Folder folder)
{
await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false); await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
} break;
} }
} }
@ -1082,7 +1103,7 @@ namespace MediaBrowser.Providers.Manager
{ {
await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true).ConfigureAwait(false); await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
} }
} }
@ -1118,12 +1139,13 @@ namespace MediaBrowser.Providers.Manager
} }
} }
/// <inheritdoc/>
public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken) public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
return RefreshItem(item, options, cancellationToken); return RefreshItem(item, options, cancellationToken);
} }
private bool _disposed; /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
_disposed = true; _disposed = true;

@ -16,8 +16,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
<PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" /> <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
<PackageReference Include="PlaylistsNET" Version="1.0.6" /> <PackageReference Include="PlaylistsNET" Version="1.0.6" />
<PackageReference Include="TvDbSharper" Version="3.2.0" /> <PackageReference Include="TvDbSharper" Version="3.2.0" />

@ -28,29 +28,31 @@
pluginId: "a629c0da-fac5-4c7e-931a-7174223f14c8" pluginId: "a629c0da-fac5-4c7e-931a-7174223f14c8"
}; };
$('.configPage').on('pageshow', function () { document.querySelector('.configPage')
Dashboard.showLoadingMsg(); .addEventListener('pageshow', function () {
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { Dashboard.showLoadingMsg();
$('#enable').checked = config.Enable; ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
$('#replaceAlbumName').checked = config.ReplaceAlbumName; document.querySelector('#enable').checked = config.Enable;
document.querySelector('#replaceAlbumName').checked = config.ReplaceAlbumName;
Dashboard.hideLoadingMsg();
Dashboard.hideLoadingMsg();
});
}); });
});
$('.configForm').on('submit', function (e) {
Dashboard.showLoadingMsg();
var form = this; document.querySelector('.configForm')
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { .addEventListener('submit', function (e) {
config.Enable = $('#enable', form).checked; Dashboard.showLoadingMsg();
config.ReplaceAlbumName = $('#replaceAlbumName', form).checked;
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); config.Enable = document.querySelector('#enable').checked;
config.ReplaceAlbumName = document.querySelector('#replaceAlbumName').checked;
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
});
e.preventDefault();
return false;
}); });
return false;
});
</script> </script>
</div> </div>
</body> </body>

@ -36,33 +36,47 @@
uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a" uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"
}; };
$('.musicBrainzConfigPage').on('pageshow', function () { document.querySelector('.musicBrainzConfigPage')
Dashboard.showLoadingMsg(); .addEventListener('pageshow', function () {
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { Dashboard.showLoadingMsg();
$('#server').val(config.Server).change(); ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
$('#rateLimit').val(config.RateLimit).change(); var server = document.querySelector('#server');
$('#enable').checked = config.Enable; server.value = config.Server;
$('#replaceArtistName').checked = config.ReplaceArtistName; server.dispatchEvent(new Event('change', {
bubbles: true,
cancelable: false
}));
var rateLimit = document.querySelector('#rateLimit');
rateLimit.value = config.RateLimit;
rateLimit.dispatchEvent(new Event('change', {
bubbles: true,
cancelable: false
}));
document.querySelector('#enable').checked = config.Enable;
document.querySelector('#replaceArtistName').checked = config.ReplaceArtistName;
Dashboard.hideLoadingMsg(); Dashboard.hideLoadingMsg();
});
}); });
});
document.querySelector('.musicBrainzConfigForm')
$('.musicBrainzConfigForm').on('submit', function (e) { .addEventListener('submit', function (e) {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
var form = this; ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { config.Server = document.querySelector('#server').value;
config.Server = $('#server', form).val(); config.RateLimit = document.querySelector('#rateLimit').value;
config.RateLimit = $('#rateLimit', form).val(); config.Enable = document.querySelector('#enable').checked;
config.Enable = $('#enable', form).checked; config.ReplaceArtistName = document.querySelector('#replaceArtistName').checked;
config.ReplaceArtistName = $('#replaceArtistName', form).checked;
ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); });
e.preventDefault();
return false;
}); });
return false;
});
</script> </script>
</div> </div>
</body> </body>

@ -24,25 +24,28 @@
pluginId: "a628c0da-fac5-4c7e-9d1a-7134223f14c8" pluginId: "a628c0da-fac5-4c7e-9d1a-7134223f14c8"
}; };
$('.configPage').on('pageshow', function () { document.querySelector('.configPage')
Dashboard.showLoadingMsg(); .addEventListener('pageshow', function () {
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { Dashboard.showLoadingMsg();
$('#castAndCrew').checked = config.CastAndCrew; ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
Dashboard.hideLoadingMsg(); document.querySelector('#castAndCrew').checked = config.CastAndCrew;
Dashboard.hideLoadingMsg();
});
}); });
});
$('.configForm').on('submit', function (e) {
Dashboard.showLoadingMsg(); document.querySelector('.configForm')
.addEventListener('submit', function (e) {
var form = this; Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
config.CastAndCrew = $('#castAndCrew', form).checked; ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); config.CastAndCrew = document.querySelector('#castAndCrew').checked;
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
});
e.preventDefault();
return false;
}); });
return false;
});
</script> </script>
</div> </div>
</body> </body>

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -229,6 +230,45 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken); return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
} }
public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
if (imagesSummary.Data.Fanart > 0)
{
yield return KeyType.Fanart;
}
if (imagesSummary.Data.Series > 0)
{
yield return KeyType.Series;
}
if (imagesSummary.Data.Poster > 0)
{
yield return KeyType.Poster;
}
}
public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
if (imagesSummary.Data.Season > 0)
{
yield return KeyType.Season;
}
if (imagesSummary.Data.Fanart > 0)
{
yield return KeyType.Fanart;
}
// TODO seasonwide is not supported in TvDbSharper
}
private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory) private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
{ {
if (_cache.TryGetValue(key, out T cachedValue)) if (_cache.TryGetValue(key, out T cachedValue))

@ -65,8 +65,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
var language = item.GetPreferredMetadataLanguage(); var language = item.GetPreferredMetadataLanguage();
var remoteImages = new List<RemoteImageInfo>(); var remoteImages = new List<RemoteImageInfo>();
var keyTypes = new[] { KeyType.Season, KeyType.Seasonwide, KeyType.Fanart }; var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false);
foreach (var keyType in keyTypes) await foreach (var keyType in keyTypes)
{ {
var imageQuery = new ImagesQuery var imageQuery = new ImagesQuery
{ {

@ -59,9 +59,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
var language = item.GetPreferredMetadataLanguage(); var language = item.GetPreferredMetadataLanguage();
var remoteImages = new List<RemoteImageInfo>(); var remoteImages = new List<RemoteImageInfo>();
var keyTypes = new[] { KeyType.Poster, KeyType.Series, KeyType.Fanart };
var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb)); var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb));
foreach (KeyType keyType in keyTypes) var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken)
.ConfigureAwait(false);
await foreach (KeyType keyType in allowedKeyTypes)
{ {
var imageQuery = new ImagesQuery var imageQuery = new ImagesQuery
{ {

@ -247,10 +247,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{ {
Name = tvdbTitles.FirstOrDefault(), Name = tvdbTitles.FirstOrDefault(),
ProductionYear = firstAired.Year, ProductionYear = firstAired.Year,
SearchProviderName = Name, SearchProviderName = Name
ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner
}; };
if (!string.IsNullOrEmpty(seriesSearchResult.Banner))
{
// Results from their Search endpoints already include the /banners/ part in the url, because reasons...
remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + seriesSearchResult.Banner;
}
try try
{ {
var seriesSesult = var seriesSesult =
@ -365,10 +370,14 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
Type = PersonType.Actor, Type = PersonType.Actor,
Name = (actor.Name ?? string.Empty).Trim(), Name = (actor.Name ?? string.Empty).Trim(),
Role = actor.Role, Role = actor.Role,
ImageUrl = TvdbUtils.BannerUrl + actor.Image,
SortOrder = actor.SortOrder SortOrder = actor.SortOrder
}; };
if (!string.IsNullOrEmpty(actor.Image))
{
personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image;
}
if (!string.IsNullOrWhiteSpace(personInfo.Name)) if (!string.IsNullOrWhiteSpace(personInfo.Name))
{ {
result.AddPerson(personInfo); result.AddPerson(personInfo);

@ -9,7 +9,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{ {
public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K"; public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K";
public const string TvdbBaseUrl = "https://www.thetvdb.com/"; public const string TvdbBaseUrl = "https://www.thetvdb.com/";
public const string BannerUrl = TvdbBaseUrl + "banners/"; public const string TvdbImageBaseUrl = "https://www.thetvdb.com";
public const string BannerUrl = TvdbImageBaseUrl + "/banners/";
public static ImageType GetImageTypeFromKeyType(string keyType) public static ImageType GetImageTypeFromKeyType(string keyType)
{ {

@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.TV
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error in DummySeasonProvider"); Logger.LogError(ex, "Error in DummySeasonProvider for {ItemPath}", item.Path);
} }
} }

6
debian/changelog vendored

@ -1,3 +1,9 @@
jellyfin-server (10.6.0-2) unstable; urgency=medium
* Fix upgrade bug
-- Joshua Boniface <joshua@boniface.me> Sun, 19 Jul 22:47:27 -0400
jellyfin-server (10.6.0-1) unstable; urgency=medium jellyfin-server (10.6.0-1) unstable; urgency=medium
* Forthcoming stable release * Forthcoming stable release

@ -31,7 +31,7 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
#JELLYFIN_SERVICE_OPT="--service" #JELLYFIN_SERVICE_OPT="--service"
# [OPTIONAL] run Jellyfin without the web app # [OPTIONAL] run Jellyfin without the web app
#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp" #JELLYFIN_NOWEBAPP_OPT="--nowebclient"
# #
# SysV init/Upstart options # SysV init/Upstart options
@ -40,4 +40,4 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
# Application username # Application username
JELLYFIN_USER="jellyfin" JELLYFIN_USER="jellyfin"
# Full application command # Full application command
JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT" JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT"

5
debian/control vendored

@ -15,9 +15,8 @@ Vcs-Git: https://github.org/jellyfin/jellyfin.git
Vcs-Browser: https://github.org/jellyfin/jellyfin Vcs-Browser: https://github.org/jellyfin/jellyfin
Package: jellyfin-server Package: jellyfin-server
Replaces: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server Replaces: jellyfin (<<10.6.0)
Breaks: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server Breaks: jellyfin (<<10.6.0)
Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
Architecture: any Architecture: any
Depends: at, Depends: at,
libsqlite3-0, libsqlite3-0,

@ -8,8 +8,7 @@ set -o xtrace
# Version variables # Version variables
NSSM_VERSION="nssm-2.24-101-g897c7ad" NSSM_VERSION="nssm-2.24-101-g897c7ad"
NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip" NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
FFMPEG_VERSION="ffmpeg-4.3-win64-static" FFMPEG_URL="https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip";
FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
# Move to source directory # Move to source directory
pushd ${SOURCE_DIR} pushd ${SOURCE_DIR}
@ -29,12 +28,11 @@ dotnet publish Jellyfin.Server --configuration Release --self-contained --runtim
# Prepare addins # Prepare addins
addin_build_dir="$( mktemp -d )" addin_build_dir="$( mktemp -d )"
wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip
wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip wget ${FFMPEG_URL} -O ${addin_build_dir}/jellyfin-ffmpeg.zip
unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir} unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir}
cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe ${output_dir}/nssm.exe cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe ${output_dir}/nssm.exe
unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir} unzip ${addin_build_dir}/jellyfin-ffmpeg.zip -d ${addin_build_dir}/jellyfin-ffmpeg
cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${output_dir}/ffmpeg.exe cp ${addin_build_dir}/jellyfin-ffmpeg/* ${output_dir}
cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe ${output_dir}/ffprobe.exe
rm -rf ${addin_build_dir} rm -rf ${addin_build_dir}
# Prepare scripts # Prepare scripts

@ -16,7 +16,7 @@
<PackageReference Include="AutoFixture" Version="4.13.0" /> <PackageReference Include="AutoFixture" Version="4.13.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.5" /> <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.5" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />

@ -47,7 +47,7 @@ function Install-FFMPEG {
param( param(
[string]$ResolvedInstallLocation, [string]$ResolvedInstallLocation,
[string]$Architecture, [string]$Architecture,
[string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared" [string]$FFMPEGVersionX86 = "ffmpeg-4.3-win32-shared"
) )
Write-Verbose "Checking Architecture" Write-Verbose "Checking Architecture"
if($Architecture -notin @('x86','x64')){ if($Architecture -notin @('x86','x64')){

Loading…
Cancel
Save