aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-package.yml22
-rw-r--r--Emby.Dlna/Api/DlnaServerService.cs7
-rw-r--r--Emby.Dlna/Eventing/EventManager.cs14
-rw-r--r--Emby.Dlna/PlayTo/Device.cs4
-rw-r--r--Emby.Dlna/Server/DescriptionXmlBuilder.cs182
-rw-r--r--Emby.Dlna/Service/ServiceXmlBuilder.cs34
-rw-r--r--Emby.Naming/Common/NamingOptions.cs60
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs5
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs14
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj8
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs13
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs10
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs2
-rw-r--r--Emby.Server.Implementations/Library/ExclusiveLiveStream.cs24
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs261
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs24
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs17
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs2
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs18
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs9
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json64
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json4
-rw-r--r--Emby.Server.Implementations/Networking/NetworkManager.cs13
-rw-r--r--Emby.Server.Implementations/Services/ServiceController.cs1
-rw-r--r--Emby.Server.Implementations/Services/ServiceHandler.cs4
-rw-r--r--Emby.Server.Implementations/Services/ServicePath.cs6
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs29
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs5
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs8
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj4
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj4
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs13
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs16
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs109
-rw-r--r--Jellyfin.Server/CoreAppHost.cs4
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj4
-rw-r--r--Jellyfin.Server/Migrations/IMigrationRoutine.cs5
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs49
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs3
-rw-r--r--Jellyfin.Server/Program.cs15
-rw-r--r--MediaBrowser.Api/Images/ImageService.cs5
-rw-r--r--MediaBrowser.Api/System/ActivityLogService.cs5
-rw-r--r--MediaBrowser.Api/UserService.cs2
-rw-r--r--MediaBrowser.Common/Extensions/HttpContextExtensions.cs33
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj4
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs7
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs395
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs2
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj2
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOption.cs13
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs264
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj4
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html42
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html62
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html35
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs40
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs3
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs2
-rw-r--r--debian/changelog6
-rw-r--r--debian/conf/jellyfin4
-rw-r--r--debian/control5
-rwxr-xr-xdeployment/build.windows.amd6410
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj2
-rw-r--r--tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj2
-rw-r--r--windows/build-jellyfin.ps12
79 files changed, 1243 insertions, 855 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/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[]
- {
- "<",
- "&lt;",
- ">",
- "&gt;",
- "\"",
- "&quot;",
- "'",
- "&apos;",
- "&",
- "&amp;"
- };
-
- private static string GetEscapeSequence(char c)
- {
- int num = s_escapeStringPairs.Length;
- for (int i = 0; i < num; i += 2)
- {
- string text = s_escapeStringPairs[i];
- string result = s_escapeStringPairs[i + 1];
- if (text[0] == c)
- {
- return result;
- }
- }
-
- return c.ToString(CultureInfo.InvariantCulture);
- }
-
- /// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
- /// <returns>The input string with invalid characters replaced.</returns>
- /// <param name="str">The string within which to escape invalid characters. </param>
- public static string Escape(string str)
- {
- if (str == null)
- {
- return null;
- }
-
- StringBuilder stringBuilder = null;
- int length = str.Length;
- int num = 0;
- while (true)
- {
- int num2 = str.IndexOfAny(s_escapeChars, num);
- if (num2 == -1)
- {
- break;
- }
-
- if (stringBuilder == null)
- {
- stringBuilder = new StringBuilder();
- }
-
- stringBuilder.Append(str, num, num2 - num);
- stringBuilder.Append(GetEscapeSequence(str[num2]));
- num = num2 + 1;
- }
-
- if (stringBuilder == null)
- {
- return str;
- }
-
- stringBuilder.Append(str, num, length - num);
- return stringBuilder.ToString();
- }
-
private void AppendDeviceProperties(StringBuilder builder)
{
builder.Append("<dlna:X_DLNACAP/>");
@@ -187,32 +113,54 @@ namespace Emby.Dlna.Server
builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
- builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>");
- builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
- builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
-
- builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
- builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
-
- builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
- builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
+ builder.Append("<friendlyName>")
+ .Append(SecurityElement.Escape(GetFriendlyName()))
+ .Append("</friendlyName>");
+ builder.Append("<manufacturer>")
+ .Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
+ .Append("</manufacturer>");
+ builder.Append("<manufacturerURL>")
+ .Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
+ .Append("</manufacturerURL>");
+
+ builder.Append("<modelDescription>")
+ .Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
+ .Append("</modelDescription>");
+ builder.Append("<modelName>")
+ .Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
+ .Append("</modelName>");
+
+ builder.Append("<modelNumber>")
+ .Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
+ .Append("</modelNumber>");
+ builder.Append("<modelURL>")
+ .Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
+ .Append("</modelURL>");
if (string.IsNullOrEmpty(_profile.SerialNumber))
{
- builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
+ builder.Append("<serialNumber>")
+ .Append(SecurityElement.Escape(_serverId))
+ .Append("</serialNumber>");
}
else
{
- builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
+ builder.Append("<serialNumber>")
+ .Append(SecurityElement.Escape(_profile.SerialNumber))
+ .Append("</serialNumber>");
}
builder.Append("<UPC/>");
- builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>");
+ builder.Append("<UDN>uuid:")
+ .Append(SecurityElement.Escape(_serverUdn))
+ .Append("</UDN>");
if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
{
- builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>");
+ builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">")
+ .Append(SecurityElement.Escape(_profile.SonyAggregationFlags))
+ .Append("</av:aggregationFlags>");
}
}
@@ -250,11 +198,21 @@ namespace Emby.Dlna.Server
{
builder.Append("<icon>");
- builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
- builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>");
- builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>");
- builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>");
- builder.Append("<url>" + BuildUrl(icon.Url) + "</url>");
+ builder.Append("<mimetype>")
+ .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
+ .Append("</mimetype>");
+ builder.Append("<width>")
+ .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
+ .Append("</width>");
+ builder.Append("<height>")
+ .Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
+ .Append("</height>");
+ builder.Append("<depth>")
+ .Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
+ .Append("</depth>");
+ builder.Append("<url>")
+ .Append(BuildUrl(icon.Url))
+ .Append("</url>");
builder.Append("</icon>");
}
@@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
{
builder.Append("<service>");
- builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
- builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
- builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>");
- builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>");
- builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>");
+ builder.Append("<serviceType>")
+ .Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
+ .Append("</serviceType>");
+ builder.Append("<serviceId>")
+ .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
+ .Append("</serviceId>");
+ builder.Append("<SCPDURL>")
+ .Append(BuildUrl(service.ScpdUrl))
+ .Append("</SCPDURL>");
+ builder.Append("<controlURL>")
+ .Append(BuildUrl(service.ControlUrl))
+ .Append("</controlURL>");
+ builder.Append("<eventSubURL>")
+ .Append(BuildUrl(service.EventSubUrl))
+ .Append("</eventSubURL>");
builder.Append("</service>");
}
@@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + url;
}
- return Escape(url);
+ return SecurityElement.Escape(url);
}
private IEnumerable<DeviceIcon> GetIcons()
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.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.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index f6f10beb0..c7519e2e3 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,
@@ -792,7 +792,6 @@ namespace Emby.Server.Implementations
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
- Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 04e5e570f..e46cfa177 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -1111,7 +1111,8 @@ namespace Emby.Server.Implementations.Data
continue;
}
- str.Append(ToValueString(i) + "|");
+ str.Append(ToValueString(i))
+ .Append('|');
}
str.Length -= 1; // Remove last |
@@ -2472,7 +2473,7 @@ namespace Emby.Server.Implementations.Data
var item = query.SimilarTo;
var builder = new StringBuilder();
- builder.Append("(");
+ builder.Append('(');
if (string.IsNullOrEmpty(item.OfficialRating))
{
@@ -2510,7 +2511,7 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrEmpty(query.SearchTerm))
{
var builder = new StringBuilder();
- builder.Append("(");
+ builder.Append('(');
builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
@@ -5239,7 +5240,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{
if (i > 0)
{
- insertText.Append(",");
+ insertText.Append(',');
}
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
@@ -6332,7 +6333,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
{
- insertText.Append("@" + column + index + ",");
+ insertText.Append('@')
+ .Append(column)
+ .Append(index)
+ .Append(',');
}
insertText.Length -= 1;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 548dc7085..efb2f81cc 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -34,10 +34,10 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
<PackageReference Include="Mono.Nat" Version="2.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index b207397bd..9486874d5 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -1,3 +1,4 @@
+using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Udp;
@@ -48,8 +49,16 @@ namespace Emby.Server.Implementations.EntryPoints
/// <inheritdoc />
public Task RunAsync()
{
- _udpServer = new UdpServer(_logger, _appHost, _config);
- _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+ try
+ {
+ _udpServer = new UdpServer(_logger, _appHost, _config);
+ _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+ }
+ catch (SocketException ex)
+ {
+ _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
+ }
+
return Task.CompletedTask;
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index a3a3f91b7..ab6483bf9 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -245,6 +245,16 @@ namespace Emby.Server.Implementations.IO
if (info is FileInfo fileInfo)
{
result.Length = fileInfo.Length;
+
+ // Issue #2354 get the size of files behind symbolic links
+ if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
+ {
+ using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+ {
+ result.Length = thisFileStream.Length;
+ }
+ }
+
result.DirectoryName = fileInfo.DirectoryName;
}
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 4690f2094..bd6371ca4 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -59,6 +59,8 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class LibraryManager : ILibraryManager
{
+ private const string ShortcutFileExtension = ".mblink";
+
private readonly ILogger<LibraryManager> _logger;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
@@ -74,63 +76,24 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
private readonly IImageProcessor _imageProcessor;
- private NamingOptions _namingOptions;
- private string[] _videoFileExtensions;
-
- private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
-
- private IProviderManager ProviderManager => _providerManagerFactory.Value;
-
- private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
-
- /// <summary>
- /// Gets or sets the postscan tasks.
- /// </summary>
- /// <value>The postscan tasks.</value>
- private ILibraryPostScanTask[] PostscanTasks { get; set; }
-
- /// <summary>
- /// Gets or sets the intro providers.
- /// </summary>
- /// <value>The intro providers.</value>
- private IIntroProvider[] IntroProviders { get; set; }
-
- /// <summary>
- /// Gets or sets the list of entity resolution ignore rules.
- /// </summary>
- /// <value>The entity resolution ignore rules.</value>
- private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
-
- /// <summary>
- /// Gets or sets the list of currently registered entity resolvers.
- /// </summary>
- /// <value>The entity resolvers enumerable.</value>
- private IItemResolver[] EntityResolvers { get; set; }
-
- private IMultiItemResolver[] MultiItemResolvers { get; set; }
-
/// <summary>
- /// Gets or sets the comparers.
+ /// The _root folder sync lock.
/// </summary>
- /// <value>The comparers.</value>
- private IBaseItemComparer[] Comparers { get; set; }
+ private readonly object _rootFolderSyncLock = new object();
+ private readonly object _userRootFolderSyncLock = new object();
- /// <summary>
- /// Occurs when [item added].
- /// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemAdded;
+ private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
- /// <summary>
- /// Occurs when [item updated].
- /// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemUpdated;
+ private NamingOptions _namingOptions;
+ private string[] _videoFileExtensions;
/// <summary>
- /// Occurs when [item removed].
+ /// The _root folder.
/// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+ private volatile AggregateFolder _rootFolder;
+ private volatile UserRootFolder _userRootFolder;
- public bool IsScanRunning { get; private set; }
+ private bool _wizardCompleted;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
@@ -185,37 +148,19 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Adds the parts.
+ /// Occurs when [item added].
/// </summary>
- /// <param name="rules">The rules.</param>
- /// <param name="resolvers">The resolvers.</param>
- /// <param name="introProviders">The intro providers.</param>
- /// <param name="itemComparers">The item comparers.</param>
- /// <param name="postscanTasks">The post scan tasks.</param>
- public void AddParts(
- IEnumerable<IResolverIgnoreRule> rules,
- IEnumerable<IItemResolver> resolvers,
- IEnumerable<IIntroProvider> introProviders,
- IEnumerable<IBaseItemComparer> itemComparers,
- IEnumerable<ILibraryPostScanTask> postscanTasks)
- {
- EntityResolutionIgnoreRules = rules.ToArray();
- EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
- MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
- IntroProviders = introProviders.ToArray();
- Comparers = itemComparers.ToArray();
- PostscanTasks = postscanTasks.ToArray();
- }
+ public event EventHandler<ItemChangeEventArgs> ItemAdded;
/// <summary>
- /// The _root folder.
+ /// Occurs when [item updated].
/// </summary>
- private volatile AggregateFolder _rootFolder;
+ public event EventHandler<ItemChangeEventArgs> ItemUpdated;
/// <summary>
- /// The _root folder sync lock.
+ /// Occurs when [item removed].
/// </summary>
- private readonly object _rootFolderSyncLock = new object();
+ public event EventHandler<ItemChangeEventArgs> ItemRemoved;
/// <summary>
/// Gets the root folder.
@@ -240,7 +185,68 @@ namespace Emby.Server.Implementations.Library
}
}
- private bool _wizardCompleted;
+ private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
+
+ private IProviderManager ProviderManager => _providerManagerFactory.Value;
+
+ private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+
+ /// <summary>
+ /// Gets or sets the postscan tasks.
+ /// </summary>
+ /// <value>The postscan tasks.</value>
+ private ILibraryPostScanTask[] PostscanTasks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the intro providers.
+ /// </summary>
+ /// <value>The intro providers.</value>
+ private IIntroProvider[] IntroProviders { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of entity resolution ignore rules.
+ /// </summary>
+ /// <value>The entity resolution ignore rules.</value>
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of currently registered entity resolvers.
+ /// </summary>
+ /// <value>The entity resolvers enumerable.</value>
+ private IItemResolver[] EntityResolvers { get; set; }
+
+ private IMultiItemResolver[] MultiItemResolvers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the comparers.
+ /// </summary>
+ /// <value>The comparers.</value>
+ private IBaseItemComparer[] Comparers { get; set; }
+
+ public bool IsScanRunning { get; private set; }
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="rules">The rules.</param>
+ /// <param name="resolvers">The resolvers.</param>
+ /// <param name="introProviders">The intro providers.</param>
+ /// <param name="itemComparers">The item comparers.</param>
+ /// <param name="postscanTasks">The post scan tasks.</param>
+ public void AddParts(
+ IEnumerable<IResolverIgnoreRule> rules,
+ IEnumerable<IItemResolver> resolvers,
+ IEnumerable<IIntroProvider> introProviders,
+ IEnumerable<IBaseItemComparer> itemComparers,
+ IEnumerable<ILibraryPostScanTask> postscanTasks)
+ {
+ EntityResolutionIgnoreRules = rules.ToArray();
+ EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
+ MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
+ IntroProviders = introProviders.ToArray();
+ Comparers = itemComparers.ToArray();
+ PostscanTasks = postscanTasks.ToArray();
+ }
/// <summary>
/// Records the configuration values.
@@ -340,7 +346,7 @@ namespace Emby.Server.Implementations.Library
if (item is LiveTvProgram)
{
_logger.LogDebug(
- "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@@ -349,7 +355,7 @@ namespace Emby.Server.Implementations.Library
else
{
_logger.LogInformation(
- "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@@ -367,7 +373,12 @@ namespace Emby.Server.Implementations.Library
continue;
}
- _logger.LogDebug("Deleting path {MetadataPath}", metadataPath);
+ _logger.LogDebug(
+ "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ metadataPath,
+ item.Id);
try
{
@@ -391,7 +402,13 @@ namespace Emby.Server.Implementations.Library
{
try
{
- _logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName);
+ _logger.LogInformation(
+ "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ fileSystemInfo.FullName,
+ item.Id);
+
if (fileSystemInfo.IsDirectory)
{
Directory.Delete(fileSystemInfo.FullName, true);
@@ -500,7 +517,7 @@ namespace Emby.Server.Implementations.Library
// Try to normalize paths located underneath program-data in an attempt to make them more portable
key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
.TrimStart(new[] { '/', '\\' })
- .Replace("/", "\\");
+ .Replace('/', '\\');
}
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@@ -763,14 +780,11 @@ namespace Emby.Server.Implementations.Library
return rootFolder;
}
- private volatile UserRootFolder _userRootFolder;
- private readonly object _syncLock = new object();
-
public Folder GetUserRootFolder()
{
if (_userRootFolder == null)
{
- lock (_syncLock)
+ lock (_userRootFolderSyncLock)
{
if (_userRootFolder == null)
{
@@ -1320,7 +1334,7 @@ namespace Emby.Server.Implementations.Library
return new QueryResult<BaseItem>
{
- Items = _itemRepository.GetItemList(query).ToArray()
+ Items = _itemRepository.GetItemList(query)
};
}
@@ -1451,11 +1465,9 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItems(query);
}
- var list = _itemRepository.GetItemList(query);
-
return new QueryResult<BaseItem>
{
- Items = list
+ Items = _itemRepository.GetItemList(query)
};
}
@@ -1864,7 +1876,8 @@ namespace Emby.Server.Implementations.Library
}
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
- if (outdated.Length == 0)
+ // Skip image processing if current or live tv source
+ if (outdated.Length == 0 || item.SourceType != SourceType.Library)
{
RegisterItem(item);
return;
@@ -1933,12 +1946,9 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Updates the item.
/// </summary>
- public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+ public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
- // Don't iterate multiple times
- var itemsList = items.ToList();
-
- foreach (var item in itemsList)
+ foreach (var item in items)
{
if (item.IsFileProtocol)
{
@@ -1950,11 +1960,11 @@ namespace Emby.Server.Implementations.Library
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
}
- _itemRepository.SaveItems(itemsList, cancellationToken);
+ _itemRepository.SaveItems(items, cancellationToken);
if (ItemUpdated != null)
{
- foreach (var item in itemsList)
+ foreach (var item in items)
{
// With the live tv guide this just creates too much noise
if (item.SourceType != SourceType.Library)
@@ -2177,8 +2187,6 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
}
- private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
-
public UserView GetNamedView(
User user,
string name,
@@ -2476,14 +2484,9 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
- var episodeInfo = episode.IsFileProtocol ?
- resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
- new Naming.TV.EpisodeInfo();
-
- if (episodeInfo == null)
- {
- episodeInfo = new Naming.TV.EpisodeInfo();
- }
+ var episodeInfo = episode.IsFileProtocol
+ ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
+ : new Naming.TV.EpisodeInfo();
try
{
@@ -2491,11 +2494,13 @@ namespace Emby.Server.Implementations.Library
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
{
// Read from metadata
- var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaSource = episode.GetMediaSources(false)[0],
- MediaType = DlnaProfileType.Video
- }, CancellationToken.None).GetAwaiter().GetResult();
+ var mediaInfo = _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = episode.GetMediaSources(false)[0],
+ MediaType = DlnaProfileType.Video
+ },
+ CancellationToken.None).GetAwaiter().GetResult();
if (mediaInfo.ParentIndexNumber > 0)
{
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@@ -2653,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
var videos = videoListResolver.Resolve(fileSystemChildren);
- var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+ var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
if (currentVideo != null)
{
@@ -2670,9 +2675,7 @@ namespace Emby.Server.Implementations.Library
.Select(video =>
{
// Try to retrieve it from the db. If we don't find it, use the resolved version
- var dbItem = GetItemById(video.Id) as Trailer;
-
- if (dbItem != null)
+ if (GetItemById(video.Id) is Trailer dbItem)
{
video = dbItem;
}
@@ -2999,23 +3002,6 @@ namespace Emby.Server.Implementations.Library
});
}
- private static bool ValidateNetworkPath(string path)
- {
- // if (Environment.OSVersion.Platform == PlatformID.Win32NT)
- //{
- // // We can't validate protocol-based paths, so just allow them
- // if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
- // {
- // return Directory.Exists(path);
- // }
- //}
-
- // Without native support for unc, we cannot validate this when running under mono
- return true;
- }
-
- private const string ShortcutFileExtension = ".mblink";
-
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
{
AddMediaPathInternal(virtualFolderName, pathInfo, true);
@@ -3040,11 +3026,6 @@ namespace Emby.Server.Implementations.Library
throw new FileNotFoundException("The path does not exist.");
}
- if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
- {
- throw new FileNotFoundException("The network path does not exist.");
- }
-
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@@ -3083,11 +3064,6 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(pathInfo));
}
- if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
- {
- throw new FileNotFoundException("The network path does not exist.");
- }
-
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@@ -3219,7 +3195,8 @@ namespace Emby.Server.Implementations.Library
if (!Directory.Exists(virtualFolderPath))
{
- throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
+ throw new FileNotFoundException(
+ string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
}
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
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..4e1316abf 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))
@@ -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/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index e3e554824..9a69bce0e 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/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/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 82df43be1..a6e9779c9 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung",
"Artists": "Interpreten",
- "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
+ "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
"Books": "Bücher",
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
"Channels": "Kanäle",
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 79d078d4a..7f8df1289 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -5,47 +5,47 @@
"Artists": "Artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku-buku",
- "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+ "CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
"Channels": "Saluran",
- "ChapterNameValue": "Chapter {0}",
+ "ChapterNameValue": "Bab {0}",
"Collections": "Koleksi",
- "DeviceOfflineWithName": "{0} has disconnected",
- "DeviceOnlineWithName": "{0} is connected",
+ "DeviceOfflineWithName": "{0} telah diputuskan sambungan",
+ "DeviceOnlineWithName": "{0} telah disambung",
"FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
- "Favorites": "Favorites",
- "Folders": "Folders",
+ "Favorites": "Kegemaran",
+ "Folders": "Fail-fail",
"Genres": "Genre-genre",
- "HeaderAlbumArtists": "Album Artists",
+ "HeaderAlbumArtists": "Album Artis-artis",
"HeaderCameraUploads": "Muatnaik Kamera",
"HeaderContinueWatching": "Terus Menonton",
- "HeaderFavoriteAlbums": "Favorite Albums",
- "HeaderFavoriteArtists": "Favorite Artists",
- "HeaderFavoriteEpisodes": "Favorite Episodes",
- "HeaderFavoriteShows": "Favorite Shows",
- "HeaderFavoriteSongs": "Favorite Songs",
- "HeaderLiveTV": "Live TV",
- "HeaderNextUp": "Next Up",
- "HeaderRecordingGroups": "Recording Groups",
- "HomeVideos": "Home videos",
- "Inherit": "Inherit",
- "ItemAddedWithName": "{0} was added to the library",
- "ItemRemovedWithName": "{0} was removed from the library",
+ "HeaderFavoriteAlbums": "Album-album Kegemaran",
+ "HeaderFavoriteArtists": "Artis-artis Kegemaran",
+ "HeaderFavoriteEpisodes": "Episod-episod Kegemaran",
+ "HeaderFavoriteShows": "Rancangan-rancangan Kegemaran",
+ "HeaderFavoriteSongs": "Lagu-lagu Kegemaran",
+ "HeaderLiveTV": "TV Siaran Langsung",
+ "HeaderNextUp": "Seterusnya",
+ "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman",
+ "HomeVideos": "Video Personal",
+ "Inherit": "Mewarisi",
+ "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka",
+ "ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
"LabelIpAddressValue": "Alamat IP: {0}",
- "LabelRunningTimeValue": "Running time: {0}",
- "Latest": "Latest",
- "MessageApplicationUpdated": "Jellyfin Server has been updated",
- "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
- "MessageServerConfigurationUpdated": "Server configuration has been updated",
- "MixedContent": "Mixed content",
- "Movies": "Movies",
+ "LabelRunningTimeValue": "Masa berjalan: {0}",
+ "Latest": "Terbaru",
+ "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini",
+ "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
+ "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
+ "MixedContent": "Kandungan campuran",
+ "Movies": "Filem",
"Music": "Muzik",
"MusicVideos": "Video muzik",
- "NameInstallFailed": "{0} installation failed",
- "NameSeasonNumber": "Season {0}",
- "NameSeasonUnknown": "Season Unknown",
- "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
- "NotificationOptionApplicationUpdateAvailable": "Application update available",
+ "NameInstallFailed": "{0} pemasangan gagal",
+ "NameSeasonNumber": "Musim {0}",
+ "NameSeasonUnknown": "Musim Tidak Diketahui",
+ "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
+ "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
"NotificationOptionAudioPlayback": "Audio playback started",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 32538ac03..576aaeb1b 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -67,5 +67,7 @@
"Artists": "นักแสดง",
"Application": "แอปพลิเคชั่น",
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
- "Albums": "อัลบั้ม"
+ "Albums": "อัลบั้ม",
+ "ScheduledTaskStartedWithName": "{0} เริ่มต้น",
+ "ScheduledTaskFailedWithName": "{0} ล้มเหลว"
}
diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs
index 6aa1dfbc9..50cb44b28 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)
{
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/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs
index a42f88ea0..3d4e1ca77 100644
--- a/Emby.Server.Implementations/Services/ServiceHandler.cs
+++ b/Emby.Server.Implementations/Services/ServiceHandler.cs
@@ -6,6 +6,7 @@ using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Services
var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
-
+
+ httpRes.HttpContext.SetServiceStackRequest(httpReq);
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
// Apply response filters
diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs
index 89538ae72..bdda7c089 100644
--- a/Emby.Server.Implementations/Services/ServicePath.cs
+++ b/Emby.Server.Implementations/Services/ServicePath.cs
@@ -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/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 21c12ae79..d1818deff 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -117,23 +117,20 @@ namespace Emby.Server.Implementations.TV
limit = limit.Value + 10;
}
- var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[] { typeof(Episode).Name },
- OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
- SeriesPresentationUniqueKey = presentationUniqueKey,
- Limit = limit,
- DtoOptions = new DtoOptions
- {
- Fields = new ItemFields[]
+ var items = _libraryManager
+ .GetItemList(
+ new InternalItemsQuery(user)
{
- ItemFields.SeriesPresentationUniqueKey
- },
- EnableImages = false
- },
- GroupBySeriesPresentationUniqueKey = true
-
- }, parentsFolders.ToList()).Cast<Episode>().Select(GetUniqueSeriesKey);
+ IncludeItemTypes = new[] { typeof(Episode).Name },
+ OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
+ SeriesPresentationUniqueKey = presentationUniqueKey,
+ Limit = limit,
+ DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
+ GroupBySeriesPresentationUniqueKey = true
+ }, parentsFolders.ToList())
+ .Cast<Episode>()
+ .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
+ .Select(GetUniqueSeriesKey);
// Avoid implicitly captured closure
var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 146ebaf25..4f54c06dd 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -148,6 +148,11 @@ namespace Emby.Server.Implementations.Updates
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
return Array.Empty<PackageInfo>();
}
+ catch (HttpRequestException ex)
+ {
+ _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
+ return Array.Empty<PackageInfo>();
+ }
}
/// <inheritdoc />
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.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 93c4612b6..3693d5122 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.5" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
</ItemGroup>
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 58d1ba2f3..8ce0f3848 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -19,8 +19,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.5" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.5" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.6" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.6" />
</ItemGroup>
</Project>
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index dcac1b34b..21748ca19 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -24,11 +24,11 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.5">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index 774970e94..4ad684063 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;
@@ -135,6 +136,18 @@ namespace Jellyfin.Server.Implementations
return base.SaveChanges();
}
+ /// <inheritdoc/>
+ public override void Dispose()
+ {
+ foreach (var entry in ChangeTracker.Entries())
+ {
+ entry.State = EntityState.Detached;
+ }
+
+ GC.SuppressFinalize(this);
+ base.Dispose();
+ }
+
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
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 9aff808db..e49a99fe5 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,8 +687,14 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void UpdatePolicy(Guid userId, UserPolicy policy)
{
- var dbContext = _dbProvider.CreateContext();
- var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
+ using var dbContext = _dbProvider.CreateContext();
+ var user = dbContext.Users
+ .Include(u => u.Permissions)
+ .Include(u => u.Preferences)
+ .Include(u => u.AccessSchedules)
+ .Include(u => u.ProfileImage)
+ .FirstOrDefault(u => u.Id == userId)
+ ?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
@@ -876,7 +899,7 @@ namespace Jellyfin.Server.Implementations.Users
}
}
- private void IncrementInvalidLoginAttemptCount(User user)
+ private async Task IncrementInvalidLoginAttemptCount(User user)
{
user.InvalidLoginAttemptCount++;
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
@@ -890,7 +913,7 @@ namespace Jellyfin.Server.Implementations.Users
user.InvalidLoginAttemptCount);
}
- UpdateUser(user);
+ await UpdateUserAsync(user).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index c5a7368aa..e6e09b79d 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -34,9 +34,9 @@ namespace Jellyfin.Server
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
public CoreAppHost(
- ServerApplicationPaths applicationPaths,
+ IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
- StartupOptions options,
+ IStartupOptions options,
IFileSystem fileSystem,
INetworkManager networkManager)
: base(
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 6a2d252ab..b1bd38cff 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -41,8 +41,8 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
<PackageReference Include="prometheus-net" Version="3.6.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
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 7f208952c..844dae61f 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -22,6 +22,7 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.RemoveDuplicateExtras),
typeof(Routines.AddDefaultPluginRepository),
typeof(Routines.MigrateUserDb),
+ typeof(Routines.ReaddDefaultPluginRepository),
typeof(Routines.MigrateDisplayPreferencesDb)
};
@@ -44,9 +45,8 @@ namespace Jellyfin.Server.Migrations
// If startup wizard is not finished, this is a fresh install.
// Don't run any migrations, just mark all of them as applied.
logger.LogInformation("Marking all known migrations as applied because this is a fresh install");
- migrationOptions.Applied.AddRange(migrations.Select(m => (m.Id, m.Name)));
+ migrationOptions.Applied.AddRange(migrations.Where(m => !m.PerformOnNewInstall).Select(m => (m.Id, m.Name)));
host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
- return;
}
var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
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/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs
index 8426a9a4f..575b1157a 100644
--- a/MediaBrowser.Api/Images/ImageService.cs
+++ b/MediaBrowser.Api/Images/ImageService.cs
@@ -895,6 +895,11 @@ namespace MediaBrowser.Api.Images
// Handle image/png; charset=utf-8
mimeType = mimeType.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+ if (user.ProfileImage != null)
+ {
+ _userManager.ClearProfileImage(user);
+ }
+
user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
await _providerManager
diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs
index 7ca31c21a..4afa6e114 100644
--- a/MediaBrowser.Api/System/ActivityLogService.cs
+++ b/MediaBrowser.Api/System/ActivityLogService.cs
@@ -56,7 +56,10 @@ namespace MediaBrowser.Api.System
DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
- entries => entries.Where(entry => entry.DateCreated >= minDate));
+ entries => entries.Where(entry => entry.DateCreated >= minDate
+ && (!request.HasUserId.HasValue || (request.HasUserId.Value
+ ? entry.UserId != Guid.Empty
+ : entry.UserId == Guid.Empty))));
var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit);
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.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index c9ca153c7..7380f39fd 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -17,8 +17,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup>
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index b5eec1846..9abcf2b62 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -200,7 +200,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Updates the item.
/// </summary>
- void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
+ void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
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/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 73e966344..67f17f7a5 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -13,8 +13,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.6" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index cbfdf2695..0242338b7 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
@@ -1351,7 +1398,7 @@ namespace MediaBrowser.Controller.MediaEncoding
transcoderChannelLimit = 6;
}
- var isTranscodingAudio = !EncodingHelper.IsCopyCodec(codec);
+ var isTranscodingAudio = !IsCopyCodec(codec);
int? resultChannels = state.GetRequestedAudioChannels(codec);
if (isTranscodingAudio)
@@ -1555,28 +1602,44 @@ namespace MediaBrowser.Controller.MediaEncoding
var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = "," + outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Substring(index);
}
else
{
- index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
+ index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = "," + outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Substring(index);
}
else
{
- index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
+ index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = "," + outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Substring(index);
}
else
{
- index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+ index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = "," + outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Substring(index);
+ }
+ else
+ {
+ index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+ if (index != -1)
+ {
+ outputSizeParam = outputSizeParam.Substring(index);
+ }
+ else
+ {
+ index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase);
+ if (index != -1)
+ {
+ outputSizeParam = outputSizeParam.Substring(index);
+ }
+ }
}
}
}
@@ -1585,43 +1648,30 @@ namespace MediaBrowser.Controller.MediaEncoding
var videoSizeParam = string.Empty;
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
+ var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
// Setup subtitle scaling
if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
{
- videoSizeParam = string.Format(
- CultureInfo.InvariantCulture,
- "scale={0}:{1}",
- state.VideoStream.Width.Value,
- state.VideoStream.Height.Value);
-
- // For QSV, feed it into hardware encoder now
- if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
- {
- videoSizeParam += ",hwupload=extra_hw_frames=64";
- }
+ // Adjust the size of graphical subtitles to fit the video stream.
+ var videoStream = state.VideoStream;
+ var inputWidth = videoStream?.Width;
+ var inputHeight = videoStream?.Height;
+ var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
- // For VAAPI and CUVID decoder
- // these encoders cannot automatically adjust the size of graphical subtitles to fit the output video,
- // thus needs to be manually adjusted.
- if (videoDecoder.IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
- || (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
- && (videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
- || outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1)))
+ if (width.HasValue && height.HasValue)
{
- var videoStream = state.VideoStream;
- var inputWidth = videoStream?.Width;
- var inputHeight = videoStream?.Height;
- var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
-
- if (width.HasValue && height.HasValue)
- {
- videoSizeParam = string.Format(
+ videoSizeParam = string.Format(
CultureInfo.InvariantCulture,
- "scale={0}:{1}",
+ "scale={0}x{1}",
width.Value,
height.Value);
- }
+ }
+
+ // For QSV, feed it into hardware encoder now
+ if (isLinux && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ videoSizeParam += ",hwupload=extra_hw_frames=64";
}
}
@@ -1634,7 +1684,10 @@ namespace MediaBrowser.Controller.MediaEncoding
: state.SubtitleStream.Index;
// Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference)
- var retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"";
+ // Always put the scaler before the overlay for better performance
+ var retStr = !string.IsNullOrEmpty(outputSizeParam) ?
+ " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"" :
+ " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
// When the input may or may not be hardware VAAPI decodable
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
@@ -1644,12 +1697,11 @@ namespace MediaBrowser.Controller.MediaEncoding
[sub]: SW scaling subtitle to FixedOutputSize
[base][sub]: SW overlay
*/
- outputSizeParam = outputSizeParam.TrimStart(',');
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"";
}
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
- else if (IsVaapiSupported(state) && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
+ else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
&& string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
{
/*
@@ -1657,7 +1709,6 @@ namespace MediaBrowser.Controller.MediaEncoding
[sub]: SW scaling subtitle to FixedOutputSize
[base][sub]: SW overlay
*/
- outputSizeParam = outputSizeParam.TrimStart(',');
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
}
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
@@ -1666,14 +1717,13 @@ namespace MediaBrowser.Controller.MediaEncoding
QSV in FFMpeg can now setup hardware overlay for transcodes.
For software decoding and hardware encoding option, frames must be hwuploaded into hardware
with fixed frame size.
+ Currently only supports linux.
*/
- if (videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1)
- {
- retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\"";
- }
- else
+ if (isLinux)
{
- retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwupload=extra_hw_frames=64[v];[v][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\"";
+ retStr = !string.IsNullOrEmpty(outputSizeParam) ?
+ " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" :
+ " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
}
}
@@ -1745,10 +1795,8 @@ namespace MediaBrowser.Controller.MediaEncoding
requestedMaxWidth,
requestedMaxHeight);
- var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !hasTextSubs))
+ || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
&& width.HasValue
&& height.HasValue)
{
@@ -1758,6 +1806,10 @@ namespace MediaBrowser.Controller.MediaEncoding
var outputWidth = width.Value;
var outputHeight = height.Value;
var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
+ var isDeintEnabled = state.DeInterlace("h264", true)
+ || state.DeInterlace("avc", true)
+ || state.DeInterlace("h265", true)
+ || state.DeInterlace("hevc", true);
if (!videoWidth.HasValue
|| outputWidth != videoWidth.Value
@@ -1769,15 +1821,20 @@ namespace MediaBrowser.Controller.MediaEncoding
filters.Add(
string.Format(
CultureInfo.InvariantCulture,
- "{0}=w={1}:h={2}:format=nv12",
+ "{0}=w={1}:h={2}:format=nv12{3}",
qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
outputWidth,
- outputHeight));
+ outputHeight,
+ (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
}
else
{
- // set w=0:h=0 for vpp_qsv to keep the original dimensions, otherwise it will fail.
- filters.Add(string.Format(CultureInfo.InvariantCulture, "{0}format=nv12", qsv_or_vaapi ? "vpp_qsv=w=0:h=0:" : "scale_vaapi="));
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}=format=nv12{1}",
+ qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
+ (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
}
}
else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
@@ -1989,7 +2046,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
var request = state.BaseRequest;
-
var videoStream = state.VideoStream;
var filters = new List<string>();
@@ -1998,32 +2054,34 @@ namespace MediaBrowser.Controller.MediaEncoding
var inputHeight = videoStream?.Height;
var threeDFormat = state.MediaSource.Video3DFormat;
+ var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
+ var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+ var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
+ var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+
var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
// When the input may or may not be hardware VAAPI decodable
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (isVaapiH264Encoder)
{
filters.Add("format=nv12|vaapi");
filters.Add("hwupload");
}
- // When the input may or may not be hardware QSV decodable
- else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
+ else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
{
- if (!hasTextSubs)
- {
- filters.Add("format=nv12|qsv");
- filters.Add("hwupload=extra_hw_frames=64");
- }
+ filters.Add("hwupload=extra_hw_frames=64");
}
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
- else if (videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
- && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+ else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
{
var codec = videoStream.Codec.ToLowerInvariant();
- var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
- || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
+ var isColorDepth10 = IsColorDepth10(state);
// Assert 10-bit hardware VAAPI decodable
if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
@@ -2048,49 +2106,49 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add hardware deinterlace filter before scaling filter
- if (state.DeInterlace("h264", true))
+ if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true))
{
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (isVaapiH264Encoder)
{
filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi"));
}
- else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
- {
- if (!hasTextSubs)
- {
- filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_qsv"));
- }
- }
}
// Add software deinterlace filter before scaling filter
- if (((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true))
- && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
- || (hasTextSubs && state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)))
+ if (state.DeInterlace("h264", true)
+ || state.DeInterlace("avc", true)
+ || state.DeInterlace("h265", true)
+ || state.DeInterlace("hevc", true))
{
+ var deintParam = string.Empty;
var inputFramerate = videoStream?.RealFrameRate;
// If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle
if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30)
{
- filters.Add("yadif=1:-1:0");
+ deintParam = "yadif=1:-1:0";
}
else
{
- filters.Add("yadif=0:-1:0");
+ deintParam = "yadif=0:-1:0";
+ }
+
+ if (!string.IsNullOrEmpty(deintParam))
+ {
+ if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder)
+ {
+ filters.Add(deintParam);
+ }
}
}
// Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr
filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
- // Add parameters to use VAAPI with burn-in text subttiles (GH issue #642)
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
+ if (isVaapiH264Encoder)
{
- if (state.SubtitleStream != null
- && state.SubtitleStream.IsTextSubtitleStream
- && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ if (hasTextSubs)
{
// Test passed on Intel and AMD gfx
filters.Add("hwmap=mode=read+write");
@@ -2100,9 +2158,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var output = string.Empty;
- if (state.SubtitleStream != null
- && state.SubtitleStream.IsTextSubtitleStream
- && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ if (hasTextSubs)
{
var subParam = GetTextSubtitleParam(state);
@@ -2110,7 +2166,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API
// Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (isVaapiH264Encoder)
{
filters.Add("hwmap");
}
@@ -2264,7 +2320,7 @@ namespace MediaBrowser.Controller.MediaEncoding
flags.Add("+ignidx");
}
- if (state.GenPtsInput || EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ if (state.GenPtsInput || IsCopyCodec(state.OutputVideoCodec))
{
flags.Add("+genpts");
}
@@ -2290,7 +2346,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
inputModifier += " " + videoDecoder;
- if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1)
+ if (!IsCopyCodec(state.OutputVideoCodec)
+ && (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1)
{
var videoStream = state.VideoStream;
var inputWidth = videoStream?.Width;
@@ -2303,11 +2360,19 @@ namespace MediaBrowser.Controller.MediaEncoding
&& width.HasValue
&& height.HasValue)
{
- inputModifier += string.Format(
- CultureInfo.InvariantCulture,
- " -resize {0}x{1}",
- width.Value,
- height.Value);
+ if (width.HasValue && height.HasValue)
+ {
+ inputModifier += string.Format(
+ CultureInfo.InvariantCulture,
+ " -resize {0}x{1}",
+ width.Value,
+ height.Value);
+ }
+
+ if (state.DeInterlace("h264", true))
+ {
+ inputModifier += " -deint 1";
+ }
}
}
}
@@ -2523,21 +2588,21 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets the name of the output video codec.
+ /// Gets the ffmpeg option string for the hardware accelerated video decoder.
/// </summary>
+ /// <param name="state">The encoding job info.</param>
+ /// <param name="encodingOptions">The encoding options.</param>
+ /// <returns>The option string or null if none available.</returns>
protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
{
- var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
var videoStream = state.VideoStream;
- var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
- || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
-
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ if (videoStream == null)
{
return null;
}
+ var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
@@ -2546,10 +2611,15 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
- if (videoStream != null
- && !string.IsNullOrEmpty(videoStream.Codec)
- && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+ if (IsCopyCodec(state.OutputVideoCodec))
{
+ return null;
+ }
+
+ if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+ {
+ var isColorDepth10 = IsColorDepth10(state);
+
// Only hevc and vp9 formats have 10-bit hardware decoder support now.
if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
@@ -2625,13 +2695,6 @@ namespace MediaBrowser.Controller.MediaEncoding
case "h264":
if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
{
- // cuvid decoder does not support 10-bit input.
- if ((videoStream.BitDepth ?? 8) > 8)
- {
- encodingOptions.HardwareDecodingCodecs = Array.Empty<string>();
- return null;
- }
-
return "-c:v h264_cuvid";
}
@@ -2898,21 +2961,24 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec)
{
- var isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
+ var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1);
var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va");
if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
{
- if (!isWindows)
+ if (isLinux)
{
return "-hwaccel vaapi";
}
- else if (isWindows8orLater)
+
+ if (isWindows && isWindows8orLater)
{
return "-hwaccel d3d11va";
}
- else
+
+ if (isWindows && !isWindows8orLater)
{
return "-hwaccel dxva2";
}
@@ -3002,7 +3068,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += " -mpegts_m2ts_mode 1";
}
- if (EncodingHelper.IsCopyCodec(videoCodec))
+ if (IsCopyCodec(videoCodec))
{
if (state.VideoStream != null
&& string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
@@ -3104,7 +3170,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var args = "-codec:a:0 " + codec;
- if (EncodingHelper.IsCopyCodec(codec))
+ if (IsCopyCodec(codec))
{
return args;
}
@@ -3181,5 +3247,42 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);
}
+
+ public static bool IsColorDepth10(EncodingJobInfo state)
+ {
+ var result = false;
+ var videoStream = state.VideoStream;
+
+ if (videoStream != null)
+ {
+ if (!string.IsNullOrEmpty(videoStream.PixelFormat))
+ {
+ result = videoStream.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase);
+ if (result)
+ {
+ return true;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(videoStream.Profile))
+ {
+ result = videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
+ || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)
+ || videoStream.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase);
+ if (result)
+ {
+ return true;
+ }
+ }
+
+ result = (videoStream.BitDepth ?? 8) == 10;
+ if (result)
+ {
+ return true;
+ }
+ }
+
+ return result;
+ }
}
}
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/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index f1aa8ea5f..a971115c9 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -172,7 +172,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
inputFiles = new[] { mediaSource.Path };
}
- var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
+ var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), subtitleStream, cancellationToken).ConfigureAwait(false);
var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 83bd0c07e..902e29b20 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -23,7 +23,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" />
<PackageReference Include="System.Globalization" Version="4.3.0" />
<PackageReference Include="System.Text.Json" Version="4.7.2" />
</ItemGroup>
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/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 446e27df6..42c7cec53 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -16,8 +16,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
- <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
<PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
<PackageReference Include="PlaylistsNET" Version="1.0.6" />
<PackageReference Include="TvDbSharper" Version="3.2.0" />
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/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
index 2c6682f82..cd2f96f14 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Providers;
@@ -229,6 +230,45 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
}
+ public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
+ var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
+
+ if (imagesSummary.Data.Fanart > 0)
+ {
+ yield return KeyType.Fanart;
+ }
+
+ if (imagesSummary.Data.Series > 0)
+ {
+ yield return KeyType.Series;
+ }
+
+ if (imagesSummary.Data.Poster > 0)
+ {
+ yield return KeyType.Poster;
+ }
+ }
+
+ public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
+ var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
+
+ if (imagesSummary.Data.Season > 0)
+ {
+ yield return KeyType.Season;
+ }
+
+ if (imagesSummary.Data.Fanart > 0)
+ {
+ yield return KeyType.Fanart;
+ }
+
+ // TODO seasonwide is not supported in TvDbSharper
+ }
+
private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
{
if (_cache.TryGetValue(key, out T cachedValue))
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
index 5af99a573..e9ba20475 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
@@ -65,8 +65,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
var language = item.GetPreferredMetadataLanguage();
var remoteImages = new List<RemoteImageInfo>();
- var keyTypes = new[] { KeyType.Season, KeyType.Seasonwide, KeyType.Fanart };
- foreach (var keyType in keyTypes)
+ var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false);
+ await foreach (var keyType in keyTypes)
{
var imageQuery = new ImagesQuery
{
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
index 7dd012825..c33aefbc1 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
@@ -59,9 +59,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
var language = item.GetPreferredMetadataLanguage();
var remoteImages = new List<RemoteImageInfo>();
- var keyTypes = new[] { KeyType.Poster, KeyType.Series, KeyType.Fanart };
var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb));
- foreach (KeyType keyType in keyTypes)
+ var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken)
+ .ConfigureAwait(false);
+ await foreach (KeyType keyType in allowedKeyTypes)
{
var imageQuery = new ImagesQuery
{
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
index b3641dc9f..df48629d0 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
@@ -247,10 +247,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
Name = tvdbTitles.FirstOrDefault(),
ProductionYear = firstAired.Year,
- SearchProviderName = Name,
- ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner
+ SearchProviderName = Name
};
+ if (!string.IsNullOrEmpty(seriesSearchResult.Banner))
+ {
+ // Results from their Search endpoints already include the /banners/ part in the url, because reasons...
+ remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + seriesSearchResult.Banner;
+ }
+
try
{
var seriesSesult =
@@ -365,10 +370,14 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
Type = PersonType.Actor,
Name = (actor.Name ?? string.Empty).Trim(),
Role = actor.Role,
- ImageUrl = TvdbUtils.BannerUrl + actor.Image,
SortOrder = actor.SortOrder
};
+ if (!string.IsNullOrEmpty(actor.Image))
+ {
+ personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image;
+ }
+
if (!string.IsNullOrWhiteSpace(personInfo.Name))
{
result.AddPerson(personInfo);
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs
index 3f71041b2..37a8d04a6 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs
@@ -9,7 +9,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K";
public const string TvdbBaseUrl = "https://www.thetvdb.com/";
- public const string BannerUrl = TvdbBaseUrl + "banners/";
+ public const string TvdbImageBaseUrl = "https://www.thetvdb.com";
+ public const string BannerUrl = TvdbImageBaseUrl + "/banners/";
public static ImageType GetImageTypeFromKeyType(string keyType)
{
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/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 219020888..132050e7b 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -16,7 +16,7 @@
<PackageReference Include="AutoFixture" Version="4.13.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.5" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
index a767c0039..8309faebd 100644
--- a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
+++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.5" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
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')){