Merge branch 'master' into displaypreferences-efcore

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

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

@ -11,6 +11,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.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[]
{
"<",
"&lt;",
">",
"&gt;",
"\"",
"&quot;",
"'",
"&apos;",
"&",
"&amp;"
};
private static string GetEscapeSequence(char c)
{
int num = s_escapeStringPairs.Length;
for (int i = 0; i < num; i += 2)
{
string text = s_escapeStringPairs[i];
string result = s_escapeStringPairs[i + 1];
if (text[0] == c)
{
return result;
}
}
return c.ToString(CultureInfo.InvariantCulture);
}
/// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
/// <returns>The input string with invalid characters replaced.</returns>
/// <param name="str">The string within which to escape invalid characters. </param>
public static string Escape(string str)
{
if (str == null)
{
return null;
}
StringBuilder stringBuilder = null;
int length = str.Length;
int num = 0;
while (true)
{
int num2 = str.IndexOfAny(s_escapeChars, num);
if (num2 == -1)
{
break;
}
if (stringBuilder == null)
{
stringBuilder = new StringBuilder();
}
stringBuilder.Append(str, num, num2 - num);
stringBuilder.Append(GetEscapeSequence(str[num2]));
num = num2 + 1;
}
if (stringBuilder == null)
{
return str;
}
stringBuilder.Append(str, num, length - num);
return stringBuilder.ToString();
}
private void AppendDeviceProperties(StringBuilder builder)
{
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("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
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>")
.Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
.Append("</modelDescription>");
builder.Append("<modelName>")
.Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
.Append("</modelName>");
builder.Append("<modelNumber>")
.Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
.Append("</modelNumber>");
builder.Append("<modelURL>")
.Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
.Append("</modelURL>");
if (string.IsNullOrEmpty(_profile.SerialNumber))
{
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,
@ -792,7 +792,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>());
}

@ -1111,7 +1111,8 @@ namespace Emby.Server.Implementations.Data
continue;
}
str.Append(ToValueString(i) + "|");
str.Append(ToValueString(i))
.Append('|');
}
str.Length -= 1; // Remove last |
@ -2472,7 +2473,7 @@ namespace Emby.Server.Implementations.Data
var item = query.SimilarTo;
var builder = new StringBuilder();
builder.Append("(");
builder.Append('(');
if (string.IsNullOrEmpty(item.OfficialRating))
{
@ -2510,7 +2511,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)");
@ -5239,7 +5240,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));
@ -6332,7 +6333,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;

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

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

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

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

@ -59,6 +59,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;
@ -74,63 +76,24 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
private readonly IImageProcessor _imageProcessor;
private NamingOptions _namingOptions;
private string[] _videoFileExtensions;
private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
private IProviderManager ProviderManager => _providerManagerFactory.Value;
private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
/// <summary>
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
private ILibraryPostScanTask[] PostscanTasks { get; set; }
/// <summary>
/// Gets or sets the intro providers.
/// </summary>
/// <value>The intro providers.</value>
private IIntroProvider[] IntroProviders { get; set; }
/// <summary>
/// Gets or sets the list of entity resolution ignore rules.
/// </summary>
/// <value>The entity resolution ignore rules.</value>
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
/// <summary>
/// Gets or sets the list of currently registered entity resolvers.
/// </summary>
/// <value>The entity resolvers enumerable.</value>
private IItemResolver[] EntityResolvers { get; set; }
private IMultiItemResolver[] MultiItemResolvers { get; set; }
/// <summary>
/// Gets or sets the comparers.
/// The _root folder sync lock.
/// </summary>
/// <value>The comparers.</value>
private IBaseItemComparer[] Comparers { get; set; }
private readonly object _rootFolderSyncLock = new object();
private readonly object _userRootFolderSyncLock = new object();
/// <summary>
/// Occurs when [item added].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemAdded;
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
/// <summary>
/// Occurs when [item updated].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemUpdated;
private NamingOptions _namingOptions;
private string[] _videoFileExtensions;
/// <summary>
/// Occurs when [item removed].
/// The _root folder.
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemRemoved;
private volatile AggregateFolder _rootFolder;
private volatile UserRootFolder _userRootFolder;
public bool IsScanRunning { get; private set; }
private bool _wizardCompleted;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
@ -185,37 +148,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.
@ -240,7 +185,68 @@ namespace Emby.Server.Implementations.Library
}
}
private bool _wizardCompleted;
private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
private IProviderManager ProviderManager => _providerManagerFactory.Value;
private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
/// <summary>
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
private ILibraryPostScanTask[] PostscanTasks { get; set; }
/// <summary>
/// Gets or sets the intro providers.
/// </summary>
/// <value>The intro providers.</value>
private IIntroProvider[] IntroProviders { get; set; }
/// <summary>
/// Gets or sets the list of entity resolution ignore rules.
/// </summary>
/// <value>The entity resolution ignore rules.</value>
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
/// <summary>
/// Gets or sets the list of currently registered entity resolvers.
/// </summary>
/// <value>The entity resolvers enumerable.</value>
private IItemResolver[] EntityResolvers { get; set; }
private IMultiItemResolver[] MultiItemResolvers { get; set; }
/// <summary>
/// Gets or sets the comparers.
/// </summary>
/// <value>The comparers.</value>
private IBaseItemComparer[] Comparers { get; set; }
public bool IsScanRunning { get; private set; }
/// <summary>
/// Adds the parts.
/// </summary>
/// <param name="rules">The rules.</param>
/// <param name="resolvers">The resolvers.</param>
/// <param name="introProviders">The intro providers.</param>
/// <param name="itemComparers">The item comparers.</param>
/// <param name="postscanTasks">The post scan tasks.</param>
public void AddParts(
IEnumerable<IResolverIgnoreRule> rules,
IEnumerable<IItemResolver> resolvers,
IEnumerable<IIntroProvider> introProviders,
IEnumerable<IBaseItemComparer> itemComparers,
IEnumerable<ILibraryPostScanTask> postscanTasks)
{
EntityResolutionIgnoreRules = rules.ToArray();
EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
IntroProviders = introProviders.ToArray();
Comparers = itemComparers.ToArray();
PostscanTasks = postscanTasks.ToArray();
}
/// <summary>
/// Records the configuration values.
@ -340,7 +346,7 @@ namespace Emby.Server.Implementations.Library
if (item is LiveTvProgram)
{
_logger.LogDebug(
"Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
"Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@ -349,7 +355,7 @@ namespace Emby.Server.Implementations.Library
else
{
_logger.LogInformation(
"Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
"Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@ -367,7 +373,12 @@ namespace Emby.Server.Implementations.Library
continue;
}
_logger.LogDebug("Deleting path {MetadataPath}", metadataPath);
_logger.LogDebug(
"Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
metadataPath,
item.Id);
try
{
@ -391,7 +402,13 @@ namespace Emby.Server.Implementations.Library
{
try
{
_logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName);
_logger.LogInformation(
"Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
item.Id);
if (fileSystemInfo.IsDirectory)
{
Directory.Delete(fileSystemInfo.FullName, true);
@ -500,7 +517,7 @@ namespace Emby.Server.Implementations.Library
// Try to normalize paths located underneath program-data in an attempt to make them more portable
key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
.TrimStart(new[] { '/', '\\' })
.Replace("/", "\\");
.Replace('/', '\\');
}
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@ -763,14 +780,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)
{
@ -1320,7 +1334,7 @@ namespace Emby.Server.Implementations.Library
return new QueryResult<BaseItem>
{
Items = _itemRepository.GetItemList(query).ToArray()
Items = _itemRepository.GetItemList(query)
};
}
@ -1451,11 +1465,9 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItems(query);
}
var list = _itemRepository.GetItemList(query);
return new QueryResult<BaseItem>
{
Items = list
Items = _itemRepository.GetItemList(query)
};
}
@ -1864,7 +1876,8 @@ namespace Emby.Server.Implementations.Library
}
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
if (outdated.Length == 0)
// Skip image processing if current or live tv source
if (outdated.Length == 0 || item.SourceType != SourceType.Library)
{
RegisterItem(item);
return;
@ -1933,12 +1946,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)
{
@ -1950,11 +1960,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)
@ -2177,8 +2187,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,
@ -2476,14 +2484,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
{
@ -2491,11 +2494,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;
@ -2653,7 +2658,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)
{
@ -2670,9 +2675,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;
}
@ -2999,23 +3002,6 @@ namespace Emby.Server.Implementations.Library
});
}
private static bool ValidateNetworkPath(string path)
{
// if (Environment.OSVersion.Platform == PlatformID.Win32NT)
//{
// // We can't validate protocol-based paths, so just allow them
// if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
// {
// return Directory.Exists(path);
// }
//}
// Without native support for unc, we cannot validate this when running under mono
return true;
}
private const string ShortcutFileExtension = ".mblink";
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
{
AddMediaPathInternal(virtualFolderName, pathInfo, true);
@ -3040,11 +3026,6 @@ namespace Emby.Server.Implementations.Library
throw new FileNotFoundException("The path does not exist.");
}
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new FileNotFoundException("The network path does not exist.");
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@ -3083,11 +3064,6 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(pathInfo));
}
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new FileNotFoundException("The network path does not exist.");
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@ -3219,7 +3195,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))
@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
public void Dispose()
{
Dispose(true);
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)
{

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

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

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

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

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

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

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

@ -6,6 +6,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;
@ -78,7 +79,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();

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

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

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

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

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

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

@ -1,5 +1,6 @@
#pragma warning disable CS1591
using System;
using System.Linq;
using Jellyfin.Data;
using Jellyfin.Data.Entities;
@ -135,6 +136,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)
@ -152,12 +167,12 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(newName));
}
if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))
if (user.Username.Equals(newName, StringComparison.Ordinal))
{
throw new ArgumentException("The new and old names must be different.");
}
if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)))
if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.Ordinal)))
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
@ -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,8 +687,14 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void UpdatePolicy(Guid userId, UserPolicy policy)
{
var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
using var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users
.Include(u => u.Permissions)
.Include(u => u.Preferences)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id == userId)
?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
@ -876,7 +899,7 @@ namespace Jellyfin.Server.Implementations.Users
}
}
private void IncrementInvalidLoginAttemptCount(User user)
private async Task IncrementInvalidLoginAttemptCount(User user)
{
user.InvalidLoginAttemptCount++;
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
@ -890,7 +913,7 @@ namespace Jellyfin.Server.Implementations.Users
user.InvalidLoginAttemptCount);
}
UpdateUser(user);
await UpdateUserAsync(user).ConfigureAwait(false);
}
}
}

@ -34,9 +34,9 @@ namespace Jellyfin.Server
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="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(

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -198,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
internal static Version GetFFmpegVersion(string output)
{
// 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"])

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

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

@ -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 NotificationOption()
{
DisabledServices = Array.Empty<string>();
DisabledMonitorUsers = Array.Empty<string>();
SendToUsers = Array.Empty<string>();
}
public string Type { get; set; }
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; }

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

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

@ -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();
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();
});
});
});
$('.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('.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;
});
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();
document.querySelector('.configPage')
.addEventListener('pageshow', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
document.querySelector('#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('.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;
});
return false;
});
</script>
</div>
</body>

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

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

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

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

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

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

6
debian/changelog vendored

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

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

5
debian/control vendored

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

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

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

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

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

Loading…
Cancel
Save