Merge branch 'master' into readonlyspan
This commit is contained in:
commit
3ff110984a
|
@ -11,6 +11,7 @@ using MediaBrowser.Controller.Configuration;
|
|||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Emby.Dlna.Api
|
||||
{
|
||||
|
@ -108,7 +109,7 @@ namespace Emby.Dlna.Api
|
|||
public string Filename { get; set; }
|
||||
}
|
||||
|
||||
public class DlnaServerService : IService, IRequiresRequest
|
||||
public class DlnaServerService : IService
|
||||
{
|
||||
private const string XMLContentType = "text/xml; charset=UTF-8";
|
||||
|
||||
|
@ -127,11 +128,13 @@ namespace Emby.Dlna.Api
|
|||
public DlnaServerService(
|
||||
IDlnaManager dlnaManager,
|
||||
IHttpResultFactory httpResultFactory,
|
||||
IServerConfigurationManager configurationManager)
|
||||
IServerConfigurationManager configurationManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
_resultFactory = httpResultFactory;
|
||||
_configurationManager = configurationManager;
|
||||
Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor));
|
||||
}
|
||||
|
||||
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\">");
|
||||
foreach (var key in stateVariables.Keys)
|
||||
{
|
||||
builder.Append("<e:property>");
|
||||
builder.Append("<" + key + ">");
|
||||
builder.Append(stateVariables[key]);
|
||||
builder.Append("</" + key + ">");
|
||||
builder.Append("</e:property>");
|
||||
builder.Append("<e:property>")
|
||||
.Append('<')
|
||||
.Append(key)
|
||||
.Append('>')
|
||||
.Append(stateVariables[key])
|
||||
.Append("</")
|
||||
.Append(key)
|
||||
.Append('>')
|
||||
.Append("</e:property>");
|
||||
}
|
||||
|
||||
builder.Append("</e:propertyset>");
|
||||
|
|
|
@ -4,12 +4,12 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Server;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
@ -334,7 +334,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return string.Empty;
|
||||
}
|
||||
|
||||
return DescriptionXmlBuilder.Escape(value);
|
||||
return SecurityElement.Escape(value);
|
||||
}
|
||||
|
||||
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
|
|
|
@ -4,6 +4,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Emby.Dlna.Common;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
|
|||
|
||||
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("<major>1</major>");
|
||||
|
@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
|
|||
|
||||
if (!EnableAbsoluteUrls)
|
||||
{
|
||||
builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>");
|
||||
builder.Append("<URLBase>")
|
||||
.Append(SecurityElement.Escape(_serverAddress))
|
||||
.Append("</URLBase>");
|
||||
}
|
||||
|
||||
AppendDeviceInfo(builder);
|
||||
|
@ -93,91 +96,14 @@ namespace Emby.Dlna.Server
|
|||
|
||||
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);
|
||||
builder.Append("</device>");
|
||||
}
|
||||
|
||||
private static readonly char[] s_escapeChars = new char[]
|
||||
{
|
||||
'<',
|
||||
'>',
|
||||
'"',
|
||||
'\'',
|
||||
'&'
|
||||
};
|
||||
|
||||
private static readonly string[] s_escapeStringPairs = new[]
|
||||
{
|
||||
"<",
|
||||
"<",
|
||||
">",
|
||||
">",
|
||||
"\"",
|
||||
""",
|
||||
"'",
|
||||
"'",
|
||||
"&",
|
||||
"&"
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
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("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>");
|
||||
builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
|
||||
builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
|
||||
builder.Append("<friendlyName>")
|
||||
.Append(SecurityElement.Escape(GetFriendlyName()))
|
||||
.Append("</friendlyName>");
|
||||
builder.Append("<manufacturer>")
|
||||
.Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
|
||||
.Append("</manufacturer>");
|
||||
builder.Append("<manufacturerURL>")
|
||||
.Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
|
||||
.Append("</manufacturerURL>");
|
||||
|
||||
builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
|
||||
builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
|
||||
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>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
|
||||
builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
|
||||
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))
|
||||
{
|
||||
builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
|
||||
builder.Append("<serialNumber>")
|
||||
.Append(SecurityElement.Escape(_serverId))
|
||||
.Append("</serialNumber>");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
|
||||
builder.Append("<serialNumber>")
|
||||
.Append(SecurityElement.Escape(_profile.SerialNumber))
|
||||
.Append("</serialNumber>");
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
|
||||
builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>");
|
||||
builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>");
|
||||
builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>");
|
||||
builder.Append("<url>" + BuildUrl(icon.Url) + "</url>");
|
||||
builder.Append("<mimetype>")
|
||||
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
|
||||
.Append("</mimetype>");
|
||||
builder.Append("<width>")
|
||||
.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>");
|
||||
}
|
||||
|
@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
|
|||
{
|
||||
builder.Append("<service>");
|
||||
|
||||
builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
|
||||
builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
|
||||
builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>");
|
||||
builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>");
|
||||
builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>");
|
||||
builder.Append("<serviceType>")
|
||||
.Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
|
||||
.Append("</serviceType>");
|
||||
builder.Append("<serviceId>")
|
||||
.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>");
|
||||
}
|
||||
|
@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
|
|||
url = _serverAddress.TrimEnd('/') + url;
|
||||
}
|
||||
|
||||
return Escape(url);
|
||||
return SecurityElement.Escape(url);
|
||||
}
|
||||
|
||||
private IEnumerable<DeviceIcon> GetIcons()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Server;
|
||||
|
||||
namespace Emby.Dlna.Service
|
||||
{
|
||||
|
@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
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>");
|
||||
|
||||
|
@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
builder.Append("<argument>");
|
||||
|
||||
builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>");
|
||||
builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>");
|
||||
builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>");
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(argument.Name ?? string.Empty))
|
||||
.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>");
|
||||
}
|
||||
|
@ -68,17 +76,25 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
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("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>");
|
||||
builder.Append("<name>")
|
||||
.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)
|
||||
{
|
||||
builder.Append("<allowedValueList>");
|
||||
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>");
|
||||
|
|
|
@ -136,8 +136,8 @@ namespace Emby.Naming.Common
|
|||
|
||||
CleanDateTimes = new[]
|
||||
{
|
||||
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{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})*",
|
||||
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
|
||||
};
|
||||
|
||||
CleanStrings = new[]
|
||||
|
@ -277,7 +277,7 @@ namespace Emby.Naming.Common
|
|||
// This isn't a Kodi naming rule, but the expression below causes false positives,
|
||||
// so we make sure this one gets tested first.
|
||||
// "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
|
||||
},
|
||||
|
@ -300,32 +300,32 @@ namespace Emby.Naming.Common
|
|||
// *** End Kodi Standard Naming
|
||||
|
||||
// [bar] Foo - 1 [baz]
|
||||
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
|
||||
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$")
|
||||
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
|
||||
{
|
||||
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
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
// "01.avi"
|
||||
new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$")
|
||||
new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
|
||||
{
|
||||
IsOptimistic = true,
|
||||
IsNamed = true
|
||||
|
@ -335,34 +335,34 @@ namespace Emby.Naming.Common
|
|||
new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
|
||||
|
||||
// "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,
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// "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,
|
||||
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"
|
||||
new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
|
||||
new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
|
||||
{
|
||||
IsOptimistic = true,
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// "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,
|
||||
IsNamed = true
|
||||
},
|
||||
// "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,
|
||||
IsNamed = true
|
||||
|
@ -625,17 +625,17 @@ namespace Emby.Naming.Common
|
|||
AudioBookPartsExpressions = new[]
|
||||
{
|
||||
// Detect specified chapters, like CH 01
|
||||
@"ch(?:apter)?[\s_-]?(?<chapter>\d+)",
|
||||
@"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
|
||||
// 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>\d+)",
|
||||
"^(?<chapter>[0-9]+)",
|
||||
// Part if often ending of filename
|
||||
@"(?<part>\d+)$",
|
||||
"(?<part>[0-9]+)$",
|
||||
// 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.
|
||||
@"dis(?:c|k)[\s_-]?(?<chapter>\d+)"
|
||||
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
|
||||
};
|
||||
|
||||
var extensions = VideoFileExtensions.ToList();
|
||||
|
@ -675,16 +675,16 @@ namespace Emby.Naming.Common
|
|||
|
||||
MultipleEpisodeExpressions = new string[]
|
||||
{
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{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]?\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]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{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](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<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>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{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]?[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]?[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]?[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>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{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)
|
||||
{
|
||||
IsNamed = true
|
||||
|
|
|
@ -192,7 +192,7 @@ namespace Emby.Server.Implementations
|
|||
/// Gets or sets the application paths.
|
||||
/// </summary>
|
||||
/// <value>The application paths.</value>
|
||||
protected ServerApplicationPaths ApplicationPaths { get; set; }
|
||||
protected IServerApplicationPaths ApplicationPaths { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets all concrete types.
|
||||
|
@ -236,7 +236,7 @@ namespace Emby.Server.Implementations
|
|||
/// Initializes a new instance of the <see cref="ApplicationHost" /> class.
|
||||
/// </summary>
|
||||
public ApplicationHost(
|
||||
ServerApplicationPaths applicationPaths,
|
||||
IServerApplicationPaths applicationPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IStartupOptions options,
|
||||
IFileSystem fileSystem,
|
||||
|
@ -795,7 +795,6 @@ namespace Emby.Server.Implementations
|
|||
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
|
||||
|
||||
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
|
||||
Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
|
||||
|
||||
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
|
||||
}
|
||||
|
|
|
@ -1110,7 +1110,8 @@ namespace Emby.Server.Implementations.Data
|
|||
continue;
|
||||
}
|
||||
|
||||
str.Append(ToValueString(i) + "|");
|
||||
str.Append(ToValueString(i))
|
||||
.Append('|');
|
||||
}
|
||||
|
||||
str.Length -= 1; // Remove last |
|
||||
|
@ -2471,7 +2472,7 @@ namespace Emby.Server.Implementations.Data
|
|||
var item = query.SimilarTo;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("(");
|
||||
builder.Append('(');
|
||||
|
||||
if (string.IsNullOrEmpty(item.OfficialRating))
|
||||
{
|
||||
|
@ -2509,7 +2510,7 @@ namespace Emby.Server.Implementations.Data
|
|||
if (!string.IsNullOrEmpty(query.SearchTerm))
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("(");
|
||||
builder.Append('(');
|
||||
|
||||
builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
|
||||
|
||||
|
@ -5238,7 +5239,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
{
|
||||
if (i > 0)
|
||||
{
|
||||
insertText.Append(",");
|
||||
insertText.Append(',');
|
||||
}
|
||||
|
||||
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
|
||||
|
@ -6331,7 +6332,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
|
||||
foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
|
||||
{
|
||||
insertText.Append("@" + column + index + ",");
|
||||
insertText.Append('@')
|
||||
.Append(column)
|
||||
.Append(index)
|
||||
.Append(',');
|
||||
}
|
||||
|
||||
insertText.Length -= 1;
|
||||
|
|
|
@ -29,7 +29,6 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
private readonly IStreamHelper _streamHelper;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// The _options.
|
||||
|
@ -49,7 +48,6 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
}
|
||||
|
||||
_streamHelper = streamHelper;
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
Path = path;
|
||||
_logger = logger;
|
||||
|
|
|
@ -13,26 +13,22 @@ using MediaBrowser.Controller.Security;
|
|||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
private readonly IAuthorizationContext _authorizationContext;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly INetworkManager _networkManager;
|
||||
|
||||
public AuthService(
|
||||
ILogger<AuthService> logger,
|
||||
IAuthorizationContext authorizationContext,
|
||||
IServerConfigurationManager config,
|
||||
ISessionManager sessionManager,
|
||||
INetworkManager networkManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_authorizationContext = authorizationContext;
|
||||
_config = config;
|
||||
_sessionManager = sessionManager;
|
||||
|
|
|
@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
|
|||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
@ -25,14 +24,9 @@ namespace Emby.Server.Implementations.Images
|
|||
/// </summary>
|
||||
public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
|
||||
public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor)
|
||||
: base(fileSystem, providerManager, applicationPaths, imageProcessor)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
|
|||
if (parent != null)
|
||||
{
|
||||
// Don't resolve these into audio files
|
||||
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename)
|
||||
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
|
||||
&& _libraryManager.IsAudioFile(filename))
|
||||
{
|
||||
return true;
|
||||
|
|
|
@ -11,6 +11,17 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
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 string OriginalStreamId { get; set; }
|
||||
|
@ -21,18 +32,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
public MediaSourceInfo MediaSource { get; set; }
|
||||
|
||||
public string UniqueId { get; private set; }
|
||||
|
||||
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 string UniqueId { get; }
|
||||
|
||||
public Task Close()
|
||||
{
|
||||
|
|
|
@ -60,6 +60,8 @@ namespace Emby.Server.Implementations.Library
|
|||
/// </summary>
|
||||
public class LibraryManager : ILibraryManager
|
||||
{
|
||||
private const string ShortcutFileExtension = ".mblink";
|
||||
|
||||
private readonly ILogger<LibraryManager> _logger;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
@ -75,63 +77,24 @@ namespace Emby.Server.Implementations.Library
|
|||
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder sync lock.
|
||||
/// </summary>
|
||||
private readonly object _rootFolderSyncLock = new object();
|
||||
private readonly object _userRootFolderSyncLock = new object();
|
||||
|
||||
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
|
||||
|
||||
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.
|
||||
/// The _root folder.
|
||||
/// </summary>
|
||||
/// <value>The postscan tasks.</value>
|
||||
private ILibraryPostScanTask[] PostscanTasks { get; set; }
|
||||
private volatile AggregateFolder _rootFolder;
|
||||
private volatile UserRootFolder _userRootFolder;
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [item added].
|
||||
/// </summary>
|
||||
public event EventHandler<ItemChangeEventArgs> ItemAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [item updated].
|
||||
/// </summary>
|
||||
public event EventHandler<ItemChangeEventArgs> ItemUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [item removed].
|
||||
/// </summary>
|
||||
public event EventHandler<ItemChangeEventArgs> ItemRemoved;
|
||||
|
||||
public bool IsScanRunning { get; private set; }
|
||||
private bool _wizardCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
|
||||
|
@ -186,37 +149,19 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the parts.
|
||||
/// Occurs when [item added].
|
||||
/// </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();
|
||||
}
|
||||
public event EventHandler<ItemChangeEventArgs> ItemAdded;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder.
|
||||
/// Occurs when [item updated].
|
||||
/// </summary>
|
||||
private volatile AggregateFolder _rootFolder;
|
||||
public event EventHandler<ItemChangeEventArgs> ItemUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder sync lock.
|
||||
/// Occurs when [item removed].
|
||||
/// </summary>
|
||||
private readonly object _rootFolderSyncLock = new object();
|
||||
public event EventHandler<ItemChangeEventArgs> ItemRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root folder.
|
||||
|
@ -241,7 +186,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>
|
||||
/// Records the configuration values.
|
||||
|
@ -775,14 +781,11 @@ namespace Emby.Server.Implementations.Library
|
|||
return rootFolder;
|
||||
}
|
||||
|
||||
private volatile UserRootFolder _userRootFolder;
|
||||
private readonly object _syncLock = new object();
|
||||
|
||||
public Folder GetUserRootFolder()
|
||||
{
|
||||
if (_userRootFolder == null)
|
||||
{
|
||||
lock (_syncLock)
|
||||
lock (_userRootFolderSyncLock)
|
||||
{
|
||||
if (_userRootFolder == null)
|
||||
{
|
||||
|
@ -1332,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
return new QueryResult<BaseItem>
|
||||
{
|
||||
Items = _itemRepository.GetItemList(query).ToArray()
|
||||
Items = _itemRepository.GetItemList(query)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1463,11 +1466,9 @@ namespace Emby.Server.Implementations.Library
|
|||
return _itemRepository.GetItems(query);
|
||||
}
|
||||
|
||||
var list = _itemRepository.GetItemList(query);
|
||||
|
||||
return new QueryResult<BaseItem>
|
||||
{
|
||||
Items = list
|
||||
Items = _itemRepository.GetItemList(query)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1876,7 +1877,8 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
|
@ -1945,12 +1947,9 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <summary>
|
||||
/// Updates the item.
|
||||
/// </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
|
||||
var itemsList = items.ToList();
|
||||
|
||||
foreach (var item in itemsList)
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
|
@ -1962,11 +1961,11 @@ namespace Emby.Server.Implementations.Library
|
|||
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(itemsList, cancellationToken);
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
if (ItemUpdated != null)
|
||||
{
|
||||
foreach (var item in itemsList)
|
||||
foreach (var item in items)
|
||||
{
|
||||
// With the live tv guide this just creates too much noise
|
||||
if (item.SourceType != SourceType.Library)
|
||||
|
@ -2189,8 +2188,6 @@ namespace Emby.Server.Implementations.Library
|
|||
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
|
||||
}
|
||||
|
||||
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
|
||||
|
||||
public UserView GetNamedView(
|
||||
User user,
|
||||
string name,
|
||||
|
@ -2488,14 +2485,9 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
|
||||
|
||||
var episodeInfo = episode.IsFileProtocol ?
|
||||
resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
|
||||
new Naming.TV.EpisodeInfo();
|
||||
|
||||
if (episodeInfo == null)
|
||||
{
|
||||
episodeInfo = new Naming.TV.EpisodeInfo();
|
||||
}
|
||||
var episodeInfo = episode.IsFileProtocol
|
||||
? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
|
||||
: new Naming.TV.EpisodeInfo();
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -2503,11 +2495,13 @@ namespace Emby.Server.Implementations.Library
|
|||
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Read from metadata
|
||||
var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
||||
{
|
||||
MediaSource = episode.GetMediaSources(false)[0],
|
||||
MediaType = DlnaProfileType.Video
|
||||
}, CancellationToken.None).GetAwaiter().GetResult();
|
||||
var mediaInfo = _mediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = episode.GetMediaSources(false)[0],
|
||||
MediaType = DlnaProfileType.Video
|
||||
},
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (mediaInfo.ParentIndexNumber > 0)
|
||||
{
|
||||
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
|
||||
|
@ -2665,7 +2659,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
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)
|
||||
{
|
||||
|
@ -2682,9 +2676,7 @@ namespace Emby.Server.Implementations.Library
|
|||
.Select(video =>
|
||||
{
|
||||
// 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 (dbItem != null)
|
||||
if (GetItemById(video.Id) is Trailer dbItem)
|
||||
{
|
||||
video = dbItem;
|
||||
}
|
||||
|
@ -3011,8 +3003,6 @@ namespace Emby.Server.Implementations.Library
|
|||
});
|
||||
}
|
||||
|
||||
private const string ShortcutFileExtension = ".mblink";
|
||||
|
||||
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
|
||||
{
|
||||
AddMediaPathInternal(virtualFolderName, pathInfo, true);
|
||||
|
@ -3206,7 +3196,8 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
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)
|
||||
|
|
|
@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private IJsonSerializer _json;
|
||||
private IApplicationPaths _appPaths;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
|
||||
{
|
||||
|
@ -72,13 +71,14 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
mediaSource.AnalyzeDurationMs = 3000;
|
||||
|
||||
mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||
ExtractChapters = false
|
||||
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
mediaInfo = await _mediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||
ExtractChapters = false
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cacheFilePath != null)
|
||||
{
|
||||
|
@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
|
|||
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)
|
||||
{
|
||||
|
@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
|
|||
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.BitRate.HasValue)
|
||||
|
|
|
@ -29,6 +29,9 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
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 IUserManager _userManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
@ -40,6 +43,11 @@ namespace Emby.Server.Implementations.Library
|
|||
private readonly ILocalizationManager _localizationManager;
|
||||
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;
|
||||
|
||||
public MediaSourceManager(
|
||||
|
@ -368,7 +376,6 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
|
||||
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
|
||||
|
||||
|
@ -451,9 +458,6 @@ namespace Emby.Server.Implementations.Library
|
|||
.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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
|
@ -884,7 +885,6 @@ namespace Emby.Server.Implementations.Library
|
|||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private readonly object _disposeLock = new object();
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
|
|
|
@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System.Globalization;
|
||||
using Emby.Naming.TV;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
@ -13,7 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
/// </summary>
|
||||
public class SeasonResolver : FolderResolver<Season>
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ILogger<SeasonResolver> _logger;
|
||||
|
@ -21,17 +19,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The config.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="localization">The localization.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public SeasonResolver(
|
||||
IServerConfigurationManager config,
|
||||
ILibraryManager libraryManager,
|
||||
ILocalizationManager localization,
|
||||
ILogger<SeasonResolver> logger)
|
||||
{
|
||||
_config = config;
|
||||
_libraryManager = libraryManager;
|
||||
_localization = localization;
|
||||
_logger = logger;
|
||||
|
|
|
@ -20,13 +20,11 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
public class SearchEngine : ISearchEngine
|
||||
{
|
||||
private readonly ILogger<SearchEngine> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager)
|
||||
public SearchEngine(ILibraryManager libraryManager, IUserManager userManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
@ -34,11 +32,7 @@ namespace Emby.Server.Implementations.Library
|
|||
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
|
||||
{
|
||||
User user = null;
|
||||
|
||||
if (query.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
}
|
||||
else
|
||||
if (query.UserId != Guid.Empty)
|
||||
{
|
||||
user = _userManager.GetUserById(query.UserId);
|
||||
}
|
||||
|
@ -48,19 +42,19 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
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)
|
||||
{
|
||||
results = results.Take(query.Limit.Value).ToList();
|
||||
results = results.GetRange(0, query.Limit.Value);
|
||||
}
|
||||
|
||||
return new QueryResult<SearchHintInfo>
|
||||
{
|
||||
TotalRecordCount = totalRecordCount,
|
||||
|
||||
Items = results.ToArray()
|
||||
Items = results
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
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();
|
||||
|
|
|
@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
|
|||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Book = MediaBrowser.Controller.Entities.Book;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
|
@ -28,18 +27,15 @@ namespace Emby.Server.Implementations.Library
|
|||
private readonly ConcurrentDictionary<string, UserItemData> _userData =
|
||||
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly ILogger<UserDataManager> _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataRepository _repository;
|
||||
|
||||
public UserDataManager(
|
||||
ILogger<UserDataManager> logger,
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager,
|
||||
IUserDataRepository repository)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_userManager = userManager;
|
||||
_repository = repository;
|
||||
|
|
|
@ -4,6 +4,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Library;
|
||||
|
@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization;
|
|||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
|
@ -54,7 +54,6 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IChannelManager _channelManager;
|
||||
private readonly LiveTvDtoService _tvDtoService;
|
||||
|
@ -73,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
ILibraryManager libraryManager,
|
||||
ITaskManager taskManager,
|
||||
ILocalizationManager localization,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IFileSystem fileSystem,
|
||||
IChannelManager channelManager,
|
||||
LiveTvDtoService liveTvDtoService)
|
||||
|
@ -85,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
_libraryManager = libraryManager;
|
||||
_taskManager = taskManager;
|
||||
_localization = localization;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
_dtoService = dtoService;
|
||||
_userDataManager = userDataManager;
|
||||
|
@ -2234,7 +2231,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
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));
|
||||
|
||||
|
@ -2278,7 +2275,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
{
|
||||
// Hack to make the object a pure ListingsProviderInfo instead of an 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));
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -37,7 +36,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IApplicationPaths _applicationPaths;
|
||||
private readonly ILogger<TaskManager> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TaskManager" /> class.
|
||||
|
@ -45,17 +43,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
/// <param name="applicationPaths">The application paths.</param>
|
||||
/// <param name="jsonSerializer">The json serializer.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="fileSystem">The filesystem manager.</param>
|
||||
public TaskManager(
|
||||
IApplicationPaths applicationPaths,
|
||||
IJsonSerializer jsonSerializer,
|
||||
ILogger<TaskManager> logger,
|
||||
IFileSystem fileSystem)
|
||||
ILogger<TaskManager> logger)
|
||||
{
|
||||
_applicationPaths = applicationPaths;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers;
|
|||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks
|
||||
|
@ -24,11 +23,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
/// </summary>
|
||||
public class ChapterImagesTask : IScheduledTask
|
||||
{
|
||||
/// <summary>
|
||||
/// The _logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<ChapterImagesTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
|
@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
|
||||
/// </summary>
|
||||
public ChapterImagesTask(
|
||||
ILoggerFactory loggerFactory,
|
||||
ILibraryManager libraryManager,
|
||||
IItemRepository itemRepo,
|
||||
IApplicationPaths appPaths,
|
||||
|
@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
IFileSystem fileSystem,
|
||||
ILocalizationManager localization)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<ChapterImagesTask>();
|
||||
_libraryManager = libraryManager;
|
||||
_itemRepo = itemRepo;
|
||||
_appPaths = appPaths;
|
||||
|
|
|
@ -189,5 +189,4 @@ namespace Emby.Server.Implementations.Services
|
|||
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using System.Reflection;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -82,7 +83,8 @@ namespace Emby.Server.Implementations.Services
|
|||
var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
|
||||
|
||||
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
|
||||
|
||||
|
||||
httpRes.HttpContext.SetServiceStackRequest(httpReq);
|
||||
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
|
||||
|
||||
// Apply response filters
|
||||
|
|
|
@ -488,7 +488,8 @@ namespace Emby.Server.Implementations.Services
|
|||
sb.Append(value);
|
||||
for (var j = pathIx + 1; j < requestComponents.Length; j++)
|
||||
{
|
||||
sb.Append(PathSeperatorChar + requestComponents[j]);
|
||||
sb.Append(PathSeperatorChar)
|
||||
.Append(requestComponents[j]);
|
||||
}
|
||||
|
||||
value = sb.ToString();
|
||||
|
@ -505,7 +506,8 @@ namespace Emby.Server.Implementations.Services
|
|||
pathIx++;
|
||||
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
|
||||
sb.Append(PathSeperatorChar)
|
||||
.Append(requestComponents[pathIx++]);
|
||||
}
|
||||
|
||||
value = sb.ToString();
|
||||
|
|
|
@ -46,14 +46,12 @@ namespace Jellyfin.Api.Controllers
|
|||
[HttpGet("Configuration")]
|
||||
public StartupConfigurationDto GetStartupConfiguration()
|
||||
{
|
||||
var result = new StartupConfigurationDto
|
||||
return new StartupConfigurationDto
|
||||
{
|
||||
UICulture = _config.Configuration.UICulture,
|
||||
MetadataCountryCode = _config.Configuration.MetadataCountryCode,
|
||||
PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -92,10 +90,10 @@ namespace Jellyfin.Api.Controllers
|
|||
/// </summary>
|
||||
/// <returns>The first user.</returns>
|
||||
[HttpGet("User")]
|
||||
public StartupUserDto GetFirstUser()
|
||||
public async Task<StartupUserDto> GetFirstUser()
|
||||
{
|
||||
// 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();
|
||||
return new StartupUserDto
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Data.Entities;
|
||||
|
@ -133,6 +134,18 @@ namespace Jellyfin.Server.Implementations
|
|||
return base.SaveChanges();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Dispose()
|
||||
{
|
||||
foreach (var entry in ChangeTracker.Entries())
|
||||
{
|
||||
entry.State = EntityState.Detached;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.IO;
|
|||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
@ -23,7 +24,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
private const string BaseResetFileName = "passwordreset";
|
||||
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IApplicationHost _appHost;
|
||||
|
||||
private readonly string _passwordResetFileBase;
|
||||
private readonly string _passwordResetFileBaseDir;
|
||||
|
@ -33,16 +34,17 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
/// </summary>
|
||||
/// <param name="configurationManager">The configuration manager.</param>
|
||||
/// <param name="jsonSerializer">The JSON serializer.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
/// <param name="appHost">The application host.</param>
|
||||
public DefaultPasswordResetProvider(
|
||||
IServerConfigurationManager configurationManager,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IUserManager userManager)
|
||||
IApplicationHost appHost)
|
||||
{
|
||||
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
|
||||
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_userManager = userManager;
|
||||
_appHost = appHost;
|
||||
// TODO: Remove the circular dependency on UserManager
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -54,6 +56,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
/// <inheritdoc />
|
||||
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||
{
|
||||
var userManager = _appHost.Resolve<IUserManager>();
|
||||
var usersReset = new List<string>();
|
||||
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
|
||||
{
|
||||
|
@ -72,10 +75,10 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
pin.Replace("-", string.Empty, StringComparison.Ordinal),
|
||||
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");
|
||||
|
||||
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
|
||||
await userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
|
||||
usersReset.Add(resetUser.Username);
|
||||
File.Delete(resetFile);
|
||||
}
|
||||
|
@ -121,7 +124,6 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
}
|
||||
|
||||
user.EasyPassword = pin;
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
|
|
|
@ -39,12 +39,11 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
private readonly IApplicationHost _appHost;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly ILogger<UserManager> _logger;
|
||||
|
||||
private IAuthenticationProvider[] _authenticationProviders = null!;
|
||||
private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!;
|
||||
private InvalidAuthProvider _invalidAuthProvider = null!;
|
||||
private IPasswordResetProvider[] _passwordResetProviders = null!;
|
||||
private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!;
|
||||
private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders;
|
||||
private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders;
|
||||
private readonly InvalidAuthProvider _invalidAuthProvider;
|
||||
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
|
||||
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserManager"/> class.
|
||||
|
@ -69,6 +68,13 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
_appHost = appHost;
|
||||
_imageProcessor = imageProcessor;
|
||||
_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/>
|
||||
|
@ -102,7 +108,16 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
}
|
||||
|
||||
/// <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/>
|
||||
public User? GetUserById(Guid id)
|
||||
|
@ -188,8 +203,24 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
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/>
|
||||
public User CreateUser(string name)
|
||||
public async Task<User> CreateUserAsync(string name)
|
||||
{
|
||||
if (!IsValidUsername(name))
|
||||
{
|
||||
|
@ -198,18 +229,10 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
|
||||
using var dbContext = _dbProvider.CreateContext();
|
||||
|
||||
// TODO: Remove after user item data is migrated.
|
||||
var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
|
||||
var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
|
||||
|
||||
var newUser = new User(
|
||||
name,
|
||||
_defaultAuthenticationProvider.GetType().FullName,
|
||||
_defaultPasswordResetProvider.GetType().FullName)
|
||||
{
|
||||
InternalId = max + 1
|
||||
};
|
||||
dbContext.Users.Add(newUser);
|
||||
dbContext.SaveChanges();
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
|
||||
|
||||
|
@ -512,7 +535,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
}
|
||||
else
|
||||
{
|
||||
IncrementInvalidLoginAttemptCount(user);
|
||||
await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
|
||||
_logger.LogInformation(
|
||||
"Authentication request for {UserName} has been denied (IP: {IP}).",
|
||||
user.Username,
|
||||
|
@ -530,7 +553,12 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
if (user != null && isInNetwork)
|
||||
{
|
||||
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
|
||||
|
@ -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 />
|
||||
public void Initialize()
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
||||
using var dbContext = _dbProvider.CreateContext();
|
||||
|
||||
if (dbContext.Users.Any())
|
||||
if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -595,13 +612,13 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
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.EnableContentDeletion, true);
|
||||
newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
|
||||
|
||||
dbContext.Users.Update(newUser);
|
||||
dbContext.SaveChanges();
|
||||
dbContext.Users.Add(newUser);
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -637,7 +654,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
/// <inheritdoc/>
|
||||
public void UpdateConfiguration(Guid userId, UserConfiguration config)
|
||||
{
|
||||
var dbContext = _dbProvider.CreateContext();
|
||||
using var dbContext = _dbProvider.CreateContext();
|
||||
var user = dbContext.Users
|
||||
.Include(u => u.Permissions)
|
||||
.Include(u => u.Preferences)
|
||||
|
@ -670,7 +687,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
/// <inheritdoc/>
|
||||
public void UpdatePolicy(Guid userId, UserPolicy policy)
|
||||
{
|
||||
var dbContext = _dbProvider.CreateContext();
|
||||
using var dbContext = _dbProvider.CreateContext();
|
||||
var user = dbContext.Users
|
||||
.Include(u => u.Permissions)
|
||||
.Include(u => u.Preferences)
|
||||
|
@ -882,7 +899,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
}
|
||||
}
|
||||
|
||||
private void IncrementInvalidLoginAttemptCount(User user)
|
||||
private async Task IncrementInvalidLoginAttemptCount(User user)
|
||||
{
|
||||
user.InvalidLoginAttemptCount++;
|
||||
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
|
||||
|
@ -896,7 +913,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
user.InvalidLoginAttemptCount);
|
||||
}
|
||||
|
||||
UpdateUser(user);
|
||||
await UpdateUserAsync(user).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ using Jellyfin.Server.Implementations;
|
|||
using Jellyfin.Server.Implementations.Activity;
|
||||
using Jellyfin.Server.Implementations.Users;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Activity;
|
||||
|
@ -33,9 +34,9 @@ namespace Jellyfin.Server
|
|||
/// <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>
|
||||
public CoreAppHost(
|
||||
ServerApplicationPaths applicationPaths,
|
||||
IServerApplicationPaths applicationPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
StartupOptions options,
|
||||
IStartupOptions options,
|
||||
IFileSystem fileSystem,
|
||||
INetworkManager networkManager)
|
||||
: base(
|
||||
|
|
|
@ -21,7 +21,8 @@ namespace Jellyfin.Server.Migrations
|
|||
typeof(Routines.MigrateActivityLogDb),
|
||||
typeof(Routines.RemoveDuplicateExtras),
|
||||
typeof(Routines.AddDefaultPluginRepository),
|
||||
typeof(Routines.MigrateUserDb)
|
||||
typeof(Routines.MigrateUserDb),
|
||||
typeof(Routines.ReaddDefaultPluginRepository)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
.UseSerilog()
|
||||
|
|
|
@ -525,7 +525,7 @@ namespace MediaBrowser.Api
|
|||
/// <returns>System.Object.</returns>
|
||||
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
|
||||
if (request.Password != null)
|
||||
|
|
33
MediaBrowser.Common/Extensions/HttpContextExtensions.cs
Normal file
33
MediaBrowser.Common/Extensions/HttpContextExtensions.cs
Normal file
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -199,7 +199,7 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <summary>
|
||||
/// Updates the item.
|
||||
/// </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);
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Events;
|
||||
|
@ -55,7 +54,7 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <summary>
|
||||
/// Initializes the user manager and ensures that a user exists.
|
||||
/// </summary>
|
||||
void Initialize();
|
||||
Task InitializeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a user by Id.
|
||||
|
@ -106,7 +105,7 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <returns>The created user.</returns>
|
||||
/// <exception cref="ArgumentNullException">name</exception>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
User CreateUser(string name);
|
||||
Task<User> CreateUserAsync(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified user.
|
||||
|
@ -166,8 +165,6 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
|
||||
|
||||
void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
|
||||
|
||||
NameIdPair[] GetAuthenticationProviders();
|
||||
|
||||
NameIdPair[] GetPasswordResetProviders();
|
||||
|
|
|
@ -372,7 +372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
public int GetVideoProfileScore(string profile)
|
||||
{
|
||||
// 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));
|
||||
}
|
||||
|
||||
|
@ -460,7 +460,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
if (!IsCopyCodec(outputVideoCodec))
|
||||
{
|
||||
if (state.IsVideoRequest
|
||||
&& IsVaapiSupported(state)
|
||||
&& _mediaEncoder.SupportsHwaccel("vaapi")
|
||||
&& string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (isVaapiDecoder)
|
||||
|
@ -468,13 +468,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
arg.Append("-hwaccel_output_format vaapi ")
|
||||
.Append("-vaapi_device ")
|
||||
.Append(encodingOptions.VaapiDevice)
|
||||
.Append(" ");
|
||||
.Append(' ');
|
||||
}
|
||||
else if (!isVaapiDecoder && isVaapiEncoder)
|
||||
{
|
||||
arg.Append("-vaapi_device ")
|
||||
.Append(encodingOptions.VaapiDevice)
|
||||
.Append(" ");
|
||||
.Append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -495,13 +495,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
}
|
||||
else
|
||||
{
|
||||
arg.Append("-hwaccel qsv -init_hw_device qsv=hw ");
|
||||
arg.Append("-hwaccel qsv ");
|
||||
}
|
||||
}
|
||||
|
||||
if (isWindows)
|
||||
{
|
||||
arg.Append("-hwaccel qsv -init_hw_device qsv=hw ");
|
||||
arg.Append("-hwaccel qsv ");
|
||||
}
|
||||
}
|
||||
// While using SW decoder
|
||||
|
@ -1606,25 +1606,41 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
}
|
||||
else
|
||||
{
|
||||
index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
|
||||
index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
|
||||
if (index != -1)
|
||||
{
|
||||
outputSizeParam = outputSizeParam.Slice(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
|
||||
index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
|
||||
if (index != -1)
|
||||
{
|
||||
outputSizeParam = outputSizeParam.Slice(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
|
||||
index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
|
||||
if (index != -1)
|
||||
{
|
||||
outputSizeParam = outputSizeParam.Slice(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1685,7 +1701,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
}
|
||||
|
||||
// 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))
|
||||
{
|
||||
/*
|
||||
|
@ -1790,6 +1806,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
var outputWidth = width.Value;
|
||||
var outputHeight = height.Value;
|
||||
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
|
||||
|| outputWidth != videoWidth.Value
|
||||
|
@ -1805,7 +1825,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
|
||||
outputWidth,
|
||||
outputHeight,
|
||||
(qsv_or_vaapi && state.DeInterlace("h264", true)) ? ":deinterlace=1" : string.Empty));
|
||||
(qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1814,7 +1834,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
CultureInfo.InvariantCulture,
|
||||
"{0}=format=nv12{1}",
|
||||
qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
|
||||
(qsv_or_vaapi && state.DeInterlace("h264", true)) ? ":deinterlace=1" : string.Empty));
|
||||
(qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
|
||||
}
|
||||
}
|
||||
else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|
@ -2026,7 +2046,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
// http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
|
||||
|
||||
var request = state.BaseRequest;
|
||||
|
||||
var videoStream = state.VideoStream;
|
||||
var filters = new List<string>();
|
||||
|
||||
|
@ -2035,23 +2054,31 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
var inputHeight = videoStream?.Height;
|
||||
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 hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
|
||||
|
||||
// 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("hwupload");
|
||||
}
|
||||
|
||||
// When the input may or may not be hardware QSV decodable
|
||||
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
// When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
|
||||
else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
|
||||
{
|
||||
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
|
||||
else if (IsVaapiSupported(state) && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||
else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
|
||||
{
|
||||
var codec = videoStream.Codec.ToLowerInvariant();
|
||||
var isColorDepth10 = IsColorDepth10(state);
|
||||
|
@ -2079,16 +2106,19 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
}
|
||||
|
||||
// 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"));
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|| state.DeInterlace("avc", true)
|
||||
|| state.DeInterlace("h265", true)
|
||||
|| state.DeInterlace("hevc", true))
|
||||
{
|
||||
string deintParam;
|
||||
var inputFramerate = videoStream?.RealFrameRate;
|
||||
|
@ -2105,9 +2135,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
|
||||
if (!string.IsNullOrEmpty(deintParam))
|
||||
{
|
||||
if (!string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
&& videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder)
|
||||
{
|
||||
filters.Add(deintParam);
|
||||
}
|
||||
|
@ -2117,12 +2145,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
// 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));
|
||||
|
||||
// Add parameters to use VAAPI with burn-in text subttiles (GH issue #642)
|
||||
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
// Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
|
||||
if (isVaapiH264Encoder)
|
||||
{
|
||||
if (state.SubtitleStream != null
|
||||
&& state.SubtitleStream.IsTextSubtitleStream
|
||||
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
|
||||
if (hasTextSubs)
|
||||
{
|
||||
// Test passed on Intel and AMD gfx
|
||||
filters.Add("hwmap=mode=read+write");
|
||||
|
@ -2132,9 +2158,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
|
||||
var output = string.Empty;
|
||||
|
||||
if (state.SubtitleStream != null
|
||||
&& state.SubtitleStream.IsTextSubtitleStream
|
||||
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
|
||||
if (hasTextSubs)
|
||||
{
|
||||
var subParam = GetTextSubtitleParam(state);
|
||||
|
||||
|
@ -2142,7 +2166,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
|
||||
// Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API
|
||||
// Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI
|
||||
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
if (isVaapiH264Encoder)
|
||||
{
|
||||
filters.Add("hwmap");
|
||||
}
|
||||
|
|
|
@ -198,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
internal static Version GetFFmpegVersion(string 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)
|
||||
{
|
||||
|
@ -225,7 +225,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
var rc = new StringBuilder(144);
|
||||
foreach (Match m in Regex.Matches(
|
||||
output,
|
||||
@"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))",
|
||||
@"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))",
|
||||
RegexOptions.Multiline))
|
||||
{
|
||||
rc.Append(m.Groups["name"])
|
||||
|
|
|
@ -174,7 +174,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
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);
|
||||
|
||||
|
@ -428,7 +428,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
|
||||
// FFmpeg automatically convert character encoding when it is UTF-16
|
||||
// If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event"
|
||||
if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) && (encodingParam == "UTF-16BE" || encodingParam == "UTF-16LE"))
|
||||
if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) &&
|
||||
(encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) ||
|
||||
encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
encodingParam = "";
|
||||
}
|
||||
|
|
|
@ -112,107 +112,146 @@ namespace MediaBrowser.Model.Entities
|
|||
{
|
||||
get
|
||||
{
|
||||
if (Type == MediaStreamType.Audio)
|
||||
switch (Type)
|
||||
{
|
||||
// if (!string.IsNullOrEmpty(Title))
|
||||
//{
|
||||
// return AddLanguageIfNeeded(Title);
|
||||
//}
|
||||
|
||||
var attributes = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(Language))
|
||||
case MediaStreamType.Audio:
|
||||
{
|
||||
attributes.Add(StringHelper.FirstToUpper(Language));
|
||||
var attributes = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(Language))
|
||||
{
|
||||
// Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes.
|
||||
string fullLanguage = CultureInfo
|
||||
.GetCultures(CultureTypes.NeutralCultures)
|
||||
.FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
|
||||
?.DisplayName;
|
||||
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
attributes.Add(AudioCodec.GetFriendlyName(Codec));
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
attributes.Add(Profile);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ChannelLayout))
|
||||
{
|
||||
attributes.Add(StringHelper.FirstToUpper(ChannelLayout));
|
||||
}
|
||||
else if (Channels.HasValue)
|
||||
{
|
||||
attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch");
|
||||
}
|
||||
|
||||
if (IsDefault)
|
||||
{
|
||||
attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Title))
|
||||
{
|
||||
var result = new StringBuilder(Title);
|
||||
foreach (var tag in attributes)
|
||||
{
|
||||
// Keep Tags that are not already in Title.
|
||||
if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
result.Append(" - ").Append(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
return string.Join(" - ", attributes);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase))
|
||||
case MediaStreamType.Video:
|
||||
{
|
||||
attributes.Add(AudioCodec.GetFriendlyName(Codec));
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
attributes.Add(Profile);
|
||||
var attributes = new List<string>();
|
||||
|
||||
var resolutionText = GetResolutionText();
|
||||
|
||||
if (!string.IsNullOrEmpty(resolutionText))
|
||||
{
|
||||
attributes.Add(resolutionText);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Codec))
|
||||
{
|
||||
attributes.Add(Codec.ToUpperInvariant());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Title))
|
||||
{
|
||||
var result = new StringBuilder(Title);
|
||||
foreach (var tag in attributes)
|
||||
{
|
||||
// Keep Tags that are not already in Title.
|
||||
if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
result.Append(" - ").Append(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
return string.Join(" ", attributes);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ChannelLayout))
|
||||
case MediaStreamType.Subtitle:
|
||||
{
|
||||
attributes.Add(ChannelLayout);
|
||||
}
|
||||
else if (Channels.HasValue)
|
||||
{
|
||||
attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch");
|
||||
var attributes = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(Language))
|
||||
{
|
||||
// Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes.
|
||||
string fullLanguage = CultureInfo
|
||||
.GetCultures(CultureTypes.NeutralCultures)
|
||||
.FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
|
||||
?.DisplayName;
|
||||
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
||||
}
|
||||
else
|
||||
{
|
||||
attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
|
||||
}
|
||||
|
||||
if (IsDefault)
|
||||
{
|
||||
attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
|
||||
}
|
||||
|
||||
if (IsForced)
|
||||
{
|
||||
attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Title))
|
||||
{
|
||||
var result = new StringBuilder(Title);
|
||||
foreach (var tag in attributes)
|
||||
{
|
||||
// Keep Tags that are not already in Title.
|
||||
if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
result.Append(" - ").Append(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
return string.Join(" - ", attributes.ToArray());
|
||||
}
|
||||
|
||||
if (IsDefault)
|
||||
{
|
||||
attributes.Add("Default");
|
||||
}
|
||||
|
||||
return string.Join(" ", attributes);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Type == MediaStreamType.Video)
|
||||
{
|
||||
var attributes = new List<string>();
|
||||
|
||||
var resolutionText = GetResolutionText();
|
||||
|
||||
if (!string.IsNullOrEmpty(resolutionText))
|
||||
{
|
||||
attributes.Add(resolutionText);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Codec))
|
||||
{
|
||||
attributes.Add(Codec.ToUpperInvariant());
|
||||
}
|
||||
|
||||
return string.Join(" ", attributes);
|
||||
}
|
||||
|
||||
if (Type == MediaStreamType.Subtitle)
|
||||
{
|
||||
|
||||
var attributes = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(Language))
|
||||
{
|
||||
attributes.Add(StringHelper.FirstToUpper(Language));
|
||||
}
|
||||
else
|
||||
{
|
||||
attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
|
||||
}
|
||||
|
||||
if (IsDefault)
|
||||
{
|
||||
attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
|
||||
}
|
||||
|
||||
if (IsForced)
|
||||
{
|
||||
attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Title))
|
||||
{
|
||||
return attributes.AsEnumerable()
|
||||
// keep Tags that are not already in Title
|
||||
.Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
|
||||
// attributes concatenation, starting with Title
|
||||
.Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr))
|
||||
.ToString();
|
||||
}
|
||||
|
||||
return string.Join(" - ", attributes.ToArray());
|
||||
}
|
||||
|
||||
if (Type == MediaStreamType.Video)
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#pragma warning disable CA1819 // Properties should not return arrays
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -9,21 +10,27 @@ namespace MediaBrowser.Model.Notifications
|
|||
public NotificationOption(string type)
|
||||
{
|
||||
Type = type;
|
||||
|
||||
DisabledServices = Array.Empty<string>();
|
||||
DisabledMonitorUsers = Array.Empty<string>();
|
||||
SendToUsers = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public string Type { get; set; }
|
||||
public NotificationOption()
|
||||
{
|
||||
DisabledServices = Array.Empty<string>();
|
||||
DisabledMonitorUsers = Array.Empty<string>();
|
||||
SendToUsers = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User Ids to not monitor (it's opt out).
|
||||
/// Gets or sets user Ids to not monitor (it's opt out).
|
||||
/// </summary>
|
||||
public string[] DisabledMonitorUsers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User Ids to send to (if SendToUserMode == Custom)
|
||||
/// Gets or sets user Ids to send to (if SendToUserMode == Custom).
|
||||
/// </summary>
|
||||
public string[] SendToUsers { get; set; }
|
||||
|
||||
|
|
|
@ -265,7 +265,7 @@ namespace MediaBrowser.Providers.Manager
|
|||
}
|
||||
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>();
|
||||
}
|
||||
}
|
||||
|
@ -430,7 +430,7 @@ namespace MediaBrowser.Providers.Manager
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,29 +28,31 @@
|
|||
pluginId: "a629c0da-fac5-4c7e-931a-7174223f14c8"
|
||||
};
|
||||
|
||||
$('.configPage').on('pageshow', function () {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
$('#enable').checked = config.Enable;
|
||||
$('#replaceAlbumName').checked = config.ReplaceAlbumName;
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
|
||||
$('.configForm').on('submit', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
var form = this;
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
config.Enable = $('#enable', form).checked;
|
||||
config.ReplaceAlbumName = $('#replaceAlbumName', form).checked;
|
||||
|
||||
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
document.querySelector('.configPage')
|
||||
.addEventListener('pageshow', function () {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
document.querySelector('#enable').checked = config.Enable;
|
||||
document.querySelector('#replaceAlbumName').checked = config.ReplaceAlbumName;
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
document.querySelector('.configForm')
|
||||
.addEventListener('submit', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
config.Enable = document.querySelector('#enable').checked;
|
||||
config.ReplaceAlbumName = document.querySelector('#replaceAlbumName').checked;
|
||||
|
||||
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -36,33 +36,47 @@
|
|||
uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"
|
||||
};
|
||||
|
||||
$('.musicBrainzConfigPage').on('pageshow', function () {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
|
||||
$('#server').val(config.Server).change();
|
||||
$('#rateLimit').val(config.RateLimit).change();
|
||||
$('#enable').checked = config.Enable;
|
||||
$('#replaceArtistName').checked = config.ReplaceArtistName;
|
||||
document.querySelector('.musicBrainzConfigPage')
|
||||
.addEventListener('pageshow', function () {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
|
||||
var server = document.querySelector('#server');
|
||||
server.value = config.Server;
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('.musicBrainzConfigForm').on('submit', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
var form = this;
|
||||
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
|
||||
config.Server = $('#server', form).val();
|
||||
config.RateLimit = $('#rateLimit', form).val();
|
||||
config.Enable = $('#enable', form).checked;
|
||||
config.ReplaceArtistName = $('#replaceArtistName', form).checked;
|
||||
|
||||
ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
|
||||
document.querySelector('.musicBrainzConfigForm')
|
||||
.addEventListener('submit', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
|
||||
config.Server = document.querySelector('#server').value;
|
||||
config.RateLimit = document.querySelector('#rateLimit').value;
|
||||
config.Enable = document.querySelector('#enable').checked;
|
||||
config.ReplaceArtistName = document.querySelector('#replaceArtistName').checked;
|
||||
|
||||
ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -24,25 +24,28 @@
|
|||
pluginId: "a628c0da-fac5-4c7e-9d1a-7134223f14c8"
|
||||
};
|
||||
|
||||
$('.configPage').on('pageshow', function () {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
$('#castAndCrew').checked = config.CastAndCrew;
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
|
||||
$('.configForm').on('submit', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
var form = this;
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
config.CastAndCrew = $('#castAndCrew', form).checked;
|
||||
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
document.querySelector('.configPage')
|
||||
.addEventListener('pageshow', function () {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
document.querySelector('#castAndCrew').checked = config.CastAndCrew;
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
document.querySelector('.configForm')
|
||||
.addEventListener('submit', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
config.CastAndCrew = document.querySelector('#castAndCrew').checked;
|
||||
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.TV
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error in DummySeasonProvider");
|
||||
Logger.LogError(ex, "Error in DummySeasonProvider for {ItemPath}", item.Path);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
4
debian/conf/jellyfin
vendored
4
debian/conf/jellyfin
vendored
|
@ -31,7 +31,7 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
|
|||
#JELLYFIN_SERVICE_OPT="--service"
|
||||
|
||||
# [OPTIONAL] run Jellyfin without the web app
|
||||
#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp"
|
||||
#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
|
||||
|
||||
#
|
||||
# SysV init/Upstart options
|
||||
|
@ -40,4 +40,4 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
|
|||
# Application username
|
||||
JELLYFIN_USER="jellyfin"
|
||||
# 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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user