diff options
80 files changed, 1295 insertions, 953 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 5539b16ab..272111299 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -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' diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs index 7fba2184a..d9c1669b0 100644 --- a/Emby.Dlna/Api/DlnaServerService.cs +++ b/Emby.Dlna/Api/DlnaServerService.cs @@ -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) diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 66baa9512..70e358019 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -364,7 +364,8 @@ namespace Emby.Dlna.Didl writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture)); } - var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container, + var mediaProfile = _profile.GetVideoMediaProfile( + streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetAudioBitrate, diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 5911a73ef..1e20ff92b 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -387,7 +387,7 @@ namespace Emby.Dlna foreach (var name in _assembly.GetManifestResourceNames()) { - if (!name.StartsWith(namespaceName)) + if (!name.StartsWith(namespaceName, StringComparison.Ordinal)) { continue; } @@ -406,7 +406,7 @@ namespace Emby.Dlna using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) { - await stream.CopyToAsync(fileStream); + await stream.CopyToAsync(fileStream).ConfigureAwait(false); } } } @@ -509,7 +509,7 @@ namespace Emby.Dlna return _jsonSerializer.DeserializeFromString<DeviceProfile>(json); } - class InternalProfileInfo + private class InternalProfileInfo { internal DeviceProfileInfo Info { get; set; } diff --git a/Emby.Dlna/Eventing/EventManager.cs b/Emby.Dlna/Eventing/EventManager.cs index 56c90c8b3..7d02f5e96 100644 --- a/Emby.Dlna/Eventing/EventManager.cs +++ b/Emby.Dlna/Eventing/EventManager.cs @@ -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>"); diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index c5080b90f..72834c69d 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -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) diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 7143c3109..bca9e81cd 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Security; using System.Text; using Emby.Dlna.Common; using MediaBrowser.Model.Dlna; @@ -64,10 +65,10 @@ namespace Emby.Dlna.Server foreach (var att in attributes) { - builder.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value); + builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value); } - builder.Append(">"); + builder.Append('>'); builder.Append("<specVersion>"); builder.Append("<major>1</major>"); @@ -76,7 +77,9 @@ namespace Emby.Dlna.Server if (!EnableAbsoluteUrls) { - builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>"); + builder.Append("<URLBase>") + .Append(SecurityElement.Escape(_serverAddress)) + .Append("</URLBase>"); } AppendDeviceInfo(builder); @@ -93,91 +96,14 @@ namespace Emby.Dlna.Server AppendIconList(builder); - builder.Append("<presentationURL>" + Escape(_serverAddress) + "/web/index.html</presentationURL>"); + builder.Append("<presentationURL>") + .Append(SecurityElement.Escape(_serverAddress)) + .Append("/web/index.html</presentationURL>"); AppendServiceList(builder); builder.Append("</device>"); } - private static readonly char[] s_escapeChars = new char[] - { - '<', - '>', - '"', - '\'', - '&' - }; - - private static readonly string[] s_escapeStringPairs = new[] - { - "<", - "<", - ">", - ">", - "\"", - """, - "'", - "'", - "&", - "&" - }; - - private static string GetEscapeSequence(char c) - { - int num = s_escapeStringPairs.Length; - for (int i = 0; i < num; i += 2) - { - string text = s_escapeStringPairs[i]; - string result = s_escapeStringPairs[i + 1]; - if (text[0] == c) - { - return result; - } - } - - return c.ToString(CultureInfo.InvariantCulture); - } - - /// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary> - /// <returns>The input string with invalid characters replaced.</returns> - /// <param name="str">The string within which to escape invalid characters. </param> - public static string Escape(string str) - { - if (str == null) - { - return null; - } - - StringBuilder stringBuilder = null; - int length = str.Length; - int num = 0; - while (true) - { - int num2 = str.IndexOfAny(s_escapeChars, num); - if (num2 == -1) - { - break; - } - - if (stringBuilder == null) - { - stringBuilder = new StringBuilder(); - } - - stringBuilder.Append(str, num, num2 - num); - stringBuilder.Append(GetEscapeSequence(str[num2])); - num = num2 + 1; - } - - if (stringBuilder == null) - { - return str; - } - - stringBuilder.Append(str, num, length - num); - return stringBuilder.ToString(); - } - private void AppendDeviceProperties(StringBuilder builder) { builder.Append("<dlna:X_DLNACAP/>"); @@ -187,32 +113,54 @@ namespace Emby.Dlna.Server builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>"); - builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>"); - builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>"); - builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>"); - - builder.Append("<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() diff --git a/Emby.Dlna/Service/ServiceXmlBuilder.cs b/Emby.Dlna/Service/ServiceXmlBuilder.cs index af557aa14..6c7d6f846 100644 --- a/Emby.Dlna/Service/ServiceXmlBuilder.cs +++ b/Emby.Dlna/Service/ServiceXmlBuilder.cs @@ -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>"); diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 8696cb280..f585b90ca 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -448,21 +448,21 @@ namespace Emby.Drawing /// or /// filename. /// </exception> - public string GetCachePath(string path, string filename) + public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename) { - if (string.IsNullOrEmpty(path)) + if (path.IsEmpty) { - throw new ArgumentNullException(nameof(path)); + throw new ArgumentException("Path can't be empty.", nameof(path)); } - if (string.IsNullOrEmpty(filename)) + if (path.IsEmpty) { - throw new ArgumentNullException(nameof(filename)); + throw new ArgumentException("Filename can't be empty.", nameof(filename)); } - var prefix = filename.Substring(0, 1); + var prefix = filename.Slice(0, 1); - return Path.Combine(path, prefix, filename); + return Path.Join(path, prefix, filename); } /// <inheritdoc /> diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 1b343790e..d1e17f416 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -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 diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index 2fa6b4353..d2e324dda 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -77,7 +77,7 @@ namespace Emby.Naming.TV if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase)) { - var testFilename = filename.Substring(1); + var testFilename = filename.AsSpan().Slice(1); if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f6077400d..45d1bb01c 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -192,7 +192,7 @@ namespace Emby.Server.Implementations /// Gets or sets the application paths. /// </summary> /// <value>The application paths.</value> - protected ServerApplicationPaths ApplicationPaths { get; set; } + protected IServerApplicationPaths ApplicationPaths { get; set; } /// <summary> /// Gets or sets all concrete types. @@ -236,7 +236,7 @@ namespace Emby.Server.Implementations /// Initializes a new instance of the <see cref="ApplicationHost" /> class. /// </summary> public ApplicationHost( - ServerApplicationPaths applicationPaths, + IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, @@ -795,7 +795,6 @@ namespace Emby.Server.Implementations Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); - Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>()); Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>()); } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index a6390b1ef..9f5566424 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1110,7 +1110,8 @@ namespace Emby.Server.Implementations.Data continue; } - str.Append(ToValueString(i) + "|"); + str.Append(ToValueString(i)) + .Append('|'); } str.Length -= 1; // Remove last | @@ -2471,7 +2472,7 @@ namespace Emby.Server.Implementations.Data var item = query.SimilarTo; var builder = new StringBuilder(); - builder.Append("("); + builder.Append('('); if (string.IsNullOrEmpty(item.OfficialRating)) { @@ -2509,7 +2510,7 @@ namespace Emby.Server.Implementations.Data if (!string.IsNullOrEmpty(query.SearchTerm)) { var builder = new StringBuilder(); - builder.Append("("); + builder.Append('('); builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)"); @@ -5238,7 +5239,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { if (i > 0) { - insertText.Append(","); + insertText.Append(','); } insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture)); @@ -6331,7 +6332,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type foreach (var column in _mediaAttachmentSaveColumns.Skip(1)) { - insertText.Append("@" + column + index + ","); + insertText.Append('@') + .Append(column) + .Append(index) + .Append(','); } insertText.Length -= 1; diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 590eee1b4..6fce8de44 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -29,7 +29,6 @@ namespace Emby.Server.Implementations.HttpServer private readonly IStreamHelper _streamHelper; private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; /// <summary> /// The _options. @@ -49,7 +48,6 @@ namespace Emby.Server.Implementations.HttpServer } _streamHelper = streamHelper; - _fileSystem = fileSystem; Path = path; _logger = logger; diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 318bc6a24..c9f802a51 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -13,26 +13,22 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.HttpServer.Security { public class AuthService : IAuthService { - private readonly ILogger<AuthService> _logger; private readonly IAuthorizationContext _authorizationContext; private readonly ISessionManager _sessionManager; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; public AuthService( - ILogger<AuthService> logger, IAuthorizationContext authorizationContext, IServerConfigurationManager config, ISessionManager sessionManager, INetworkManager networkManager) { - _logger = logger; _authorizationContext = authorizationContext; _config = config; _sessionManager = sessionManager; diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs index 52896720e..bf57382ed 100644 --- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs +++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs @@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -25,14 +24,9 @@ namespace Emby.Server.Implementations.Images /// </summary> public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist> { - /// <summary> - /// The library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; - - public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) + : base(fileSystem, providerManager, applicationPaths, imageProcessor) { - _libraryManager = libraryManager; } /// <summary> diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 77b2c0a69..3380e29d4 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -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; diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs index ab39a7223..236453e80 100644 --- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs +++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs @@ -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() { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index c27b73c74..9165ede35 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -60,6 +60,8 @@ namespace Emby.Server.Implementations.Library /// </summary> public class LibraryManager : ILibraryManager { + private const string ShortcutFileExtension = ".mblink"; + private readonly ILogger<LibraryManager> _logger; private readonly ITaskManager _taskManager; private readonly IUserManager _userManager; @@ -75,63 +77,24 @@ namespace Emby.Server.Implementations.Library private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache; private readonly IImageProcessor _imageProcessor; - 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. @@ -186,37 +149,19 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Adds the parts. + /// Occurs when [item added]. /// </summary> - /// <param name="rules">The rules.</param> - /// <param name="resolvers">The resolvers.</param> - /// <param name="introProviders">The intro providers.</param> - /// <param name="itemComparers">The item comparers.</param> - /// <param name="postscanTasks">The post scan tasks.</param> - public void AddParts( - IEnumerable<IResolverIgnoreRule> rules, - IEnumerable<IItemResolver> resolvers, - IEnumerable<IIntroProvider> introProviders, - IEnumerable<IBaseItemComparer> itemComparers, - IEnumerable<ILibraryPostScanTask> postscanTasks) - { - EntityResolutionIgnoreRules = rules.ToArray(); - EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); - MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray(); - IntroProviders = introProviders.ToArray(); - Comparers = itemComparers.ToArray(); - PostscanTasks = postscanTasks.ToArray(); - } + public event EventHandler<ItemChangeEventArgs> ItemAdded; /// <summary> - /// The _root folder. + /// Occurs when [item updated]. /// </summary> - private volatile AggregateFolder _rootFolder; + public event EventHandler<ItemChangeEventArgs> ItemUpdated; /// <summary> - /// The _root folder sync lock. + /// Occurs when [item removed]. /// </summary> - private readonly object _rootFolderSyncLock = new object(); + public event EventHandler<ItemChangeEventArgs> ItemRemoved; /// <summary> /// Gets the root folder. @@ -241,7 +186,68 @@ namespace Emby.Server.Implementations.Library } } - private bool _wizardCompleted; + private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value; + + private IProviderManager ProviderManager => _providerManagerFactory.Value; + + private IUserViewManager UserViewManager => _userviewManagerFactory.Value; + + /// <summary> + /// Gets or sets the postscan tasks. + /// </summary> + /// <value>The postscan tasks.</value> + private ILibraryPostScanTask[] PostscanTasks { get; set; } + + /// <summary> + /// Gets or sets the intro providers. + /// </summary> + /// <value>The intro providers.</value> + private IIntroProvider[] IntroProviders { get; set; } + + /// <summary> + /// Gets or sets the list of entity resolution ignore rules. + /// </summary> + /// <value>The entity resolution ignore rules.</value> + private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } + + /// <summary> + /// Gets or sets the list of currently registered entity resolvers. + /// </summary> + /// <value>The entity resolvers enumerable.</value> + private IItemResolver[] EntityResolvers { get; set; } + + private IMultiItemResolver[] MultiItemResolvers { get; set; } + + /// <summary> + /// Gets or sets the comparers. + /// </summary> + /// <value>The comparers.</value> + private IBaseItemComparer[] Comparers { get; set; } + + public bool IsScanRunning { get; private set; } + + /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="rules">The rules.</param> + /// <param name="resolvers">The resolvers.</param> + /// <param name="introProviders">The intro providers.</param> + /// <param name="itemComparers">The item comparers.</param> + /// <param name="postscanTasks">The post scan tasks.</param> + public void AddParts( + IEnumerable<IResolverIgnoreRule> rules, + IEnumerable<IItemResolver> resolvers, + IEnumerable<IIntroProvider> introProviders, + IEnumerable<IBaseItemComparer> itemComparers, + IEnumerable<ILibraryPostScanTask> postscanTasks) + { + EntityResolutionIgnoreRules = rules.ToArray(); + EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); + MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray(); + IntroProviders = introProviders.ToArray(); + Comparers = itemComparers.ToArray(); + PostscanTasks = postscanTasks.ToArray(); + } /// <summary> /// Records the configuration values. @@ -511,8 +517,8 @@ 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("/", "\\"); + .TrimStart('/', '\\') + .Replace('/', '\\'); } if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds) @@ -775,14 +781,11 @@ namespace Emby.Server.Implementations.Library return rootFolder; } - private volatile UserRootFolder _userRootFolder; - private readonly object _syncLock = new object(); - public Folder GetUserRootFolder() { if (_userRootFolder == null) { - lock (_syncLock) + lock (_userRootFolderSyncLock) { if (_userRootFolder == null) { @@ -1332,7 +1335,7 @@ namespace Emby.Server.Implementations.Library return new QueryResult<BaseItem> { - Items = _itemRepository.GetItemList(query).ToArray() + Items = _itemRepository.GetItemList(query) }; } @@ -1463,11 +1466,9 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetItems(query); } - var list = _itemRepository.GetItemList(query); - return new QueryResult<BaseItem> { - Items = list + Items = _itemRepository.GetItemList(query) }; } @@ -1876,7 +1877,8 @@ namespace Emby.Server.Implementations.Library } var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray(); - if (outdated.Length == 0) + // Skip image processing if current or live tv source + if (outdated.Length == 0 || item.SourceType != SourceType.Library) { RegisterItem(item); return; @@ -1945,12 +1947,9 @@ namespace Emby.Server.Implementations.Library /// <summary> /// Updates the item. /// </summary> - public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - // Don't iterate multiple times - var itemsList = items.ToList(); - - foreach (var item in itemsList) + foreach (var item in items) { if (item.IsFileProtocol) { @@ -1962,11 +1961,11 @@ namespace Emby.Server.Implementations.Library UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate); } - _itemRepository.SaveItems(itemsList, cancellationToken); + _itemRepository.SaveItems(items, cancellationToken); if (ItemUpdated != null) { - foreach (var item in itemsList) + foreach (var item in items) { // With the live tv guide this just creates too much noise if (item.SourceType != SourceType.Library) @@ -2189,8 +2188,6 @@ namespace Emby.Server.Implementations.Library .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } - private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); - public UserView GetNamedView( User user, string name, @@ -2488,14 +2485,9 @@ namespace Emby.Server.Implementations.Library var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; - var episodeInfo = episode.IsFileProtocol ? - resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) : - new Naming.TV.EpisodeInfo(); - - if (episodeInfo == null) - { - episodeInfo = new Naming.TV.EpisodeInfo(); - } + var episodeInfo = episode.IsFileProtocol + ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo() + : new Naming.TV.EpisodeInfo(); try { @@ -2503,11 +2495,13 @@ namespace Emby.Server.Implementations.Library if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase)) { // Read from metadata - var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest - { - MediaSource = episode.GetMediaSources(false)[0], - MediaType = DlnaProfileType.Video - }, CancellationToken.None).GetAwaiter().GetResult(); + var mediaInfo = _mediaEncoder.GetMediaInfo( + new MediaInfoRequest + { + MediaSource = episode.GetMediaSources(false)[0], + MediaType = DlnaProfileType.Video + }, + CancellationToken.None).GetAwaiter().GetResult(); if (mediaInfo.ParentIndexNumber > 0) { episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber; @@ -2665,7 +2659,7 @@ namespace Emby.Server.Implementations.Library var videos = videoListResolver.Resolve(fileSystemChildren); - var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase)); + var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase)); if (currentVideo != null) { @@ -2682,9 +2676,7 @@ namespace Emby.Server.Implementations.Library .Select(video => { // Try to retrieve it from the db. If we don't find it, use the resolved version - var dbItem = GetItemById(video.Id) as Trailer; - - if (dbItem != null) + if (GetItemById(video.Id) is Trailer dbItem) { video = dbItem; } @@ -3011,8 +3003,6 @@ namespace Emby.Server.Implementations.Library }); } - private const string ShortcutFileExtension = ".mblink"; - public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) { AddMediaPathInternal(virtualFolderName, pathInfo, true); @@ -3206,7 +3196,8 @@ namespace Emby.Server.Implementations.Library if (!Directory.Exists(virtualFolderPath)) { - throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName)); + throw new FileNotFoundException( + string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName)); } var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index 9b9f53049..041619d1e 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -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) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index ceb36b389..bd59ee0e4 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -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)) @@ -869,7 +870,7 @@ namespace Emby.Server.Implementations.Library var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase)); - var splitIndex = key.IndexOf(LiveStreamIdDelimeter); + var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal); var keyId = key.Substring(splitIndex + 1); return new Tuple<IMediaSourceProvider, string>(provider, keyId); @@ -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> diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index ca904c4ec..179e0ed98 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -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) { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index c8e41001a..3332e1806 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -1,6 +1,5 @@ using System.Globalization; using Emby.Naming.TV; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; @@ -13,7 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// </summary> public class SeasonResolver : FolderResolver<Season> { - private readonly IServerConfigurationManager _config; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly ILogger<SeasonResolver> _logger; @@ -21,17 +19,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <summary> /// Initializes a new instance of the <see cref="SeasonResolver"/> class. /// </summary> - /// <param name="config">The config.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="localization">The localization.</param> /// <param name="logger">The logger.</param> public SeasonResolver( - IServerConfigurationManager config, ILibraryManager libraryManager, ILocalizationManager localization, ILogger<SeasonResolver> logger) { - _config = config; _libraryManager = libraryManager; _localization = localization; _logger = logger; diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 3df9cc06f..d67c9e542 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -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(); diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 175b3af57..f9e5e6bbc 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using Microsoft.Extensions.Logging; using Book = MediaBrowser.Controller.Entities.Book; namespace Emby.Server.Implementations.Library @@ -28,18 +27,15 @@ namespace Emby.Server.Implementations.Library private readonly ConcurrentDictionary<string, UserItemData> _userData = new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase); - private readonly ILogger<UserDataManager> _logger; private readonly IServerConfigurationManager _config; private readonly IUserManager _userManager; private readonly IUserDataRepository _repository; public UserDataManager( - ILogger<UserDataManager> logger, IServerConfigurationManager config, IUserManager userManager, IUserDataRepository repository) { - _logger = logger; _config = config; _userManager = userManager; _repository = repository; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 3709f8fe4..77a7069eb 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -461,7 +461,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms( ListingsProviderInfo info, List<string> programIds, - CancellationToken cancellationToken) + CancellationToken cancellationToken) { if (programIds.Count == 0) { @@ -474,7 +474,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { var imageId = i.Substring(0, 10); - if (!imageIdString.Contains(imageId)) + if (!imageIdString.Contains(imageId, StringComparison.Ordinal)) { imageIdString += "\"" + imageId + "\","; } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 4c1de3bcc..1b075d86a 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -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)); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 2e2488e6e..00420bd2a 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -195,7 +195,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun while (!sr.EndOfStream) { string line = StripXML(sr.ReadLine()); - if (line.Contains("Channel")) + if (line.Contains("Channel", StringComparison.Ordinal)) { LiveTvTunerStatus status; var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); @@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private static string StripXML(string source) { + if (string.IsNullOrEmpty(source)) + { + return string.Empty; + } + char[] buffer = new char[source.Length]; int bufferIndex = 0; bool inside = false; @@ -270,7 +275,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun for (int i = 0; i < model.TunerCount; ++i) { - var name = string.Format("Tuner {0}", i + 1); + var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1); var currentChannel = "none"; // @todo Get current channel and map back to Station Id var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false); var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index c798c0a85..875977219 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -158,15 +158,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl) { var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null; + var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty; string numberString = null; string attributeValue; - double doubleValue; if (attributes.TryGetValue("tvg-chno", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) + if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } @@ -176,36 +175,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { if (attributes.TryGetValue("tvg-id", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) + if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } else if (attributes.TryGetValue("channel-id", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) + if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } } } - if (String.IsNullOrWhiteSpace(numberString)) + if (string.IsNullOrWhiteSpace(numberString)) { // Using this as a fallback now as this leads to Problems with channels like "5 USA" // where 5 isnt ment to be the channel number // Check for channel number with the format from SatIp // #EXTINF:0,84. VOX Schweiz // #EXTINF:0,84.0 - VOX Schweiz - if (!string.IsNullOrWhiteSpace(nameInExtInf)) + if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace()) { var numberIndex = nameInExtInf.IndexOf(' '); if (numberIndex > 0) { - var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); + var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) + if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { - numberString = numberPart; + numberString = numberPart.ToString(); } } } @@ -231,7 +230,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { try { - numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last()); + numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]); if (!IsValidChannelNumber(numberString)) { @@ -258,7 +257,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return false; } - if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) + if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { return false; } @@ -281,7 +280,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) + if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { // channel.Number = number.ToString(); nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 62a23118f..90e2766b8 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -247,7 +247,7 @@ namespace Emby.Server.Implementations.Localization } // Try splitting by : to handle "Germany: FSK 18" - var index = rating.IndexOf(':'); + var index = rating.IndexOf(':', StringComparison.Ordinal); if (index != -1) { rating = rating.Substring(index).TrimStart(':').Trim(); @@ -312,12 +312,12 @@ namespace Emby.Server.Implementations.Localization throw new ArgumentNullException(nameof(culture)); } - const string prefix = "Core"; - var key = prefix + culture; + const string Prefix = "Core"; + var key = Prefix + culture; return _dictionaries.GetOrAdd( key, - f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); + f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); } private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename) diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 6aa1dfbc9..ff95302ee 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -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) { @@ -379,7 +390,7 @@ namespace Emby.Server.Implementations.Networking var host = uri.DnsSafeHost; _logger.LogDebug("Resolving host {0}", host); - address = GetIpAddresses(host).Result.FirstOrDefault(); + address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault(); if (address != null) { diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index 3fe15ec68..81096026b 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Events; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -37,7 +36,6 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly IJsonSerializer _jsonSerializer; private readonly IApplicationPaths _applicationPaths; private readonly ILogger<TaskManager> _logger; - private readonly IFileSystem _fileSystem; /// <summary> /// Initializes a new instance of the <see cref="TaskManager" /> class. @@ -45,17 +43,14 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <param name="applicationPaths">The application paths.</param> /// <param name="jsonSerializer">The json serializer.</param> /// <param name="logger">The logger.</param> - /// <param name="fileSystem">The filesystem manager.</param> public TaskManager( IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, - ILogger<TaskManager> logger, - IFileSystem fileSystem) + ILogger<TaskManager> logger) { _applicationPaths = applicationPaths; _jsonSerializer = jsonSerializer; _logger = logger; - _fileSystem = fileSystem; ScheduledTasks = Array.Empty<IScheduledTaskWorker>(); } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 3854be703..8439f8a99 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Logging; using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks @@ -25,11 +24,6 @@ namespace Emby.Server.Implementations.ScheduledTasks public class ChapterImagesTask : IScheduledTask { /// <summary> - /// The _logger. - /// </summary> - private readonly ILogger<ChapterImagesTask> _logger; - - /// <summary> /// The _library manager. /// </summary> private readonly ILibraryManager _libraryManager; @@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class. /// </summary> public ChapterImagesTask( - ILoggerFactory loggerFactory, ILibraryManager libraryManager, IItemRepository itemRepo, IApplicationPaths appPaths, @@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks IFileSystem fileSystem, ILocalizationManager localization) { - _logger = loggerFactory.CreateLogger<ChapterImagesTask>(); _libraryManager = libraryManager; _itemRepo = itemRepo; _appPaths = appPaths; diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index 857df591a..d884d4f37 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -189,5 +189,4 @@ namespace Emby.Server.Implementations.Services return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()); } } - } diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs index cbc4b754d..7b970627e 100644 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ b/Emby.Server.Implementations/Services/ServiceExec.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -105,7 +106,13 @@ namespace Emby.Server.Implementations.Services } var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant(); - throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetMethodName(), expectedMethodName, serviceType.GetMethodName())); + throw new NotImplementedException( + string.Format( + CultureInfo.InvariantCulture, + "Could not find method named {1}({0}) or Any({0}) on Service {2}", + requestDto.GetType().GetMethodName(), + expectedMethodName, + serviceType.GetMethodName())); } private static async Task<object> GetTaskResult(Task task) diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index a42f88ea0..c87e418c2 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; +using System.Net.Mime; 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; @@ -44,7 +46,7 @@ namespace Emby.Server.Implementations.Services var pos = pathInfo.LastIndexOf('.'); if (pos != -1) { - var format = pathInfo.Substring(pos + 1); + var format = pathInfo.AsSpan().Slice(pos + 1); contentType = GetFormatContentType(format); if (contentType != null) { @@ -55,15 +57,18 @@ namespace Emby.Server.Implementations.Services return pathInfo; } - private static string GetFormatContentType(string format) + private static string GetFormatContentType(ReadOnlySpan<char> format) { - // built-in formats - switch (format) + if (format.Equals("json", StringComparison.Ordinal)) { - case "json": return "application/json"; - case "xml": return "application/xml"; - default: return null; + return MediaTypeNames.Application.Json; } + else if (format.Equals("xml", StringComparison.Ordinal)) + { + return MediaTypeNames.Application.Xml; + } + + return null; } public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken) @@ -78,7 +83,8 @@ namespace Emby.Server.Implementations.Services var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false); httpHost.ApplyRequestFilters(httpReq, httpRes, request); - + + httpRes.HttpContext.SetServiceStackRequest(httpReq); var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); // Apply response filters diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs index 89538ae72..442b2ab1c 100644 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ b/Emby.Server.Implementations/Services/ServicePath.cs @@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.Services { var component = components[i]; - if (component.StartsWith(VariablePrefix)) + if (component.StartsWith(VariablePrefix, StringComparison.Ordinal)) { var variableName = component.Substring(1, component.Length - 2); if (variableName[variableName.Length - 1] == WildCardChar) @@ -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(); diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 04f134b8b..93fb22c4e 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -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 { diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 53120a763..acc6eec1c 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.Linq; using Jellyfin.Data; using Jellyfin.Data.Entities; @@ -133,6 +134,18 @@ namespace Jellyfin.Server.Implementations return base.SaveChanges(); } + /// <inheritdoc/> + public override void Dispose() + { + foreach (var entry in ChangeTracker.Entries()) + { + entry.State = EntityState.Detached; + } + + GC.SuppressFinalize(this); + base.Dispose(); + } + /// <inheritdoc /> protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index cf5a01f08..007c29643 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -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 { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index e3a7d5467..eaa6a0a81 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -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,7 +687,7 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void UpdatePolicy(Guid userId, UserPolicy policy) { - var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateContext(); var user = dbContext.Users .Include(u => u.Permissions) .Include(u => u.Preferences) @@ -749,8 +766,8 @@ namespace Jellyfin.Server.Implementations.Users { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness - // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) - return Regex.IsMatch(name, @"^[\w\-'._@]*$"); + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( ) + return Regex.IsMatch(name, @"^[\w\ \-'._@]*$"); } private IAuthenticationProvider GetAuthenticationProvider(User user) @@ -882,7 +899,7 @@ namespace Jellyfin.Server.Implementations.Users } } - private void IncrementInvalidLoginAttemptCount(User user) + private async Task IncrementInvalidLoginAttemptCount(User user) { user.InvalidLoginAttemptCount++; int? maxInvalidLogins = user.LoginAttemptsBeforeLockout; @@ -896,7 +913,7 @@ namespace Jellyfin.Server.Implementations.Users user.InvalidLoginAttemptCount); } - UpdateUser(user); + await UpdateUserAsync(user).ConfigureAwait(false); } } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 207eaa98d..71b0fd8f3 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -9,6 +9,7 @@ using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Net; +using MediaBrowser.Controller; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; @@ -33,9 +34,9 @@ namespace Jellyfin.Server /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param> public CoreAppHost( - ServerApplicationPaths applicationPaths, + IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, - StartupOptions options, + IStartupOptions options, IFileSystem fileSystem, INetworkManager networkManager) : base( diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs index 6b5780a26..c1000eede 100644 --- a/Jellyfin.Server/Migrations/IMigrationRoutine.cs +++ b/Jellyfin.Server/Migrations/IMigrationRoutine.cs @@ -18,6 +18,11 @@ namespace Jellyfin.Server.Migrations 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> public void Perform(); diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index d633c554d..98a90500c 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -21,7 +21,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.MigrateActivityLogDb), typeof(Routines.RemoveDuplicateExtras), typeof(Routines.AddDefaultPluginRepository), - typeof(Routines.MigrateUserDb) + typeof(Routines.MigrateUserDb), + typeof(Routines.ReaddDefaultPluginRepository) }; /// <summary> @@ -43,9 +44,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(); diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs index a9d5ad16a..f6d8c9cc0 100644 --- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs +++ b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs @@ -33,6 +33,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "AddDefaultPluginRepository"; /// <inheritdoc/> + public bool PerformOnNewInstall => true; + + /// <inheritdoc/> public void Perform() { _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo); diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs index b15e09290..6821630db 100644 --- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs +++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs @@ -49,6 +49,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "CreateLoggingConfigHeirarchy"; /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> public void Perform() { var logDirectory = _appPaths.ConfigurationDirectoryPath; diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs index c18aa1629..0925a87b5 100644 --- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs +++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs @@ -26,6 +26,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "DisableTranscodingThrottling"; /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> public void Perform() { // Set EnableThrottling to false since it wasn't used before and may introduce issues diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index fb3466e13..6048160c6 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -42,6 +42,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "MigrateActivityLogDatabase"; /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> public void Perform() { var logLevelDictionary = new Dictionary<string, LogLevel>(StringComparer.OrdinalIgnoreCase) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 2be10c708..274e6ab73 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -55,6 +55,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "MigrateUserDatabase"; /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> public void Perform() { var dataPath = _paths.DataPath; diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs new file mode 100644 index 000000000..b281b5cc0 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs @@ -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(); + } + } + } +}
\ No newline at end of file diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs index 2ebef0241..6c26e47e1 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs @@ -30,6 +30,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "RemoveDuplicateExtras"; /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> public void Perform() { var dataPath = _paths.DataPath; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index ef3ebe90c..444a91c02 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -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() diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index fe5f980b1..6ff96ac56 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -369,7 +369,7 @@ namespace MediaBrowser.Api.Playback.Hls var playlistFilename = Path.GetFileNameWithoutExtension(playlist); - var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); + var indexString = Path.GetFileNameWithoutExtension(file.Name).AsSpan().Slice(playlistFilename.Length); return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture); } diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 6e9d788dc..440d1840d 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -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) diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs new file mode 100644 index 000000000..d746207c7 --- /dev/null +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -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]; + } + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 90f62e153..f34309c40 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -675,11 +675,11 @@ namespace MediaBrowser.Controller.Entities return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture)); } - var idString = Id.ToString("N", CultureInfo.InvariantCulture); + ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture); basePath = System.IO.Path.Combine(basePath, "library"); - return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString); + return System.IO.Path.Join(basePath, idString.Slice(0, 2), idString); } /// <summary> @@ -702,26 +702,27 @@ namespace MediaBrowser.Controller.Entities foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) { - sortable = sortable.Replace(removeChar, string.Empty); + sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal); } foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) { - sortable = sortable.Replace(replaceChar, " "); + sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal); } foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) { // Remove from beginning if a space follows - if (sortable.StartsWith(search + " ")) + if (sortable.StartsWith(search + " ", StringComparison.Ordinal)) { sortable = sortable.Remove(0, search.Length + 1); } + // Remove from middle if surrounded by spaces - sortable = sortable.Replace(" " + search + " ", " "); + sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal); // Remove from end if followed by a space - if (sortable.EndsWith(" " + search)) + if (sortable.EndsWith(" " + search, StringComparison.Ordinal)) { sortable = sortable.Remove(sortable.Length - (search.Length + 1)); } @@ -751,6 +752,7 @@ namespace MediaBrowser.Controller.Entities builder.Append(chunkBuilder); } + // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); return builder.ToString().RemoveDiacritics(); } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 9d6646857..bb56c83c2 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -199,7 +199,7 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Updates the item. /// </summary> - void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index e73fe7120..6685861a9 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -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(); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 534e0c372..c20c9c2c4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -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 @@ -1541,7 +1588,7 @@ namespace MediaBrowser.Controller.MediaEncoding { outputVideoCodec ??= string.Empty; - var outputSizeParam = string.Empty; + var outputSizeParam = ReadOnlySpan<char>.Empty; var request = state.BaseRequest; // Add resolution params, if specified @@ -1555,28 +1602,44 @@ namespace MediaBrowser.Controller.MediaEncoding var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = "," + outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Slice(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.Slice(index); } else { - index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = "," + outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Slice(index); } else { - index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = "," + outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Slice(index); + } + else + { + index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = outputSizeParam.Slice(index); + } + else + { + index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = outputSizeParam.Slice(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); + // 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 QSV, feed it into hardware encoder now - if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + if (width.HasValue && height.HasValue) { - videoSizeParam += ",hwupload=extra_hw_frames=64"; - } - - // 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))) - { - 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 = !outputSizeParam.IsEmpty + ? " -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) + if (isLinux) { - retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; - } - else - { - retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwupload=extra_hw_frames=64[v];[v][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; + retStr = !outputSizeParam.IsEmpty ? + " -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\""; } } @@ -1683,7 +1733,7 @@ namespace MediaBrowser.Controller.MediaEncoding mapPrefix, subtitleStreamIndex, state.VideoStream.Index, - outputSizeParam, + outputSizeParam.ToString(), videoSizeParam); } @@ -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)) { + string deintParam; 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"); } @@ -2127,7 +2183,6 @@ namespace MediaBrowser.Controller.MediaEncoding return output; } - /// <summary> /// Gets the number of threads. /// </summary> @@ -2290,7 +2345,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 +2359,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"; + } } } } @@ -2553,8 +2617,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) { - var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) - && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)); + 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) @@ -2631,13 +2694,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"; } @@ -2904,21 +2960,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"; } @@ -3187,5 +3246,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; + } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 0fd0239b4..5c43fdcfa 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -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"]) diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs index 0e2d70017..43a45291c 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles continue; } - if (line.StartsWith("[")) + if (line[0] == '[') { break; } @@ -62,7 +62,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles return trackInfo; } - long GetTicks(string time) + private long GetTicks(ReadOnlySpan<char> time) { return TimeSpan.TryParseExact(time, @"h\:mm\:ss\.ff", _usCulture, out var span) ? span.Ticks : 0; diff --git a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs index bf8808eb8..dca5c1e8a 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs @@ -1,6 +1,9 @@ +#nullable enable +#pragma warning disable CS1591 + namespace MediaBrowser.MediaEncoding.Subtitles { - public class ParserValues + public static class ParserValues { public const string NewLine = "\r\n"; } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs index 728efa788..cc35efb3f 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -20,6 +22,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles _logger = logger; } + /// <inheritdoc /> public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); @@ -55,11 +58,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles } subEvent.StartPositionTicks = GetTicks(time[0]); - var endTime = time[1]; - var idx = endTime.IndexOf(" ", StringComparison.Ordinal); + var endTime = time[1].AsSpan(); + var idx = endTime.IndexOf(' '); if (idx > 0) { - endTime = endTime.Substring(0, idx); + endTime = endTime.Slice(0, idx); } subEvent.EndPositionTicks = GetTicks(endTime); @@ -88,7 +91,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles return trackInfo; } - long GetTicks(string time) + private long GetTicks(ReadOnlySpan<char> time) { return TimeSpan.TryParseExact(time, @"hh\:mm\:ss\.fff", _usCulture, out var span) ? span.Ticks diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs index 45b317b2e..143c010b7 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs @@ -8,8 +8,12 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles { + /// <summary> + /// SRT subtitle writer. + /// </summary> public class SrtWriter : ISubtitleWriter { + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs index 77033c6b4..bd330f568 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs @@ -12,6 +12,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// </summary> public class SsaParser : ISubtitleParser { + /// <inheritdoc /> public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); @@ -45,7 +46,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles header.AppendLine(line); } - if (line.Trim().ToLowerInvariant() == "[events]") + if (string.Equals(line.Trim(), "[events]", StringComparison.OrdinalIgnoreCase)) { eventsStarted = true; } @@ -63,27 +64,27 @@ namespace MediaBrowser.MediaEncoding.Subtitles format = line.ToLowerInvariant().Substring(8).Split(','); for (int i = 0; i < format.Length; i++) { - if (format[i].Trim().ToLowerInvariant() == "layer") + if (string.Equals(format[i].Trim(), "layer", StringComparison.OrdinalIgnoreCase)) { indexLayer = i; } - else if (format[i].Trim().ToLowerInvariant() == "start") + else if (string.Equals(format[i].Trim(), "start", StringComparison.OrdinalIgnoreCase)) { indexStart = i; } - else if (format[i].Trim().ToLowerInvariant() == "end") + else if (string.Equals(format[i].Trim(), "end", StringComparison.OrdinalIgnoreCase)) { indexEnd = i; } - else if (format[i].Trim().ToLowerInvariant() == "text") + else if (string.Equals(format[i].Trim(), "text", StringComparison.OrdinalIgnoreCase)) { indexText = i; } - else if (format[i].Trim().ToLowerInvariant() == "effect") + else if (string.Equals(format[i].Trim(), "effect", StringComparison.OrdinalIgnoreCase)) { indexEffect = i; } - else if (format[i].Trim().ToLowerInvariant() == "style") + else if (string.Equals(format[i].Trim(), "style", StringComparison.OrdinalIgnoreCase)) { indexStyle = i; } @@ -184,12 +185,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles int.Parse(timeCode[3]) * 10).Ticks; } - public static string GetFormattedText(string text) + private static string GetFormattedText(string text) { text = text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase); - bool italic = false; - for (int i = 0; i < 10; i++) // just look ten times... { if (text.Contains(@"{\fn")) @@ -200,7 +199,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { string fontName = text.Substring(start + 4, end - (start + 4)); string extraTags = string.Empty; - CheckAndAddSubTags(ref fontName, ref extraTags, out italic); + CheckAndAddSubTags(ref fontName, ref extraTags, out bool italic); text = text.Remove(start, end - start + 1); if (italic) { @@ -231,7 +230,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { string fontSize = text.Substring(start + 4, end - (start + 4)); string extraTags = string.Empty; - CheckAndAddSubTags(ref fontSize, ref extraTags, out italic); + CheckAndAddSubTags(ref fontSize, ref extraTags, out bool italic); if (IsInteger(fontSize)) { text = text.Remove(start, end - start + 1); @@ -265,7 +264,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { string color = text.Substring(start + 4, end - (start + 4)); string extraTags = string.Empty; - CheckAndAddSubTags(ref color, ref extraTags, out italic); + CheckAndAddSubTags(ref color, ref extraTags, out bool italic); color = color.Replace("&", string.Empty).TrimStart('H'); color = color.PadLeft(6, '0'); @@ -303,7 +302,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { string color = text.Substring(start + 5, end - (start + 5)); string extraTags = string.Empty; - CheckAndAddSubTags(ref color, ref extraTags, out italic); + CheckAndAddSubTags(ref color, ref extraTags, out bool italic); color = color.Replace("&", string.Empty).TrimStart('H'); color = color.PadLeft(6, '0'); @@ -354,14 +353,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } private static bool IsInteger(string s) - { - if (int.TryParse(s, out var i)) - { - return true; - } - - return false; - } + => int.TryParse(s, out _); private static int CountTagInText(string text, string tag) { diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index f1aa8ea5f..8e6543d80 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Concurrent; using System.Diagnostics; @@ -172,7 +174,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles inputFiles = new[] { mediaSource.Path }; } - var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false); + var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), subtitleStream, cancellationToken).ConfigureAwait(false); var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); @@ -365,7 +367,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <returns>System.Object.</returns> private SemaphoreSlim GetLock(string filename) { - return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); + return _semaphoreLocks.GetOrAdd(filename, _ => new SemaphoreSlim(1, 1)); } /// <summary> @@ -426,7 +428,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles // FFmpeg automatically convert character encoding when it is UTF-16 // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event" - if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) && (encodingParam == "UTF-16BE" || encodingParam == "UTF-16LE")) + if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) && + (encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) || + encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase))) { encodingParam = ""; } diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs index 7d3e18578..e5c785bc5 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs @@ -6,8 +6,12 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles { + /// <summary> + /// TTML subtitle writer. + /// </summary> public class TtmlWriter : ISubtitleWriter { + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { // Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml @@ -36,9 +40,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = Regex.Replace(text, @"\\n", "<br/>", RegexOptions.IgnoreCase); - writer.WriteLine("<p begin=\"{0}\" dur=\"{1}\">{2}</p>", + writer.WriteLine( + "<p begin=\"{0}\" dur=\"{1}\">{2}</p>", trackEvent.StartPositionTicks, - (trackEvent.EndPositionTicks - trackEvent.StartPositionTicks), + trackEvent.EndPositionTicks - trackEvent.StartPositionTicks, text); } diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs index de35acbba..ad32cb794 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase); writer.WriteLine(text); - writer.WriteLine(string.Empty); + writer.WriteLine(); } } } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 7a488005e..1b37cfc93 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -112,107 +112,146 @@ namespace MediaBrowser.Model.Entities { get { - if (Type == MediaStreamType.Audio) + switch (Type) { - // if (!string.IsNullOrEmpty(Title)) - //{ - // return AddLanguageIfNeeded(Title); - //} - - var attributes = new List<string>(); - - if (!string.IsNullOrEmpty(Language)) - { - attributes.Add(StringHelper.FirstToUpper(Language)); - } - - if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase)) - { - attributes.Add(AudioCodec.GetFriendlyName(Codec)); - } - else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase)) - { - attributes.Add(Profile); - } - - if (!string.IsNullOrEmpty(ChannelLayout)) - { - attributes.Add(ChannelLayout); - } - else if (Channels.HasValue) + case MediaStreamType.Audio: { - attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch"); + var attributes = new List<string>(); + + if (!string.IsNullOrEmpty(Language)) + { + // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes. + string fullLanguage = CultureInfo + .GetCultures(CultureTypes.NeutralCultures) + .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase)) + ?.DisplayName; + attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language)); + } + + if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase)) + { + attributes.Add(AudioCodec.GetFriendlyName(Codec)); + } + else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase)) + { + attributes.Add(Profile); + } + + if (!string.IsNullOrEmpty(ChannelLayout)) + { + attributes.Add(StringHelper.FirstToUpper(ChannelLayout)); + } + else if (Channels.HasValue) + { + attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch"); + } + + if (IsDefault) + { + attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); + } + + if (!string.IsNullOrEmpty(Title)) + { + var result = new StringBuilder(Title); + foreach (var tag in attributes) + { + // Keep Tags that are not already in Title. + if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + { + result.Append(" - ").Append(tag); + } + } + + return result.ToString(); + } + + return string.Join(" - ", attributes); } - if (IsDefault) + case MediaStreamType.Video: { - attributes.Add("Default"); + var attributes = new List<string>(); + + var resolutionText = GetResolutionText(); + + if (!string.IsNullOrEmpty(resolutionText)) + { + attributes.Add(resolutionText); + } + + if (!string.IsNullOrEmpty(Codec)) + { + attributes.Add(Codec.ToUpperInvariant()); + } + + if (!string.IsNullOrEmpty(Title)) + { + var result = new StringBuilder(Title); + foreach (var tag in attributes) + { + // Keep Tags that are not already in Title. + if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + { + result.Append(" - ").Append(tag); + } + } + + return result.ToString(); + } + + return string.Join(" ", attributes); } - return string.Join(" ", attributes); - } - - if (Type == MediaStreamType.Video) - { - var attributes = new List<string>(); - - var resolutionText = GetResolutionText(); - - if (!string.IsNullOrEmpty(resolutionText)) + case MediaStreamType.Subtitle: { - attributes.Add(resolutionText); + var attributes = new List<string>(); + + if (!string.IsNullOrEmpty(Language)) + { + // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes. + string fullLanguage = CultureInfo + .GetCultures(CultureTypes.NeutralCultures) + .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase)) + ?.DisplayName; + attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language)); + } + else + { + attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined); + } + + if (IsDefault) + { + attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); + } + + if (IsForced) + { + attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced); + } + + if (!string.IsNullOrEmpty(Title)) + { + var result = new StringBuilder(Title); + foreach (var tag in attributes) + { + // Keep Tags that are not already in Title. + if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + { + result.Append(" - ").Append(tag); + } + } + + return result.ToString(); + } + + return string.Join(" - ", attributes.ToArray()); } - if (!string.IsNullOrEmpty(Codec)) - { - attributes.Add(Codec.ToUpperInvariant()); - } - - return string.Join(" ", attributes); + default: + return null; } - - if (Type == MediaStreamType.Subtitle) - { - - var attributes = new List<string>(); - - if (!string.IsNullOrEmpty(Language)) - { - attributes.Add(StringHelper.FirstToUpper(Language)); - } - else - { - attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined); - } - - if (IsDefault) - { - attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); - } - - if (IsForced) - { - attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced); - } - - if (!string.IsNullOrEmpty(Title)) - { - return attributes.AsEnumerable() - // keep Tags that are not already in Title - .Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) - // attributes concatenation, starting with Title - .Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr)) - .ToString(); - } - - return string.Join(" - ", attributes.ToArray()); - } - - if (Type == MediaStreamType.Video) - { - } - - return null; } } diff --git a/MediaBrowser.Model/Notifications/NotificationOption.cs b/MediaBrowser.Model/Notifications/NotificationOption.cs index ea363d9b1..58aecb3d3 100644 --- a/MediaBrowser.Model/Notifications/NotificationOption.cs +++ b/MediaBrowser.Model/Notifications/NotificationOption.cs @@ -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; } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 86a182fe5..9170c7002 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -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; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html index fbf413f2b..82f26a8f2 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html @@ -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> diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 90196b046..1945e6cb4 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -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> diff --git a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html index 8b117ec8d..f4375b3cb 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html @@ -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> diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 4181d37ef..a2c0e62c1 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -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); } } diff --git a/debian/changelog b/debian/changelog index 35fb65957..d38d03805 100644 --- a/debian/changelog +++ b/debian/changelog @@ -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 diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin index 64c98520c..7cbfa88ee 100644 --- a/debian/conf/jellyfin +++ b/debian/conf/jellyfin @@ -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" diff --git a/debian/control b/debian/control index 896d8286b..39c2aa055 100644 --- a/debian/control +++ b/debian/control @@ -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, diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64 index aae061e9d..bd5dc6438 100755 --- a/deployment/build.windows.amd64 +++ b/deployment/build.windows.amd64 @@ -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 diff --git a/windows/build-jellyfin.ps1 b/windows/build-jellyfin.ps1 index c762137a7..b76a8e0bb 100644 --- a/windows/build-jellyfin.ps1 +++ b/windows/build-jellyfin.ps1 @@ -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')){ |
