aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Emby.Dlna/DlnaManager.cs10
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs23
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs2
-rw-r--r--Emby.Dlna/Service/BaseControlHandler.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs262
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs35
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs15
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj4
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs37
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs14
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs19
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs2
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs20
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs15
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs5
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs23
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json28
-rw-r--r--Emby.Server.Implementations/Localization/Core/gsw.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/ml.json121
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/pa.json121
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ur_PK.json4
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs17
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs688
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManifest.cs60
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs42
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs8
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs2
-rw-r--r--Emby.Server.Implementations/Serialization/JsonSerializer.cs281
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs2
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs453
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs20
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs3
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs293
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs2
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs1
-rw-r--r--Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs2
-rw-r--r--Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs2
-rw-r--r--Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs2
-rw-r--r--Jellyfin.Api/Models/ConfigurationPageInfo.cs16
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj2
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs11
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs1
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj5
-rw-r--r--Jellyfin.Server/Program.cs3
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs42
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs2
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs33
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs44
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs35
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs2
-rw-r--r--MediaBrowser.Common/Json/JsonDefaults.cs2
-rw-r--r--MediaBrowser.Common/Net/IPNetAddress.cs2
-rw-r--r--MediaBrowser.Common/Plugins/BasePlugin.cs220
-rw-r--r--MediaBrowser.Common/Plugins/BasePluginOfT.cs208
-rw-r--r--MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs27
-rw-r--r--MediaBrowser.Common/Plugins/IPlugin.cs40
-rw-r--r--MediaBrowser.Common/Plugins/IPluginManager.cs86
-rw-r--r--MediaBrowser.Common/Plugins/LocalPlugin.cs91
-rw-r--r--MediaBrowser.Common/Plugins/PluginManifest.cs110
-rw-r--r--MediaBrowser.Common/Updates/IInstallationManager.cs32
-rw-r--r--MediaBrowser.Common/Updates/InstallationEventArgs.cs11
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs7
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs7
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs10
-rw-r--r--MediaBrowser.Controller/Resolvers/ResolverPriority.cs7
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs27
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj2
-rw-r--r--MediaBrowser.Model/Plugins/PluginInfo.cs43
-rw-r--r--MediaBrowser.Model/Plugins/PluginPageInfo.cs34
-rw-r--r--MediaBrowser.Model/Plugins/PluginStatus.cs47
-rw-r--r--MediaBrowser.Model/Providers/ExternalIdInfo.cs21
-rw-r--r--MediaBrowser.Model/Serialization/IJsonSerializer.cs102
-rw-r--r--MediaBrowser.Model/Session/SessionMessageType.cs2
-rw-r--r--MediaBrowser.Model/Updates/InstallationInfo.cs8
-rw-r--r--MediaBrowser.Model/Updates/PackageInfo.cs50
-rw-r--r--MediaBrowser.Model/Updates/VersionInfo.cs47
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs12
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj1
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs20
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs72
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Plugin.cs2
-rw-r--r--MediaBrowser.Providers/Properties/AssemblyInfo.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/EntryPoint.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj1
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs9
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs4
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs6
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs3
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs2
-rw-r--r--MediaBrowser.sln4
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj6
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj1
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs69
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs88
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj4
120 files changed, 2918 insertions, 1642 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index a63db6ed7..33799f24b 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -141,6 +141,7 @@
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
+ - [ianjazz246](https://github.com/ianjazz246)
# Emby Contributors
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index fedd20b68..21ba1c755 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -7,12 +7,14 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
+using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Emby.Dlna.Profiles;
using Emby.Dlna.Server;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@@ -32,9 +34,9 @@ namespace Emby.Dlna
private readonly IXmlSerializer _xmlSerializer;
private readonly IFileSystem _fileSystem;
private readonly ILogger<DlnaManager> _logger;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
@@ -43,14 +45,12 @@ namespace Emby.Dlna
IFileSystem fileSystem,
IApplicationPaths appPaths,
ILoggerFactory loggerFactory,
- IJsonSerializer jsonSerializer,
IServerApplicationHost appHost)
{
_xmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
_appPaths = appPaths;
_logger = loggerFactory.CreateLogger<DlnaManager>();
- _jsonSerializer = jsonSerializer;
_appHost = appHost;
}
@@ -495,9 +495,9 @@ namespace Emby.Dlna
return profile;
}
- var json = _jsonSerializer.SerializeToString(profile);
+ var json = JsonSerializer.Serialize(profile, _jsonOptions);
- return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
+ return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
}
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 175a1ad2b..82490ec31 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
+using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -52,6 +53,8 @@ namespace Emby.Dlna.Main
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
+ private readonly NetworkConfiguration _netConfig;
+ private readonly bool _disabled;
private PlayToManager _manager;
private SsdpDevicePublisher _publisher;
@@ -122,6 +125,13 @@ namespace Emby.Dlna.Main
httpClientFactory,
config);
Current = this;
+
+ _netConfig = config.GetConfiguration<NetworkConfiguration>("network");
+ _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
+ if (_disabled)
+ {
+ _logger.LogError("The DLNA specification does not support HTTPS.");
+ }
}
public static DlnaEntryPoint Current { get; private set; }
@@ -141,6 +151,12 @@ namespace Emby.Dlna.Main
{
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
+ if (_disabled)
+ {
+ // No use starting as dlna won't work, as we're running purely on HTTPS.
+ return;
+ }
+
ReloadComponents();
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
@@ -296,12 +312,15 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
- var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
+ var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
+ // DLNA will only work over http, so we must reset to http:// : {port}
+ uri.Scheme = "http://";
+ uri.Port = _netConfig.HttpServerPortNumber;
var device = new SsdpRootDevice
{
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
- Location = uri, // Must point to the URL that serves your devices UPnP description document.
+ Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
Address = address.Address,
PrefixLength = address.PrefixLength,
FriendlyName = "Jellyfin",
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 486109304..311fae240 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -826,7 +826,7 @@ namespace Emby.Dlna.PlayTo
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
- if (name == SessionMessageType.PlayState)
+ if (name == SessionMessageType.Playstate)
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 198852ec1..8d2486fee 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -49,7 +49,7 @@ namespace Emby.Dlna.Service
{
ControlRequestInfo requestInfo = null;
- using (var streamReader = new StreamReader(request.InputXml))
+ using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
{
var readerSettings = new XmlReaderSettings()
{
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 50ef71a46..1b9bb86bb 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -7,11 +7,11 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
-using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
@@ -50,6 +50,7 @@ using Jellyfin.Networking.Manager;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
@@ -118,12 +119,14 @@ namespace Emby.Server.Implementations
private readonly IFileSystem _fileSystemManager;
private readonly IXmlSerializer _xmlSerializer;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
+ private readonly IPluginManager _pluginManager;
+ private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private string[] _urlPrefixes;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
/// <summary>
/// Gets a value indicating whether this instance can self restart.
@@ -182,16 +185,6 @@ namespace Emby.Server.Implementations
protected IServiceCollection ServiceCollection { get; }
- private IPlugin[] _plugins;
-
- private IReadOnlyList<LocalPlugin> _pluginsManifests;
-
- /// <summary>
- /// Gets the plugins.
- /// </summary>
- /// <value>The plugins.</value>
- public IReadOnlyList<IPlugin> Plugins => _plugins;
-
/// <summary>
/// Gets the logger factory.
/// </summary>
@@ -257,7 +250,6 @@ namespace Emby.Server.Implementations
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
- _jsonSerializer = new JsonSerializer();
ServiceCollection = serviceCollection;
@@ -288,6 +280,13 @@ namespace Emby.Server.Implementations
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
+
+ _pluginManager = new PluginManager(
+ LoggerFactory.CreateLogger<PluginManager>(),
+ this,
+ ServerConfigurationManager.Configuration,
+ ApplicationPaths.PluginsPath,
+ ApplicationVersion);
}
/// <summary>
@@ -387,16 +386,41 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type)
{
+ if (_creatingInstances == null)
+ {
+ _creatingInstances = new List<Type>();
+ }
+
+ if (_creatingInstances.IndexOf(type) != -1)
+ {
+ Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
+ foreach (var entry in _creatingInstances)
+ {
+ Logger.LogError("Called from: {TypeName}", entry.FullName);
+ }
+
+ _pluginManager.FailPlugin(type.Assembly);
+
+ throw new ExternalException("DI Loop detected.");
+ }
+
try
{
+ _creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type);
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error creating {Type}", type);
+ // If this is a plugin fail it.
+ _pluginManager.FailPlugin(type.Assembly);
return null;
}
+ finally
+ {
+ _creatingInstances.Remove(type);
+ }
}
/// <summary>
@@ -406,11 +430,7 @@ namespace Emby.Server.Implementations
/// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>();
- /// <summary>
- /// Gets the export types.
- /// </summary>
- /// <typeparam name="T">The type.</typeparam>
- /// <returns>IEnumerable{Type}.</returns>
+ /// <inheritdoc/>
public IEnumerable<Type> GetExportTypes<T>()
{
var currentType = typeof(T);
@@ -439,6 +459,27 @@ namespace Emby.Server.Implementations
return parts;
}
+ /// <inheritdoc />
+ public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
+ {
+ // Convert to list so this isn't executed for each iteration
+ var parts = GetExportTypes<T>()
+ .Select(i => defaultFunc(i))
+ .Where(i => i != null)
+ .Cast<T>()
+ .ToList();
+
+ if (manageLifetime)
+ {
+ lock (_disposableParts)
+ {
+ _disposableParts.AddRange(parts.OfType<IDisposable>());
+ }
+ }
+
+ return parts;
+ }
+
/// <summary>
/// Runs the startup tasks.
/// </summary>
@@ -511,7 +552,7 @@ namespace Emby.Server.Implementations
RegisterServices();
- RegisterPluginServices();
+ _pluginManager.RegisterServices(ServiceCollection);
}
/// <summary>
@@ -525,11 +566,9 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this);
-
+ ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
- ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
-
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TmdbClientManager>();
@@ -754,7 +793,6 @@ namespace Emby.Server.Implementations
UserView.CollectionManager = Resolve<ICollectionManager>();
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
CollectionFolder.XmlSerializer = _xmlSerializer;
- CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this;
}
@@ -770,34 +808,7 @@ namespace Emby.Server.Implementations
}
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
- _plugins = GetExports<IPlugin>()
- .Where(i => i != null)
- .ToArray();
-
- if (Plugins != null)
- {
- foreach (var plugin in Plugins)
- {
- if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin)
- {
- // Ensure the version number matches the Plugin Manifest information.
- foreach (var item in _pluginsManifests)
- {
- if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase))
- {
- // Update version number to that of the manifest.
- assemblyPlugin.SetAttributes(
- plugin.AssemblyFilePath,
- Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)),
- item.Version);
- break;
- }
- }
- }
-
- Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
- }
- }
+ _pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray();
@@ -836,22 +847,6 @@ namespace Emby.Server.Implementations
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
}
- private void RegisterPluginServices()
- {
- foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
- {
- try
- {
- var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
- instance.RegisterServices(ServiceCollection);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
- }
- }
- }
-
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{
foreach (var ass in assemblies)
@@ -864,11 +859,13 @@ namespace Emby.Server.Implementations
catch (FileNotFoundException ex)
{
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
+ _pluginManager.FailPlugin(ass);
continue;
}
catch (TypeLoadException ex)
{
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
+ _pluginManager.FailPlugin(ass);
continue;
}
@@ -967,7 +964,7 @@ namespace Emby.Server.Implementations
{
return true;
}
-
+
throw new FileNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
@@ -1031,129 +1028,15 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
- /// <inheritdoc/>
- public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
- {
- var minimumVersion = new Version(0, 0, 0, 1);
- var versions = new List<LocalPlugin>();
- if (!Directory.Exists(path))
- {
- // Plugin path doesn't exist, don't try to enumerate subfolders.
- return Enumerable.Empty<LocalPlugin>();
- }
-
- var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
-
- foreach (var dir in directories)
- {
- try
- {
- var metafile = Path.Combine(dir, "meta.json");
- if (File.Exists(metafile))
- {
- var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
-
- if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
- {
- targetAbi = minimumVersion;
- }
-
- if (!Version.TryParse(manifest.Version, out var version))
- {
- version = minimumVersion;
- }
-
- if (ApplicationVersion >= targetAbi)
- {
- // Only load Plugins if the plugin is built for this version or below.
- versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
- }
- }
- else
- {
- // No metafile, so lets see if the folder is versioned.
- metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
-
- int versionIndex = dir.LastIndexOf('_');
- if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion))
- {
- // Versioned folder.
- versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
- }
- else
- {
- // Un-versioned folder - Add it under the path name and version 0.0.0.1.
- versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
- }
- }
- }
- catch
- {
- continue;
- }
- }
-
- string lastName = string.Empty;
- versions.Sort(LocalPlugin.Compare);
- // Traverse backwards through the list.
- // The first item will be the latest version.
- for (int x = versions.Count - 1; x >= 0; x--)
- {
- if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
- {
- versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
- lastName = versions[x].Name;
- continue;
- }
-
- if (!string.IsNullOrEmpty(lastName) && cleanup)
- {
- // Attempt a cleanup of old folders.
- try
- {
- Logger.LogDebug("Deleting {Path}", versions[x].Path);
- Directory.Delete(versions[x].Path, true);
- }
- catch (Exception e)
- {
- Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
- }
-
- versions.RemoveAt(x);
- }
- }
-
- return versions;
- }
-
/// <summary>
/// Gets the composable part assemblies.
/// </summary>
/// <returns>IEnumerable{Assembly}.</returns>
protected IEnumerable<Assembly> GetComposablePartAssemblies()
{
- if (Directory.Exists(ApplicationPaths.PluginsPath))
+ foreach (var p in _pluginManager.LoadAssemblies())
{
- _pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList();
- foreach (var plugin in _pluginsManifests)
- {
- foreach (var file in plugin.DllFiles)
- {
- Assembly plugAss;
- try
- {
- plugAss = Assembly.LoadFrom(file);
- }
- catch (FileLoadException ex)
- {
- Logger.LogError(ex, "Failed to load assembly {Path}", file);
- continue;
- }
-
- Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
- yield return plugAss;
- }
- }
+ yield return p;
}
// Include composable parts in the Model assembly
@@ -1395,17 +1278,6 @@ namespace Emby.Server.Implementations
}
}
- /// <summary>
- /// Removes the plugin.
- /// </summary>
- /// <param name="plugin">The plugin.</param>
- public void RemovePlugin(IPlugin plugin)
- {
- var list = _plugins.ToList();
- list.Remove(plugin);
- _plugins = list.ToArray();
- }
-
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 57684a429..2d5b19fa6 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -3,11 +3,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -21,7 +24,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -44,10 +46,10 @@ namespace Emby.Server.Implementations.Channels
private readonly ILogger<ChannelManager> _logger;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
/// <summary>
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
@@ -59,7 +61,6 @@ namespace Emby.Server.Implementations.Channels
/// <param name="config">The server configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="userDataManager">The user data manager.</param>
- /// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="providerManager">The provider manager.</param>
/// <param name="memoryCache">The memory cache.</param>
public ChannelManager(
@@ -70,7 +71,6 @@ namespace Emby.Server.Implementations.Channels
IServerConfigurationManager config,
IFileSystem fileSystem,
IUserDataManager userDataManager,
- IJsonSerializer jsonSerializer,
IProviderManager providerManager,
IMemoryCache memoryCache)
{
@@ -81,7 +81,6 @@ namespace Emby.Server.Implementations.Channels
_config = config;
_fileSystem = fileSystem;
_userDataManager = userDataManager;
- _jsonSerializer = jsonSerializer;
_providerManager = providerManager;
_memoryCache = memoryCache;
}
@@ -343,7 +342,9 @@ namespace Emby.Server.Implementations.Channels
try
{
- return _jsonSerializer.DeserializeFromFile<List<MediaSourceInfo>>(path) ?? new List<MediaSourceInfo>();
+ var jsonString = File.ReadAllText(path, Encoding.UTF8);
+ return JsonSerializer.Deserialize<List<MediaSourceInfo>>(jsonString, _jsonOptions)
+ ?? new List<MediaSourceInfo>();
}
catch
{
@@ -351,7 +352,7 @@ namespace Emby.Server.Implementations.Channels
}
}
- private void SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
+ private async Task SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
{
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
@@ -370,7 +371,8 @@ namespace Emby.Server.Implementations.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(mediaSources, path);
+ await using FileStream createStream = File.Create(path);
+ await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -812,7 +814,8 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
- var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
+ await using FileStream jsonStream = File.OpenRead(cachePath);
+ var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
return null;
@@ -834,7 +837,8 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
- var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
+ await using FileStream jsonStream = File.OpenRead(cachePath);
+ var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
return null;
@@ -865,7 +869,7 @@ namespace Emby.Server.Implementations.Channels
throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
}
- CacheResponse(result, cachePath);
+ await CacheResponse(result, cachePath);
return result;
}
@@ -875,13 +879,14 @@ namespace Emby.Server.Implementations.Channels
}
}
- private void CacheResponse(object result, string path)
+ private async Task CacheResponse(object result, string path)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(result, path);
+ await using FileStream createStream = File.Create(path);
+ await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -1176,11 +1181,11 @@ namespace Emby.Server.Implementations.Channels
{
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
{
- SaveMediaSources(item, new List<MediaSourceInfo>());
+ await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
}
else
{
- SaveMediaSources(item, info.MediaSources);
+ await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
}
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 686944a28..d5e1f5124 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -582,7 +582,20 @@ namespace Emby.Server.Implementations.Dto
{
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
- baseItemPerson.ImageBlurHashes = dto.ImageBlurHashes;
+ // Only add BlurHash for the person's image.
+ baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+ foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
+ {
+ baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
+ foreach (var (imageId, blurHashValue) in blurHash)
+ {
+ if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
+ {
+ baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
+ }
+ }
+ }
+
list.Add(baseItemPerson);
}
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 7c9a5fbe1..67f23f055 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -29,10 +29,9 @@
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
- <PackageReference Include="ServiceStack.Text.Core" Version="5.10.2" />
<PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
- <PackageReference Include="DotNet.Glob" Version="3.1.0" />
+ <PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup>
<ItemGroup>
@@ -66,5 +65,4 @@
<EmbeddedResource Include="Localization\Core\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.csv" />
</ItemGroup>
-
</Project>
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 3cb025111..5ebc9b61b 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -582,9 +582,7 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
{
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
-
- return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", searchOption));
+ return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
}
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
@@ -594,16 +592,16 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+ var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
{
- return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption));
+ return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
}
- var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption);
+ var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
if (extensions != null && extensions.Count > 0)
{
@@ -625,10 +623,10 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
var directoryInfo = new DirectoryInfo(path);
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+ var enumerationOptions = GetEnumerationOptions(recursive);
- return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption))
- .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption)));
+ return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
+ .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
}
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
@@ -638,8 +636,7 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
{
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
- return Directory.EnumerateDirectories(path, "*", searchOption);
+ return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
}
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
@@ -649,16 +646,16 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+ var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
{
- return Directory.EnumerateFiles(path, "*" + extensions[0], searchOption);
+ return Directory.EnumerateFiles(path, "*" + extensions[0], enumerationOptions);
}
- var files = Directory.EnumerateFiles(path, "*", searchOption);
+ var files = Directory.EnumerateFiles(path, "*", enumerationOptions);
if (extensions != null && extensions.Length > 0)
{
@@ -679,8 +676,16 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
{
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
- return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
+ return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
+ }
+
+ private EnumerationOptions GetEnumerationOptions(bool recursive)
+ {
+ return new EnumerationOptions
+ {
+ RecurseSubdirectories = recursive,
+ IgnoreInaccessible = true
+ };
}
private static void RunProcess(string path, string args, string workingDirectory)
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 041619d1e..2070df31e 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -5,16 +5,17 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library
@@ -23,14 +24,13 @@ namespace Emby.Server.Implementations.Library
{
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger;
- private readonly IJsonSerializer _json;
private readonly IApplicationPaths _appPaths;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
- public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
+ public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths)
{
_mediaEncoder = mediaEncoder;
_logger = logger;
- _json = json;
_appPaths = appPaths;
}
@@ -47,7 +47,8 @@ namespace Emby.Server.Implementations.Library
{
try
{
- mediaInfo = _json.DeserializeFromFile<MediaInfo>(cacheFilePath);
+ await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+ mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
@@ -83,7 +84,8 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- _json.SerializeToFile(mediaInfo, cacheFilePath);
+ await using FileStream createStream = File.OpenWrite(cacheFilePath);
+ await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 928f5f88e..660ec106b 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -6,12 +6,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -23,7 +25,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library
@@ -36,7 +37,6 @@ namespace Emby.Server.Implementations.Library
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly ILogger<MediaSourceManager> _logger;
private readonly IUserDataManager _userDataManager;
@@ -46,6 +46,7 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
private IMediaSourceProvider[] _providers;
@@ -56,7 +57,6 @@ namespace Emby.Server.Implementations.Library
IUserManager userManager,
ILibraryManager libraryManager,
ILogger<MediaSourceManager> logger,
- IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IUserDataManager userDataManager,
IMediaEncoder mediaEncoder)
@@ -65,7 +65,6 @@ namespace Emby.Server.Implementations.Library
_userManager = userManager;
_libraryManager = libraryManager;
_logger = logger;
- _jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
_userDataManager = userDataManager;
_mediaEncoder = mediaEncoder;
@@ -504,7 +503,7 @@ namespace Emby.Server.Implementations.Library
// hack - these two values were taken from LiveTVMediaSourceProvider
string cacheKey = request.OpenToken;
- await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _appPaths)
+ await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken)
.ConfigureAwait(false);
}
@@ -516,9 +515,9 @@ namespace Emby.Server.Implementations.Library
}
// TODO: @bond Fix
- var json = _jsonSerializer.SerializeToString(mediaSource);
+ var json = JsonSerializer.Serialize(mediaSource, _jsonOptions);
_logger.LogInformation("Live stream opened: " + json);
- var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
+ var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
if (!request.UserId.Equals(Guid.Empty))
{
@@ -643,7 +642,8 @@ namespace Emby.Server.Implementations.Library
{
try
{
- mediaInfo = _jsonSerializer.DeserializeFromFile<MediaInfo>(cacheFilePath);
+ await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+ mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
@@ -679,7 +679,8 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- _jsonSerializer.SerializeToFile(mediaInfo, cacheFilePath);
+ await using FileStream createStream = File.Create(cacheFilePath);
+ await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 2c4497c69..90b6a8a7d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
- public override ResolverPriority Priority => ResolverPriority.Fourth;
+ public override ResolverPriority Priority => ResolverPriority.Fifth;
public MultiItemResolverResult ResolveMultiple(
Folder parent,
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 18ceb5e76..bf32381eb 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
- public override ResolverPriority Priority => ResolverPriority.Second;
+ public override ResolverPriority Priority => ResolverPriority.Third;
/// <summary>
/// Resolves the specified args.
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index baf0e3cf9..8ef7172de 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
- public override ResolverPriority Priority => ResolverPriority.Third;
+ public override ResolverPriority Priority => ResolverPriority.Fourth;
/// <inheritdoc />
public MultiItemResolverResult ResolveMultiple(
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index 41561916f..c76d41e5c 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
// It's a directory-based playlist if the directory contains a playlist file
- var filePaths = Directory.EnumerateFiles(args.Path);
+ var filePaths = Directory.EnumerateFiles(args.Path, "*", new EnumerationOptions { IgnoreInaccessible = true });
if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
{
return new Playlist
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 1d9529dff..94602582b 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library
if (query.Limit.HasValue)
{
- results = results.GetRange(0, query.Limit.Value);
+ results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
}
return new QueryResult<SearchHintInfo>
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index f9e5e6bbc..d16275b19 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Book = MediaBrowser.Controller.Entities.Book;
+using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
namespace Emby.Server.Implementations.Library
{
@@ -219,7 +220,7 @@ namespace Emby.Server.Implementations.Library
var hasRuntime = runtimeTicks > 0;
// If a position has been reported, and if we know the duration
- if (positionTicks > 0 && hasRuntime)
+ if (positionTicks > 0 && hasRuntime && !(item is AudioBook))
{
var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
@@ -245,6 +246,23 @@ namespace Emby.Server.Implementations.Library
}
}
}
+ else if (positionTicks > 0 && hasRuntime && item is AudioBook)
+ {
+ var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes;
+ var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
+
+ if (minIn > _config.Configuration.MinAudiobookResume)
+ {
+ // ignore progress during the beginning
+ positionTicks = 0;
+ }
+ else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
+ {
+ // mark as completed close to the end
+ positionTicks = 0;
+ data.Played = playedToCompletion = true;
+ }
+ }
else if (!hasRuntime)
{
// If we don't know the runtime we'll just have to assume it was fully played
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 0dc045ee6..2c0de661d 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -36,7 +36,6 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -51,7 +50,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ILogger<EmbyTV> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
- private readonly IJsonSerializer _jsonSerializer;
private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
private readonly TimerManager _timerProvider;
@@ -81,7 +79,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IStreamHelper streamHelper,
IMediaSourceManager mediaSourceManager,
ILogger<EmbyTV> logger,
- IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
ILiveTvManager liveTvManager,
@@ -103,12 +100,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_providerManager = providerManager;
_mediaEncoder = mediaEncoder;
_liveTvManager = (LiveTvManager)liveTvManager;
- _jsonSerializer = jsonSerializer;
_mediaSourceManager = mediaSourceManager;
_streamHelper = streamHelper;
- _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json"));
- _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"));
+ _seriesTimerProvider = new SeriesTimerManager(_logger, Path.Combine(DataPath, "seriestimers.json"));
+ _timerProvider = new TimerManager(_logger, Path.Combine(DataPath, "timers.json"));
_timerProvider.TimerFired += OnTimerProviderTimerFired;
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
@@ -1052,7 +1048,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IgnoreIndex = true
};
- await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _config.CommonApplicationPaths)
+ await new LiveStreamHelper(_mediaEncoder, _logger, _config.CommonApplicationPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
return new List<MediaSourceInfo>
@@ -1635,7 +1631,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
{
- return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
+ return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _config);
}
return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index e6ee9819e..78a82118e 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -6,16 +6,17 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -25,10 +26,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ILogger _logger;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationPaths _appPaths;
- private readonly IJsonSerializer _json;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
private readonly IServerConfigurationManager _serverConfigurationManager;
-
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
private bool _hasExited;
private Stream _logFileStream;
private string _targetPath;
@@ -38,13 +38,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
ILogger logger,
IMediaEncoder mediaEncoder,
IServerApplicationPaths appPaths,
- IJsonSerializer json,
IServerConfigurationManager serverConfigurationManager)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
- _json = json;
_serverConfigurationManager = serverConfigurationManager;
}
@@ -66,7 +64,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogInformation("Recording completed to file {0}", targetFile);
}
- private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
+ private async Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
_targetPath = targetFile;
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
@@ -95,8 +93,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
_logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
- var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
- _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
+ await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
_process = new Process
{
@@ -115,8 +113,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
_logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
-
- return _taskCompletionSource.Task;
}
private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile, TimeSpan duration)
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index fc543dc55..c80ecd6b3 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -4,7 +4,10 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using MediaBrowser.Model.Serialization;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Json;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -12,18 +15,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class ItemDataProvider<T>
where T : class
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly string _dataPath;
private readonly object _fileDataLock = new object();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
private T[] _items;
public ItemDataProvider(
- IJsonSerializer jsonSerializer,
ILogger logger,
string dataPath,
Func<T, T, bool> equalityComparer)
{
- _jsonSerializer = jsonSerializer;
Logger = logger;
_dataPath = dataPath;
EqualityComparer = equalityComparer;
@@ -46,7 +47,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try
{
- _items = _jsonSerializer.DeserializeFromFile<T[]>(_dataPath);
+ var jsonString = File.ReadAllText(_dataPath, Encoding.UTF8);
+ _items = JsonSerializer.Deserialize<T[]>(jsonString, _jsonOptions);
return;
}
catch (Exception ex)
@@ -61,7 +63,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void SaveList()
{
Directory.CreateDirectory(Path.GetDirectoryName(_dataPath));
- _jsonSerializer.SerializeToFile(_items, _dataPath);
+ var jsonString = JsonSerializer.Serialize(_items, _jsonOptions);
+ File.WriteAllText(_dataPath, jsonString);
}
public IReadOnlyList<T> GetAll()
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
index 194e4606d..da707fec6 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
@@ -9,8 +9,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo>
{
- public SeriesTimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
- : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
+ public SeriesTimerManager(ILogger logger, string dataPath)
+ : base(logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index dd479b7d1..1efa90e25 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -8,7 +8,6 @@ using System.Threading;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -17,8 +16,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase);
- public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
- : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
+ public TimerManager(ILogger logger, string dataPath)
+ : base(logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index b19ccadd8..7567ea312 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -9,16 +9,17 @@ using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.Listings
@@ -28,7 +29,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
private readonly ILogger<SchedulesDirect> _logger;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly IApplicationHost _appHost;
@@ -36,16 +36,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private DateTime _lastErrorResponse;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
- IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory,
IApplicationHost appHost,
ICryptoProvider cryptoProvider)
{
_logger = logger;
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_cryptoProvider = cryptoProvider;
@@ -104,7 +103,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
};
- var requestString = _jsonSerializer.SerializeToString(requestList);
+ var requestString = JsonSerializer.Serialize(requestList, _jsonOptions);
_logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
@@ -112,7 +111,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(responseStream).ConfigureAwait(false);
+ var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions).ConfigureAwait(false);
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
@@ -123,7 +122,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream).ConfigureAwait(false);
+ var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions).ConfigureAwait(false);
var programDict = programDetails.ToDictionary(p => p.programID, y => y);
var programIdsWithImages =
@@ -479,7 +478,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(response).ConfigureAwait(false);
+ return await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ShowImages>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -508,7 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Headends>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (root != null)
{
@@ -649,7 +648,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Token>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
{
_logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
@@ -705,7 +704,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
- var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions).ConfigureAwait(false);
return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
}
@@ -777,7 +776,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions).ConfigureAwait(false);
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
_logger.LogInformation("Mapping Stations to Channel");
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 84c8a99f7..a23037af8 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -14,8 +14,8 @@
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
- "HeaderAlbumArtists": "এলবাম শিল্পী",
- "Genres": "জেনার",
+ "HeaderAlbumArtists": "এলবাম শিল্পীবৃন্দ",
+ "Genres": "শৈলী",
"Folders": "ফোল্ডারগুলো",
"Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
@@ -114,5 +114,8 @@
"TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন",
"TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।",
"Undefined": "অসঙ্গায়িত",
- "Forced": "জোরপূর্বক"
+ "Forced": "জোরকরে",
+ "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন",
+ "TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
+ "Default": "প্রাথমিক"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 6ab22b8a4..4a505d0b3 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -94,22 +94,22 @@
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Durchsucht das Internet nach fehlenden Untertiteln, basierend auf den Meta Einstellungen.",
"TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter",
- "TaskRefreshChannelsDescription": "Erneuere Internet Kanal Informationen.",
- "TaskRefreshChannels": "Erneuere Kanäle",
- "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.",
- "TaskCleanTranscode": "Lösche Transkodier Pfad",
- "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
- "TaskUpdatePlugins": "Update Plugins",
- "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
- "TaskRefreshPeople": "Erneuere Schauspieler",
- "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
- "TaskCleanLogs": "Lösche Log Pfad",
- "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
+ "TaskRefreshChannelsDescription": "Aktualisiere Internet Kanal Informationen.",
+ "TaskRefreshChannels": "Aktualisiere Kanäle",
+ "TaskCleanTranscodeDescription": "Löscht Transkodierdateien, welche älter als einen Tag sind.",
+ "TaskCleanTranscode": "Lösche Transkodier-Pfad",
+ "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
+ "TaskUpdatePlugins": "Aktualisiere Plugins",
+ "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
+ "TaskRefreshPeople": "Aktualisiere Schauspieler",
+ "TaskCleanLogsDescription": "Lösche Log Dateien, die älter als {0} Tage sind.",
+ "TaskCleanLogs": "Lösche Log-Verzeichnis",
+ "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiere Metadaten.",
"TaskRefreshLibrary": "Scanne Medien-Bibliothek",
- "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
+ "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, welche Kapitel besitzen.",
"TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
- "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
- "TaskCleanCache": "Leere Cache Pfad",
+ "TaskCleanCacheDescription": "Löscht nicht mehr benötigte Zwischenspeicherdateien.",
+ "TaskCleanCache": "Leere Zwischenspeicher",
"TasksChannelsCategory": "Internet Kanäle",
"TasksApplicationCategory": "Anwendung",
"TasksLibraryCategory": "Bibliothek",
diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json
index ee1f8775e..3364ee333 100644
--- a/Emby.Server.Implementations/Localization/Core/gsw.json
+++ b/Emby.Server.Implementations/Localization/Core/gsw.json
@@ -113,5 +113,10 @@
"TaskUpdatePlugins": "Aktualisiere Erweiterungen",
"TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schausteller und Regisseure in deiner Bibliothek.",
"TaskRefreshPeople": "Aktualisiere Schauspieler",
- "TaskCleanLogsDescription": "Löscht Log Dateien die älter als {0} Tage sind."
+ "TaskCleanLogsDescription": "Löscht Log Dateien die älter als {0} Tage sind.",
+ "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
+ "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen",
+ "Undefined": "Undefiniert",
+ "Forced": "Erzwungen",
+ "Default": "Standard"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index df68d3bbd..4cc2b378b 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -1,3 +1,30 @@
{
- "Albums": "आल्बुम्"
+ "Albums": "संग्रह",
+ "HeaderRecordingGroups": "रिकॉर्डिंग समूह",
+ "HeaderNextUp": "इसके बाद",
+ "HeaderLiveTV": "लाइव टीवी",
+ "HeaderFavoriteSongs": "पसंदीदा गीत",
+ "HeaderFavoriteShows": "पसंदीदा शोज",
+ "HeaderFavoriteEpisodes": "पसंदीदा एपिसोड्स",
+ "HeaderFavoriteArtists": "पसंदीदा कलाकारसमूह",
+ "HeaderFavoriteAlbums": "पसंदीदा एलबम्स",
+ "HeaderContinueWatching": "देखते रहिए",
+ "HeaderAlbumArtists": "एल्बम कलकरसमुह",
+ "Genres": "शैली",
+ "Forced": "बलपूर्वक",
+ "Folders": "फोल्डेरें",
+ "Favorites": "पसंदीदा",
+ "FailedLoginAttemptWithUserName": "{0} से लॉगिन असफल हुआ है",
+ "DeviceOnlineWithName": "{0} से संयोग हो गया है",
+ "DeviceOfflineWithName": "{0} से संयोग विच्छिन्न हो गया है",
+ "Default": "प्राथमिक",
+ "Collections": "संग्रह",
+ "ChapterNameValue": "अध्याय",
+ "Channels": "चैनल",
+ "CameraImageUploadedFrom": "कैमरा से एक नया चित्र अपलोड किया गया है",
+ "Books": "किताब",
+ "AuthenticationSucceededWithUserName": "सफलता से प्रमाणीकृत",
+ "Artists": "कलाकारों",
+ "Application": "एप्लिकेशन",
+ "AppDeviceValues": "एप: {0}, मशीन: {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index b8b39833c..9179bbc8d 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -115,5 +115,8 @@
"TasksChannelsCategory": "인터넷 채널",
"TasksLibraryCategory": "라이브러리",
"TaskCleanActivityLogDescription": "구성된 기간보다 오래된 활동내역 삭제.",
- "TaskCleanActivityLog": "활동내역청소"
+ "TaskCleanActivityLog": "활동내역청소",
+ "Undefined": "일치하지 않음",
+ "Forced": "강제하기",
+ "Default": "기본 설정"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json
new file mode 100644
index 000000000..e764963cf
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/ml.json
@@ -0,0 +1,121 @@
+{
+ "AppDeviceValues": "അപ്ലിക്കേഷൻ: {0}, ഉപകരണം: {1}",
+ "Application": "അപ്ലിക്കേഷൻ",
+ "AuthenticationSucceededWithUserName": "{0} വിജയകരമായി പ്രാമാണീകരിച്ചു",
+ "CameraImageUploadedFrom": "Camera 0 from എന്നതിൽ നിന്ന് ഒരു പുതിയ ക്യാമറ ചിത്രം അപ്‌ലോഡുചെയ്‌തു",
+ "ChapterNameValue": "അധ്യായം {0}",
+ "DeviceOfflineWithName": "{0} വിച്ഛേദിച്ചു",
+ "DeviceOnlineWithName": "{0} ബന്ധിപ്പിച്ചു",
+ "FailedLoginAttemptWithUserName": "Log 0 from എന്നതിൽ നിന്നുള്ള പ്രവേശന ശ്രമം പരാജയപ്പെട്ടു",
+ "Forced": "നിർബന്ധിച്ചു",
+ "HeaderFavoriteAlbums": "പ്രിയപ്പെട്ട ആൽബങ്ങൾ",
+ "HeaderFavoriteArtists": "പ്രിയപ്പെട്ട കലാകാരന്മാർ",
+ "HeaderFavoriteEpisodes": "പ്രിയപ്പെട്ട എപ്പിസോഡുകൾ",
+ "HeaderFavoriteShows": "പ്രിയപ്പെട്ട ഷോകൾ",
+ "HeaderFavoriteSongs": "പ്രിയപ്പെട്ട ഗാനങ്ങൾ",
+ "HeaderLiveTV": "തത്സമയ ടിവി",
+ "HeaderNextUp": "അടുത്തത്",
+ "HeaderRecordingGroups": "ഗ്രൂപ്പുകൾ റെക്കോർഡുചെയ്യുന്നു",
+ "HomeVideos": "ഹോം വീഡിയോകൾ",
+ "Inherit": "അനന്തരാവകാശം",
+ "ItemAddedWithName": "{0} ലൈബ്രറിയിൽ ചേർത്തു",
+ "ItemRemovedWithName": "{0} ലൈബ്രറിയിൽ നിന്ന് നീക്കംചെയ്തു",
+ "LabelIpAddressValue": "IP വിലാസം: {0}",
+ "LabelRunningTimeValue": "പ്രവർത്തന സമയം: {0}",
+ "Latest": "ഏറ്റവും പുതിയ",
+ "MessageApplicationUpdated": "ജെല്ലിഫിൻ സെർവർ അപ്‌ഡേറ്റുചെയ്‌തു",
+ "MessageApplicationUpdatedTo": "ജെല്ലിഫിൻ സെർവർ {0 to ലേക്ക് അപ്‌ഡേറ്റുചെയ്‌തു",
+ "MessageNamedServerConfigurationUpdatedWithValue": "സെർവർ കോൺഫിഗറേഷൻ വിഭാഗം {0 അപ്‌ഡേറ്റുചെയ്‌തു",
+ "MessageServerConfigurationUpdated": "സെർവർ കോൺഫിഗറേഷൻ അപ്‌ഡേറ്റുചെയ്‌തു",
+ "MixedContent": "മിശ്രിത ഉള്ളടക്കം",
+ "Music": "സംഗീതം",
+ "MusicVideos": "സംഗീത വീഡിയോകൾ",
+ "NameInstallFailed": "{0} ഇൻസ്റ്റാളേഷൻ പരാജയപ്പെട്ടു",
+ "NameSeasonNumber": "സീസൺ {0}",
+ "NameSeasonUnknown": "സീസൺ അജ്ഞാതം",
+ "NewVersionIsAvailable": "ജെല്ലിഫിൻ സെർവറിന്റെ പുതിയ പതിപ്പ് ഡ .ൺ‌ലോഡിനായി ലഭ്യമാണ്.",
+ "NotificationOptionApplicationUpdateAvailable": "അപ്ലിക്കേഷൻ അപ്‌ഡേറ്റ് ലഭ്യമാണ്",
+ "NotificationOptionApplicationUpdateInstalled": "അപ്ലിക്കേഷൻ അപ്‌ഡേറ്റ് ഇൻസ്റ്റാളുചെയ്‌തു",
+ "NotificationOptionAudioPlayback": "ഓഡിയോ പ്ലേബാക്ക് ആരംഭിച്ചു",
+ "NotificationOptionAudioPlaybackStopped": "ഓഡിയോ പ്ലേബാക്ക് നിർത്തി",
+ "NotificationOptionCameraImageUploaded": "ക്യാമറ ചിത്രം അപ്‌ലോഡുചെയ്‌തു",
+ "NotificationOptionInstallationFailed": "ഇൻസ്റ്റാളേഷൻ പരാജയം",
+ "NotificationOptionNewLibraryContent": "പുതിയ ഉള്ളടക്കം ചേർത്തു",
+ "NotificationOptionPluginError": "പ്ലഗിൻ പരാജയം",
+ "NotificationOptionPluginInstalled": "പ്ലഗിൻ ഇൻസ്റ്റാളുചെയ്‌തു",
+ "NotificationOptionPluginUninstalled": "പ്ലഗിൻ അൺഇൻസ്റ്റാൾ ചെയ്തു",
+ "NotificationOptionPluginUpdateInstalled": "പ്ലഗിൻ അപ്‌ഡേറ്റ് ഇൻസ്റ്റാളുചെയ്‌തു",
+ "NotificationOptionServerRestartRequired": "സെർവർ പുനരാരംഭിക്കൽ ആവശ്യമാണ്",
+ "NotificationOptionTaskFailed": "ഷെഡ്യൂൾ ചെയ്ത ടാസ്‌ക് പരാജയം",
+ "NotificationOptionUserLockedOut": "ഉപയോക്താവ് ലോക്ക് out ട്ട് ചെയ്‌തു",
+ "NotificationOptionVideoPlayback": "വീഡിയോ പ്ലേബാക്ക് ആരംഭിച്ചു",
+ "NotificationOptionVideoPlaybackStopped": "വീഡിയോ പ്ലേബാക്ക് നിർത്തി",
+ "Plugin": "പ്ലഗിൻ",
+ "PluginInstalledWithName": "{0} ഇൻസ്റ്റാളുചെയ്‌തു",
+ "PluginUninstalledWithName": "{0 un അൺഇൻസ്റ്റാൾ ചെയ്തു",
+ "PluginUpdatedWithName": "{0} അപ്‌ഡേറ്റുചെയ്‌തു",
+ "ProviderValue": "ദാതാവ്: {0}",
+ "ScheduledTaskFailedWithName": "{0} പരാജയപ്പെട്ടു",
+ "ScheduledTaskStartedWithName": "{0} ആരംഭിച്ചു",
+ "ServerNameNeedsToBeRestarted": "{0} പുനരാരംഭിക്കേണ്ടതുണ്ട്",
+ "StartupEmbyServerIsLoading": "ജെല്ലിഫിൻ സെർവർ ലോഡുചെയ്യുന്നു. ഉടൻ തന്നെ വീണ്ടും ശ്രമിക്കുക.",
+ "SubtitleDownloadFailureFromForItem": "സബ്ടൈറ്റിലുകൾ {1} ന് {0 from ൽ നിന്ന് ഡ download ൺലോഡ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു",
+ "System": "സിസ്റ്റം",
+ "TvShows": "ടിവി ഷോകൾ",
+ "Undefined": "നിർവചിച്ചിട്ടില്ല",
+ "User": "ഉപയോക്താവ്",
+ "UserCreatedWithName": "ഉപയോക്താവ് {0 created സൃഷ്ടിച്ചു",
+ "UserDeletedWithName": "ഉപയോക്താവ് {0 deleted ഇല്ലാതാക്കി",
+ "UserDownloadingItemWithValues": "{0} ഡൗൺലോഡുചെയ്യുന്നു {1}",
+ "UserLockedOutWithName": "{0} ഉപയോക്താവ് ലോക്ക് out ട്ട് ചെയ്‌തു",
+ "UserOfflineFromDevice": "{0} {1} ൽ നിന്ന് വിച്ഛേദിച്ചു",
+ "UserOnlineFromDevice": "{0} {1} മുതൽ ഓൺ‌ലൈനിലാണ്",
+ "UserPasswordChangedWithName": "{0} ഉപയോക്താവിനായി പാസ്‌വേഡ് മാറ്റി",
+ "UserPolicyUpdatedWithName": "{0} എന്നതിനായി ഉപയോക്തൃ നയം അപ്‌ഡേറ്റുചെയ്‌തു",
+ "UserStartedPlayingItemWithValues": "{0} {2} ൽ {1} പ്ലേ ചെയ്യുന്നു",
+ "UserStoppedPlayingItemWithValues": "{0} {2} ൽ {1 play കളിക്കുന്നത് പൂർത്തിയാക്കി",
+ "ValueHasBeenAddedToLibrary": "Media 0 your നിങ്ങളുടെ മീഡിയ ലൈബ്രറിയിലേക്ക് ചേർത്തു",
+ "VersionNumber": "പതിപ്പ് {0}",
+ "TasksMaintenanceCategory": "പരിപാലനം",
+ "TasksLibraryCategory": "പുസ്തകശാല",
+ "TasksApplicationCategory": "അപ്ലിക്കേഷൻ",
+ "TasksChannelsCategory": "ഇന്റർനെറ്റ് ചാനലുകൾ",
+ "TaskCleanActivityLog": "പ്രവർത്തന ലോഗ് വൃത്തിയാക്കുക",
+ "TaskCleanActivityLogDescription": "കോൺഫിഗർ ചെയ്‌ത പ്രായത്തേക്കാൾ പഴയ പ്രവർത്തന ലോഗ് എൻട്രികൾ ഇല്ലാതാക്കുന്നു.",
+ "TaskCleanCache": "കാഷെ ഡയറക്ടറി വൃത്തിയാക്കുക",
+ "TaskCleanCacheDescription": "സിസ്റ്റത്തിന് ഇനി ആവശ്യമില്ലാത്ത കാഷെ ഫയലുകൾ ഇല്ലാതാക്കുന്നു.",
+ "TaskRefreshChapterImages": "ചാപ്റ്റർ ഇമേജുകൾ എക്‌സ്‌ട്രാക്റ്റുചെയ്യുക",
+ "TaskRefreshChapterImagesDescription": "അധ്യായങ്ങളുള്ള വീഡിയോകൾക്കായി ലഘുചിത്രങ്ങൾ സൃഷ്ടിക്കുന്നു.",
+ "TaskRefreshLibrary": "മീഡിയ ലൈബ്രറി സ്കാൻ ചെയ്യുക",
+ "TaskRefreshLibraryDescription": "പുതിയ ഫയലുകൾക്കായി നിങ്ങളുടെ മീഡിയ ലൈബ്രറി സ്കാൻ ചെയ്യുകയും മെറ്റാഡാറ്റ പുതുക്കുകയും ചെയ്യുന്നു.",
+ "TaskCleanLogs": "ലോഗ് ഡയറക്ടറി വൃത്തിയാക്കുക",
+ "TaskCleanLogsDescription": "Log 0} ദിവസത്തിൽ കൂടുതൽ പഴക്കമുള്ള ലോഗ് ഫയലുകൾ ഇല്ലാതാക്കുന്നു.",
+ "TaskRefreshPeople": "ആളുകളെ പുതുക്കുക",
+ "TaskRefreshPeopleDescription": "നിങ്ങളുടെ മീഡിയ ലൈബ്രറിയിലെ അഭിനേതാക്കൾക്കും സംവിധായകർക്കും മെറ്റാഡാറ്റ അപ്‌ഡേറ്റുചെയ്യുന്നു.",
+ "TaskUpdatePlugins": "പ്ലഗിനുകൾ അപ്‌ഡേറ്റുചെയ്യുക",
+ "TaskUpdatePluginsDescription": "യാന്ത്രികമായി അപ്‌ഡേറ്റുചെയ്യുന്നതിന് കോൺഫിഗർ ചെയ്‌തിരിക്കുന്ന പ്ലഗിനുകൾക്കായുള്ള അപ്‌ഡേറ്റുകൾ ഡൗൺലോഡുചെയ്യുകയും ഇൻസ്റ്റാളുചെയ്യുകയും ചെയ്യുന്നു.",
+ "TaskCleanTranscode": "ട്രാൻസ്‌കോഡ് ഡയറക്‌ടറി വൃത്തിയാക്കുക",
+ "TaskCleanTranscodeDescription": "ഒരു ദിവസത്തിൽ കൂടുതൽ പഴക്കമുള്ള ട്രാൻസ്‌കോഡ് ഫയലുകൾ ഇല്ലാതാക്കുന്നു.",
+ "TaskRefreshChannels": "ചാനലുകൾ പുതുക്കുക",
+ "TaskRefreshChannelsDescription": "ഇന്റർനെറ്റ് ചാനൽ വിവരങ്ങൾ പുതുക്കുന്നു.",
+ "TaskDownloadMissingSubtitles": "നഷ്‌ടമായ സബ്‌ടൈറ്റിലുകൾ ഡൗൺലോഡുചെയ്യുക",
+ "TaskDownloadMissingSubtitlesDescription": "മെറ്റാഡാറ്റ കോൺഫിഗറേഷനെ അടിസ്ഥാനമാക്കി നഷ്‌ടമായ സബ്‌ടൈറ്റിലുകൾക്കായി ഇന്റർനെറ്റ് തിരയുന്നു.",
+ "ValueSpecialEpisodeName": "പ്രത്യേക - {0}",
+ "Collections": "ശേഖരങ്ങൾ",
+ "Folders": "ഫോൾഡറുകൾ",
+ "HeaderAlbumArtists": "ആൽബം ആർട്ടിസ്റ്റുകൾ",
+ "Sync": "സമന്വയിപ്പിക്കുക",
+ "Movies": "സിനിമകൾ",
+ "Photos": "ഫോട്ടോകൾ",
+ "Albums": "ആൽബങ്ങൾ",
+ "Playlists": "പ്ലേലിസ്റ്റുകൾ",
+ "Songs": "ഗാനങ്ങൾ",
+ "HeaderContinueWatching": "കാണുന്നത് തുടരുക",
+ "Artists": "കലാകാരന്മാർ",
+ "Shows": "ഷോകൾ",
+ "Default": "സ്ഥിരസ്ഥിതി",
+ "Favorites": "പ്രിയങ്കരങ്ങൾ",
+ "Books": "പുസ്തകങ്ങൾ",
+ "Genres": "വിഭാഗങ്ങൾ",
+ "Channels": "ചാനലുകൾ"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 1e80d0b5f..ffc329e35 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -26,7 +26,7 @@
"HeaderNextUp": "Volgende",
"HeaderRecordingGroups": "Opnamegroepen",
"HomeVideos": "Home video's",
- "Inherit": "Overerven",
+ "Inherit": "Erven",
"ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
"ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
"LabelIpAddressValue": "IP-adres: {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json
new file mode 100644
index 000000000..469fa89b6
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/pa.json
@@ -0,0 +1,121 @@
+{
+ "TaskRefreshChapterImages": "ਐਬਸਟਰੈਕਟ ਅਧਿਆਇ ਅਧਿਆਇ",
+ "TaskDownloadMissingSubtitlesDescription": "ਮੈਟਾਡੇਟਾ ਕੌਂਫਿਗਰੇਸ਼ਨ ਦੇ ਅਧਾਰ ਤੇ ਗਾਇਬ ਉਪਸਿਰਲੇਖਾਂ ਲਈ ਇੰਟਰਨੈਟ ਦੀ ਭਾਲ ਕਰਦਾ ਹੈ.",
+ "TaskDownloadMissingSubtitles": "ਗਾਇਬ ਉਪਸਿਰਲੇਖ ਡਾ Download ਨਲੋਡ ਕਰੋ",
+ "TaskRefreshChannelsDescription": "ਇੰਟਰਨੈੱਟ ਚੈਨਲ ਦੀ ਜਾਣਕਾਰੀ ਨੂੰ ਤਾਜ਼ਾ ਕਰਦਾ ਹੈ.",
+ "TaskRefreshChannels": "ਚੈਨਲਾਂ ਨੂੰ ਤਾਜ਼ਾ ਕਰੋ",
+ "TaskCleanTranscodeDescription": "ਇੱਕ ਦਿਨ ਤੋਂ ਵੱਧ ਪੁਰਾਣੀ ਟ੍ਰਾਂਸਕੋਡ ਫਾਈਲਾਂ ਨੂੰ ਮਿਟਾਉਂਦਾ ਹੈ.",
+ "TaskCleanTranscode": "ਕਲੀਨ ਟ੍ਰਾਂਸਕੋਡ ਡਾਇਰੈਕਟਰੀ",
+ "TaskUpdatePluginsDescription": "ਪਲਗਇੰਸਾਂ ਲਈ ਡਾਉਨਲੋਡ ਅਤੇ ਸਥਾਪਨਾ ਅਪਡੇਟਾਂ ਜੋ ਆਪਣੇ ਆਪ ਅਪਡੇਟ ਕਰਨ ਲਈ ਕੌਂਫਿਗਰ ਕੀਤੀਆਂ ਜਾਂਦੀਆਂ ਹਨ.",
+ "TaskUpdatePlugins": "ਪਲੱਗਇਨ ਅਪਡੇਟ ਕਰੋ",
+ "TaskRefreshPeopleDescription": "ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਅਦਾਕਾਰਾਂ ਅਤੇ ਨਿਰਦੇਸ਼ਕਾਂ ਲਈ ਮੈਟਾਡੇਟਾ ਨੂੰ ਅਪਡੇਟ ਕਰਦਾ ਹੈ.",
+ "TaskRefreshPeople": "ਲੋਕਾਂ ਨੂੰ ਤਾਜ਼ਾ ਕਰੋ",
+ "TaskCleanLogsDescription": "ਲੌਗ ਫਾਈਲਾਂ ਨੂੰ ਮਿਟਾਉਂਦਾ ਹੈ ਜੋ {0} ਦਿਨਾਂ ਤੋਂ ਵੱਧ ਪੁਰਾਣੀਆਂ ਹਨ.",
+ "TaskCleanLogs": "ਕਲੀਨ ਲਾਗ ਡਾਇਰੈਕਟਰੀ",
+ "TaskRefreshLibraryDescription": "ਨਵੀਆਂ ਫਾਈਲਾਂ ਲਈ ਆਪਣੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਕੈਨ ਕਰਦਾ ਹੈ ਅਤੇ ਮੈਟਾਡੇਟਾ ਨੂੰ ਤਾਜ਼ਾ ਕਰਦਾ ਹੈ.",
+ "TaskRefreshLibrary": "ਸਕੈਨ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ",
+ "TaskRefreshChapterImagesDescription": "ਚੈਪਟਰਾਂ ਵਾਲੇ ਵੀਡੀਓ ਲਈ ਥੰਬਨੇਲ ਬਣਾਉਂਦੇ ਹਨ.",
+ "TaskCleanCacheDescription": "ਸਿਸਟਮ ਦੁਆਰਾ ਹੁਣ ਕੈਚੇ ਫਾਈਲਾਂ ਦੀ ਜਰੂਰਤ ਨਹੀਂ ਹੈ.",
+ "TaskCleanCache": "ਸਾਫ਼ ਕੈਸ਼ ਡਾਇਰੈਕਟਰੀ",
+ "TaskCleanActivityLogDescription": "ਕੌਂਫਿਗਰ ਕੀਤੀ ਉਮਰ ਤੋਂ ਪੁਰਾਣੀ ਗਤੀਵਿਧੀ ਲੌਗ ਐਂਟਰੀਜ ਨੂੰ ਮਿਟਾਉਂਦਾ ਹੈ.",
+ "TaskCleanActivityLog": "ਸਾਫ਼ ਗਤੀਵਿਧੀ ਲਾਗ",
+ "TasksChannelsCategory": "ਇੰਟਰਨੈੱਟ ਚੈਨਲ",
+ "TasksApplicationCategory": "ਐਪਲੀਕੇਸ਼ਨ",
+ "TasksLibraryCategory": "ਲਾਇਬ੍ਰੇਰੀ",
+ "TasksMaintenanceCategory": "ਰੱਖ-ਰਖਾਅ",
+ "VersionNumber": "ਵਰਜਨ {0}",
+ "ValueSpecialEpisodeName": "ਵਿਸ਼ੇਸ਼ - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ",
+ "UserStoppedPlayingItemWithValues": "{0} ਨੇ {2} 'ਤੇ {1} ਖੇਡਣਾ ਪੂਰਾ ਕਰ ਲਿਆ ਹੈ",
+ "UserStartedPlayingItemWithValues": "{0} {2} 'ਤੇ {1} ਖੇਡ ਰਿਹਾ ਹੈ",
+ "UserPolicyUpdatedWithName": "ਉਪਭੋਗਤਾ ਨੀਤੀ ਨੂੰ {0} ਲਈ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
+ "UserPasswordChangedWithName": "ਪਾਸਵਰਡ ਯੂਜ਼ਰ ਲਈ ਬਦਲਿਆ ਗਿਆ ਹੈ {0}",
+ "UserOnlineFromDevice": "{0} ਤੋਂ isਨਲਾਈਨ ਹੈ {1}",
+ "UserOfflineFromDevice": "{0} ਤੋਂ ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ {1}",
+ "UserLockedOutWithName": "ਯੂਜ਼ਰ {0} ਨੂੰ ਲਾਕ ਆਉਟ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ",
+ "UserDownloadingItemWithValues": "{0} ਡਾ{ਨਲੋਡ ਕਰ ਰਿਹਾ ਹੈ {1}",
+ "UserDeletedWithName": "ਯੂਜ਼ਰ {0} ਨੂੰ ਮਿਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ",
+ "UserCreatedWithName": "ਯੂਜ਼ਰ {0} ਬਣਾਇਆ ਗਿਆ ਹੈ",
+ "User": "ਯੂਜ਼ਰ",
+ "Undefined": "ਪਰਿਭਾਸ਼ਤ",
+ "TvShows": "ਟੀਵੀ ਸ਼ੋਅਜ਼",
+ "System": "ਸਿਸਟਮ",
+ "Sync": "ਸਿੰਕ",
+ "SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾ toਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ",
+ "StartupEmbyServerIsLoading": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ. ਕਿਰਪਾ ਕਰਕੇ ਜਲਦੀ ਹੀ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ.",
+ "Songs": "ਗਾਣੇ",
+ "Shows": "ਸ਼ੋਅਜ਼",
+ "ServerNameNeedsToBeRestarted": "{0} ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ",
+ "ScheduledTaskStartedWithName": "{0} ਸ਼ੁਰੂ ਹੋਇਆ",
+ "ScheduledTaskFailedWithName": "{0} ਅਸਫਲ",
+ "ProviderValue": "ਦੇਣ ਵਾਲੇ: {0}",
+ "PluginUpdatedWithName": "{0} ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਸੀ",
+ "PluginUninstalledWithName": "{0} ਅਣਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ ਸੀ",
+ "PluginInstalledWithName": "{0} ਲਗਾਇਆ ਗਿਆ ਸੀ",
+ "Plugin": "ਪਲੱਗਇਨ",
+ "Playlists": "ਪਲੇਲਿਸਟਸ",
+ "Photos": "ਫੋਟੋਆਂ",
+ "NotificationOptionVideoPlaybackStopped": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਰੋਕਿਆ ਗਿਆ",
+ "NotificationOptionVideoPlayback": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ",
+ "NotificationOptionUserLockedOut": "ਉਪਭੋਗਤਾ ਨੂੰ ਲਾਕ ਆਉਟ ਕੀਤਾ ਗਿਆ",
+ "NotificationOptionTaskFailed": "ਨਿਰਧਾਰਤ ਕਾਰਜ ਅਸਫਲਤਾ",
+ "NotificationOptionServerRestartRequired": "ਸਰਵਰ ਨੂੰ ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ",
+ "NotificationOptionPluginUpdateInstalled": "ਪਲੱਗਇਨ ਅਪਡੇਟ ਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ",
+ "NotificationOptionPluginUninstalled": "ਪਲੱਗਇਨ ਅਣਇੰਸਟੌਲ ਕੀਤਾ",
+ "NotificationOptionPluginInstalled": "ਪਲੱਗਇਨ ਸਥਾਪਿਤ ਕੀਤਾ",
+ "NotificationOptionPluginError": "ਪਲੱਗਇਨ ਅਸਫਲ",
+ "NotificationOptionNewLibraryContent": "ਨਵੀਂ ਸਮੱਗਰੀ ਸ਼ਾਮਲ ਕੀਤੀ ਗਈ",
+ "NotificationOptionInstallationFailed": "ਇੰਸਟਾਲੇਸ਼ਨ ਅਸਫਲ",
+ "NotificationOptionCameraImageUploaded": "ਕੈਮਰਾ ਤਸਵੀਰ ਅਪਲੋਡ ਕੀਤੀ ਗਈ",
+ "NotificationOptionAudioPlaybackStopped": "ਆਡੀਓ ਪਲੇਅਬੈਕ ਰੋਕਿਆ ਗਿਆ",
+ "NotificationOptionAudioPlayback": "ਆਡੀਓ ਪਲੇਅਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ",
+ "NotificationOptionApplicationUpdateInstalled": "ਐਪਲੀਕੇਸ਼ਨ ਅਪਡੇਟ ਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ",
+ "NotificationOptionApplicationUpdateAvailable": "ਐਪਲੀਕੇਸ਼ਨ ਅਪਡੇਟ ਉਪਲਬਧ ਹੈ",
+ "NewVersionIsAvailable": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਦਾ ਨਵਾਂ ਸੰਸਕਰਣ ਡਾਉਨਲੋਡ ਲਈ ਉਪਲਬਧ ਹੈ.",
+ "NameSeasonUnknown": "ਸੀਜ਼ਨ ਅਣਜਾਣ",
+ "NameSeasonNumber": "ਸੀਜ਼ਨ {0}",
+ "NameInstallFailed": "{0} ਇੰਸਟਾਲੇਸ਼ਨ ਫੇਲ੍ਹ ਹੋਈ",
+ "MusicVideos": "ਸੰਗੀਤ ਵੀਡੀਓ",
+ "Music": "ਸੰਗੀਤ",
+ "Movies": "ਫਿਲਮਾਂ",
+ "MixedContent": "ਮਿਸ਼ਰਤ ਸਮੱਗਰੀ",
+ "MessageServerConfigurationUpdated": "ਸਰਵਰ ਕੌਂਫਿਗਰੇਸ਼ਨ ਨੂੰ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
+ "MessageNamedServerConfigurationUpdatedWithValue": "ਸਰਵਰ ਕੌਂਫਿਗਰੇਸ਼ਨ ਸੈਕਸ਼ਨ {0} ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
+ "MessageApplicationUpdatedTo": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਨੂੰ ਅੱਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ {0}",
+ "MessageApplicationUpdated": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
+ "Latest": "ਤਾਜ਼ਾ",
+ "LabelRunningTimeValue": "ਚੱਲਦਾ ਸਮਾਂ: {0}",
+ "LabelIpAddressValue": "IP ਪਤਾ: {0}",
+ "ItemRemovedWithName": "{0} ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ ਹਟਾ ਦਿੱਤਾ ਗਿਆ ਸੀ",
+ "ItemAddedWithName": "{0} ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਸੀ",
+ "Inherit": "ਵਿਰਾਸਤ",
+ "HomeVideos": "ਘਰੇਲੂ ਵੀਡੀਓ",
+ "HeaderRecordingGroups": "ਰਿਕਾਰਡਿੰਗ ਸਮੂਹ",
+ "HeaderNextUp": "ਅੱਗੇ",
+ "HeaderLiveTV": "ਲਾਈਵ ਟੀ",
+ "HeaderFavoriteSongs": "ਮਨਪਸੰਦ ਗਾਣੇ",
+ "HeaderFavoriteShows": "ਮਨਪਸੰਦ ਸ਼ੋਅ",
+ "HeaderFavoriteEpisodes": "ਮਨਪਸੰਦ ਐਪੀਸੋਡ",
+ "HeaderFavoriteArtists": "ਮਨਪਸੰਦ ਕਲਾਕਾਰ",
+ "HeaderFavoriteAlbums": "ਮਨਪਸੰਦ ਐਲਬਮ",
+ "HeaderContinueWatching": "ਵੇਖਣਾ ਜਾਰੀ ਰੱਖੋ",
+ "HeaderAlbumArtists": "ਐਲਬਮ ਕਲਾਕਾਰ",
+ "Genres": "ਸ਼ੈਲੀਆਂ",
+ "Forced": "ਮਜਬੂਰ",
+ "Folders": "ਫੋਲਡਰ",
+ "Favorites": "ਮਨਪਸੰਦ",
+ "FailedLoginAttemptWithUserName": "ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ {0}",
+ "DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ",
+ "DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ",
+ "Default": "ਮੂਲ",
+ "Collections": "ਸੰਗ੍ਰਹਿ",
+ "ChapterNameValue": "ਅਧਿਆਇ {0}",
+ "Channels": "ਚੈਨਲ",
+ "CameraImageUploadedFrom": "ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ {0}",
+ "Books": "ਕਿਤਾਬਾਂ",
+ "AuthenticationSucceededWithUserName": "{0} ਸਫਲਤਾਪੂਰਕ ਪ੍ਰਮਾਣਿਤ",
+ "Artists": "ਕਲਾਕਾਰ",
+ "Application": "ਐਪਲੀਕੇਸ਼ਨ",
+ "AppDeviceValues": "ਐਪ: {0}, ਜੰਤਰ: {1}",
+ "Albums": "ਐਲਬਮਾਂ"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 8d25e27f6..5ec8f1e88 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -115,5 +115,8 @@
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskCleanActivityLogDescription": "Apaga o registro de atividades mais antigo que a idade configurada.",
- "TaskCleanActivityLog": "Limpar Registro de Atividades"
+ "TaskCleanActivityLog": "Limpar Registro de Atividades",
+ "Undefined": "Indefinido",
+ "Forced": "Forçado",
+ "Default": "Padrão"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index bea294ba2..552710d70 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -113,5 +113,9 @@
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Underhåll",
- "TaskRefreshPeople": "Uppdatera Personer"
+ "TaskRefreshPeople": "Uppdatera Personer",
+ "TaskCleanActivityLogDescription": "Radera aktivitets logg inlägg som är äldre än definerad ålder.",
+ "TaskCleanActivityLog": "Rensa Aktivitets Logg",
+ "Undefined": "odefinierad",
+ "Forced": "Tvinga"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index fa7b2d4d0..5d6d0775c 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -8,7 +8,7 @@
"Collections": "مجموعہ",
"Folders": "فولڈرز",
"HeaderLiveTV": "براہ راست ٹی وی",
- "Channels": "چینل",
+ "Channels": "چینلز",
"HeaderContinueWatching": "دیکھنا جاری رکھیں",
"Playlists": "پلے لسٹس",
"ValueSpecialEpisodeName": "خاص - {0}",
@@ -17,7 +17,7 @@
"Artists": "فنکار",
"Sync": "مطابقت",
"Photos": "تصوریں",
- "Albums": "البم",
+ "Albums": "البمز",
"Favorites": "پسندیدہ",
"Songs": "گانے",
"Books": "کتابیں",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 30aaf3a05..3f9e22106 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -5,7 +5,9 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Text.Json;
using System.Threading.Tasks;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -24,7 +26,6 @@ namespace Emby.Server.Implementations.Localization
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
private readonly IServerConfigurationManager _configurationManager;
- private readonly IJsonSerializer _jsonSerializer;
private readonly ILogger<LocalizationManager> _logger;
private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings =
@@ -35,19 +36,18 @@ namespace Emby.Server.Implementations.Localization
private List<CultureDto> _cultures;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+
/// <summary>
/// Initializes a new instance of the <see cref="LocalizationManager" /> class.
/// </summary>
/// <param name="configurationManager">The configuration manager.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
/// <param name="logger">The logger.</param>
public LocalizationManager(
IServerConfigurationManager configurationManager,
- IJsonSerializer jsonSerializer,
ILogger<LocalizationManager> logger)
{
_configurationManager = configurationManager;
- _jsonSerializer = jsonSerializer;
_logger = logger;
}
@@ -179,8 +179,11 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<CountryInfo> GetCountries()
- => _jsonSerializer.DeserializeFromStream<IEnumerable<CountryInfo>>(
- _assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
+ {
+ StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
+
+ return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions);
+ }
/// <inheritdoc />
public IEnumerable<ParentalRating> GetParentalRatings()
@@ -344,7 +347,7 @@ namespace Emby.Server.Implementations.Localization
// If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
if (stream != null)
{
- var dict = await _jsonSerializer.DeserializeFromStreamAsync<Dictionary<string, string>>(stream).ConfigureAwait(false);
+ var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
foreach (var key in dict.Keys)
{
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
new file mode 100644
index 000000000..1ab01252d
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -0,0 +1,688 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Common.Json.Converters;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Plugins;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.Plugins
+{
+ /// <summary>
+ /// Defines the <see cref="PluginManager" />.
+ /// </summary>
+ public class PluginManager : IPluginManager
+ {
+ private readonly string _pluginsPath;
+ private readonly Version _appVersion;
+ private readonly JsonSerializerOptions _jsonOptions;
+ private readonly ILogger<PluginManager> _logger;
+ private readonly IApplicationHost _appHost;
+ private readonly ServerConfiguration _config;
+ private readonly IList<LocalPlugin> _plugins;
+ private readonly Version _minimumVersion;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginManager"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger"/>.</param>
+ /// <param name="appHost">The <see cref="IApplicationHost"/>.</param>
+ /// <param name="config">The <see cref="ServerConfiguration"/>.</param>
+ /// <param name="pluginsPath">The plugin path.</param>
+ /// <param name="appVersion">The application version.</param>
+ public PluginManager(
+ ILogger<PluginManager> logger,
+ IApplicationHost appHost,
+ ServerConfiguration config,
+ string pluginsPath,
+ Version appVersion)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _pluginsPath = pluginsPath;
+ _appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions())
+ {
+ WriteIndented = true
+ };
+
+ // We need to use the default GUID converter, so we need to remove any custom ones.
+ for (int a = _jsonOptions.Converters.Count - 1; a >= 0; a--)
+ {
+ if (_jsonOptions.Converters[a] is JsonGuidConverter convertor)
+ {
+ _jsonOptions.Converters.Remove(convertor);
+ break;
+ }
+ }
+
+ _config = config;
+ _appHost = appHost;
+ _minimumVersion = new Version(0, 0, 0, 1);
+ _plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
+ }
+
+ /// <summary>
+ /// Gets the Plugins.
+ /// </summary>
+ public IList<LocalPlugin> Plugins => _plugins;
+
+ /// <summary>
+ /// Returns all the assemblies.
+ /// </summary>
+ /// <returns>An IEnumerable{Assembly}.</returns>
+ public IEnumerable<Assembly> LoadAssemblies()
+ {
+ // Attempt to remove any deleted plugins and change any successors to be active.
+ for (int i = _plugins.Count - 1; i >= 0; i--)
+ {
+ var plugin = _plugins[i];
+ if (plugin.Manifest.Status == PluginStatus.Deleted && DeletePlugin(plugin))
+ {
+ // See if there is another version, and if so make that active.
+ ProcessAlternative(plugin);
+ }
+ }
+
+ // Now load the assemblies..
+ foreach (var plugin in _plugins)
+ {
+ UpdatePluginSuperceedStatus(plugin);
+
+ if (plugin.IsEnabledAndSupported == false)
+ {
+ _logger.LogInformation("Skipping disabled plugin {Version} of {Name} ", plugin.Version, plugin.Name);
+ continue;
+ }
+
+ foreach (var file in plugin.DllFiles)
+ {
+ Assembly assembly;
+ try
+ {
+ assembly = Assembly.LoadFrom(file);
+
+ // This force loads all reference dll's that the plugin uses in the try..catch block.
+ // Removing this will cause JF to bomb out if referenced dll's cause issues.
+ assembly.GetExportedTypes();
+ }
+ catch (FileLoadException ex)
+ {
+ _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
+ ChangePluginState(plugin, PluginStatus.Malfunctioned);
+ continue;
+ }
+
+ _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
+ yield return assembly;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Creates all the plugin instances.
+ /// </summary>
+ public void CreatePlugins()
+ {
+ _ = _appHost.GetExports<IPlugin>(CreatePluginInstance)
+ .Where(i => i != null)
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Registers the plugin's services with the DI.
+ /// Note: DI is not yet instantiated yet.
+ /// </summary>
+ /// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param>
+ public void RegisterServices(IServiceCollection serviceCollection)
+ {
+ foreach (var pluginServiceRegistrator in _appHost.GetExportTypes<IPluginServiceRegistrator>())
+ {
+ var plugin = GetPluginByAssembly(pluginServiceRegistrator.Assembly);
+ if (plugin == null)
+ {
+ _logger.LogError("Unable to find plugin in assembly {Assembly}", pluginServiceRegistrator.Assembly.FullName);
+ continue;
+ }
+
+ UpdatePluginSuperceedStatus(plugin);
+ if (!plugin.IsEnabledAndSupported)
+ {
+ continue;
+ }
+
+ try
+ {
+ var instance = (IPluginServiceRegistrator?)Activator.CreateInstance(pluginServiceRegistrator);
+ instance?.RegisterServices(serviceCollection);
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch (Exception ex)
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ _logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly.FullName);
+ if (ChangePluginState(plugin, PluginStatus.Malfunctioned))
+ {
+ _logger.LogInformation("Disabling plugin {Path}", plugin.Path);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Imports a plugin manifest from <paramref name="folder"/>.
+ /// </summary>
+ /// <param name="folder">Folder of the plugin.</param>
+ public void ImportPluginFrom(string folder)
+ {
+ if (string.IsNullOrEmpty(folder))
+ {
+ throw new ArgumentNullException(nameof(folder));
+ }
+
+ // Load the plugin.
+ var plugin = LoadManifest(folder);
+ // Make sure we haven't already loaded this.
+ if (_plugins.Any(p => p.Manifest.Equals(plugin.Manifest)))
+ {
+ return;
+ }
+
+ _plugins.Add(plugin);
+ EnablePlugin(plugin);
+ }
+
+ /// <summary>
+ /// Removes the plugin reference '<paramref name="plugin"/>.
+ /// </summary>
+ /// <param name="plugin">The plugin.</param>
+ /// <returns>Outcome of the operation.</returns>
+ public bool RemovePlugin(LocalPlugin plugin)
+ {
+ if (plugin == null)
+ {
+ throw new ArgumentNullException(nameof(plugin));
+ }
+
+ if (DeletePlugin(plugin))
+ {
+ ProcessAlternative(plugin);
+ return true;
+ }
+
+ _logger.LogWarning("Unable to delete {Path}, so marking as deleteOnStartup.", plugin.Path);
+ // Unable to delete, so disable.
+ if (ChangePluginState(plugin, PluginStatus.Deleted))
+ {
+ ProcessAlternative(plugin);
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Attempts to find the plugin with and id of <paramref name="id"/>.
+ /// </summary>
+ /// <param name="id">The <see cref="Guid"/> of plugin.</param>
+ /// <param name="version">Optional <see cref="Version"/> of the plugin to locate.</param>
+ /// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns>
+ public LocalPlugin? GetPlugin(Guid id, Version? version = null)
+ {
+ LocalPlugin? plugin;
+
+ if (version == null)
+ {
+ // If no version is given, return the current instance.
+ var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList();
+
+ plugin = plugins.FirstOrDefault(p => p.Instance != null);
+ if (plugin == null)
+ {
+ plugin = plugins.OrderByDescending(p => p.Version).FirstOrDefault();
+ }
+ }
+ else
+ {
+ // Match id and version number.
+ plugin = _plugins.FirstOrDefault(p => p.Id.Equals(id) && p.Version.Equals(version));
+ }
+
+ return plugin;
+ }
+
+ /// <summary>
+ /// Enables the plugin, disabling all other versions.
+ /// </summary>
+ /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
+ public void EnablePlugin(LocalPlugin plugin)
+ {
+ if (plugin == null)
+ {
+ throw new ArgumentNullException(nameof(plugin));
+ }
+
+ if (ChangePluginState(plugin, PluginStatus.Active))
+ {
+ // See if there is another version, and if so, supercede it.
+ ProcessAlternative(plugin);
+ }
+ }
+
+ /// <summary>
+ /// Disable the plugin.
+ /// </summary>
+ /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
+ public void DisablePlugin(LocalPlugin plugin)
+ {
+ if (plugin == null)
+ {
+ throw new ArgumentNullException(nameof(plugin));
+ }
+
+ // Update the manifest on disk
+ if (ChangePluginState(plugin, PluginStatus.Disabled))
+ {
+ // If there is another version, activate it.
+ ProcessAlternative(plugin);
+ }
+ }
+
+ /// <summary>
+ /// Disable the plugin.
+ /// </summary>
+ /// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param>
+ public void FailPlugin(Assembly assembly)
+ {
+ // Only save if disabled.
+ if (assembly == null)
+ {
+ throw new ArgumentNullException(nameof(assembly));
+ }
+
+ var plugin = _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location));
+ if (plugin == null)
+ {
+ // A plugin's assembly didn't cause this issue, so ignore it.
+ return;
+ }
+
+ ChangePluginState(plugin, PluginStatus.Malfunctioned);
+ }
+
+ /// <summary>
+ /// Saves the manifest back to disk.
+ /// </summary>
+ /// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
+ /// <param name="path">The path where to save the manifest.</param>
+ /// <returns>True if successful.</returns>
+ public bool SaveManifest(PluginManifest manifest, string path)
+ {
+ if (manifest == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ var data = JsonSerializer.Serialize(manifest, _jsonOptions);
+ File.WriteAllText(Path.Combine(path, "meta.json"), data, Encoding.UTF8);
+ return true;
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch (Exception e)
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ _logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Changes a plugin's load status.
+ /// </summary>
+ /// <param name="plugin">The <see cref="LocalPlugin"/> instance.</param>
+ /// <param name="state">The <see cref="PluginStatus"/> of the plugin.</param>
+ /// <returns>Success of the task.</returns>
+ private bool ChangePluginState(LocalPlugin plugin, PluginStatus state)
+ {
+ if (plugin.Manifest.Status == state || string.IsNullOrEmpty(plugin.Path))
+ {
+ // No need to save as the state hasn't changed.
+ return true;
+ }
+
+ plugin.Manifest.Status = state;
+ return SaveManifest(plugin.Manifest, plugin.Path);
+ }
+
+ /// <summary>
+ /// Finds the plugin record using the assembly.
+ /// </summary>
+ /// <param name="assembly">The <see cref="Assembly"/> being sought.</param>
+ /// <returns>The matching record, or null if not found.</returns>
+ private LocalPlugin? GetPluginByAssembly(Assembly assembly)
+ {
+ // Find which plugin it is by the path.
+ return _plugins.FirstOrDefault(p => string.Equals(p.Path, Path.GetDirectoryName(assembly.Location), StringComparison.Ordinal));
+ }
+
+ /// <summary>
+ /// Creates the instance safe.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>System.Object.</returns>
+ private IPlugin? CreatePluginInstance(Type type)
+ {
+ // Find the record for this plugin.
+ var plugin = GetPluginByAssembly(type.Assembly);
+ if (plugin?.Manifest.Status < PluginStatus.Active)
+ {
+ return null;
+ }
+
+ try
+ {
+ _logger.LogDebug("Creating instance of {Type}", type);
+ var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
+ if (plugin == null)
+ {
+ // Create a dummy record for the providers.
+ // TODO: remove this code, if all provided have been released as separate plugins.
+ plugin = new LocalPlugin(
+ instance.AssemblyFilePath,
+ true,
+ new PluginManifest
+ {
+ Id = instance.Id,
+ Status = PluginStatus.Active,
+ Name = instance.Name,
+ Version = instance.Version.ToString()
+ })
+ {
+ Instance = instance
+ };
+
+ _plugins.Add(plugin);
+
+ plugin.Manifest.Status = PluginStatus.Active;
+ }
+ else
+ {
+ plugin.Instance = instance;
+ var manifest = plugin.Manifest;
+ var pluginStr = plugin.Instance.Version.ToString();
+ bool changed = false;
+ if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal))
+ {
+ // If a plugin without a manifest failed to load due to an external issue (eg config),
+ // this updates the manifest to the actual plugin values.
+ manifest.Version = pluginStr;
+ manifest.Name = plugin.Instance.Name;
+ manifest.Description = plugin.Instance.Description;
+ changed = true;
+ }
+
+ changed = changed || manifest.Status != PluginStatus.Active;
+ manifest.Status = PluginStatus.Active;
+
+ if (changed)
+ {
+ SaveManifest(manifest, plugin.Path);
+ }
+ }
+
+ _logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
+
+ return instance;
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch (Exception ex)
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ _logger.LogError(ex, "Error creating {Type}", type.FullName);
+ if (plugin != null)
+ {
+ if (ChangePluginState(plugin, PluginStatus.Malfunctioned))
+ {
+ _logger.LogInformation("Plugin {Path} has been disabled.", plugin.Path);
+ return null;
+ }
+ }
+
+ _logger.LogDebug("Unable to auto-disable.");
+ return null;
+ }
+ }
+
+ private void UpdatePluginSuperceedStatus(LocalPlugin plugin)
+ {
+ if (plugin.Manifest.Status != PluginStatus.Superceded)
+ {
+ return;
+ }
+
+ var predecessor = _plugins.OrderByDescending(p => p.Version)
+ .FirstOrDefault(p => p.Id.Equals(plugin.Id) && p.IsEnabledAndSupported && p.Version != plugin.Version);
+ if (predecessor != null)
+ {
+ return;
+ }
+
+ plugin.Manifest.Status = PluginStatus.Active;
+ }
+
+ /// <summary>
+ /// Attempts to delete a plugin.
+ /// </summary>
+ /// <param name="plugin">A <see cref="LocalPlugin"/> instance to delete.</param>
+ /// <returns>True if successful.</returns>
+ private bool DeletePlugin(LocalPlugin plugin)
+ {
+ // Attempt a cleanup of old folders.
+ try
+ {
+ Directory.Delete(plugin.Path, true);
+ _logger.LogDebug("Deleted {Path}", plugin.Path);
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ return false;
+ }
+
+ return _plugins.Remove(plugin);
+ }
+
+ private LocalPlugin LoadManifest(string dir)
+ {
+ Version? version;
+ PluginManifest? manifest = null;
+ var metafile = Path.Combine(dir, "meta.json");
+ if (File.Exists(metafile))
+ {
+ try
+ {
+ var data = File.ReadAllText(metafile, Encoding.UTF8);
+ manifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions);
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch (Exception ex)
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ _logger.LogError(ex, "Error deserializing {Path}.", dir);
+ }
+ }
+
+ if (manifest != null)
+ {
+ if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+ {
+ targetAbi = _minimumVersion;
+ }
+
+ if (!Version.TryParse(manifest.Version, out version))
+ {
+ manifest.Version = _minimumVersion.ToString();
+ }
+
+ return new LocalPlugin(dir, _appVersion >= targetAbi, manifest);
+ }
+
+ // No metafile, so lets see if the folder is versioned.
+ // TODO: Phase this support out in future versions.
+ metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
+ int versionIndex = dir.LastIndexOf('_');
+ if (versionIndex != -1)
+ {
+ // Get the version number from the filename if possible.
+ metafile = Path.GetFileName(dir[..versionIndex]) ?? dir[..versionIndex];
+ version = Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version? parsedVersion) ? parsedVersion : _appVersion;
+ }
+ else
+ {
+ // Un-versioned folder - Add it under the path name and version it suitable for this instance.
+ version = _appVersion;
+ }
+
+ // Auto-create a plugin manifest, so we can disable it, if it fails to load.
+ manifest = new PluginManifest
+ {
+ Status = PluginStatus.Restart,
+ Name = metafile,
+ AutoUpdate = false,
+ Id = metafile.GetMD5(),
+ TargetAbi = _appVersion.ToString(),
+ Version = version.ToString()
+ };
+
+ return new LocalPlugin(dir, true, manifest);
+ }
+
+ /// <summary>
+ /// Gets the list of local plugins.
+ /// </summary>
+ /// <returns>Enumerable of local plugins.</returns>
+ private IEnumerable<LocalPlugin> DiscoverPlugins()
+ {
+ var versions = new List<LocalPlugin>();
+
+ if (!Directory.Exists(_pluginsPath))
+ {
+ // Plugin path doesn't exist, don't try to enumerate sub-folders.
+ return Enumerable.Empty<LocalPlugin>();
+ }
+
+ var directories = Directory.EnumerateDirectories(_pluginsPath, "*.*", SearchOption.TopDirectoryOnly);
+ foreach (var dir in directories)
+ {
+ versions.Add(LoadManifest(dir));
+ }
+
+ string lastName = string.Empty;
+ versions.Sort(LocalPlugin.Compare);
+ // Traverse backwards through the list.
+ // The first item will be the latest version.
+ for (int x = versions.Count - 1; x >= 0; x--)
+ {
+ var entry = versions[x];
+ if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories));
+ if (entry.IsEnabledAndSupported)
+ {
+ lastName = entry.Name;
+ continue;
+ }
+ }
+
+ if (string.IsNullOrEmpty(lastName))
+ {
+ continue;
+ }
+
+ var manifest = entry.Manifest;
+ var cleaned = false;
+ var path = entry.Path;
+ if (_config.RemoveOldPlugins)
+ {
+ // Attempt a cleanup of old folders.
+ try
+ {
+ _logger.LogDebug("Deleting {Path}", path);
+ Directory.Delete(path, true);
+ cleaned = true;
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch (Exception e)
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ _logger.LogWarning(e, "Unable to delete {Path}", path);
+ }
+
+ if (cleaned)
+ {
+ versions.RemoveAt(x);
+ }
+ else
+ {
+ if (manifest == null)
+ {
+ _logger.LogWarning("Unable to disable plugin {Path}", entry.Path);
+ continue;
+ }
+
+ ChangePluginState(entry, PluginStatus.Deleted);
+ }
+ }
+ }
+
+ // Only want plugin folders which have files.
+ return versions.Where(p => p.DllFiles.Count != 0);
+ }
+
+ /// <summary>
+ /// Changes the status of the other versions of the plugin to "Superceded".
+ /// </summary>
+ /// <param name="plugin">The <see cref="LocalPlugin"/> that's master.</param>
+ private void ProcessAlternative(LocalPlugin plugin)
+ {
+ // Detect whether there is another version of this plugin that needs disabling.
+ var previousVersion = _plugins.OrderByDescending(p => p.Version)
+ .FirstOrDefault(
+ p => p.Id.Equals(plugin.Id)
+ && p.IsEnabledAndSupported
+ && p.Version != plugin.Version);
+
+ if (previousVersion == null)
+ {
+ // This value is memory only - so that the web will show restart required.
+ plugin.Manifest.Status = PluginStatus.Restart;
+ return;
+ }
+
+ if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Superceded))
+ {
+ _logger.LogError("Unable to enable version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
+ }
+ else if (plugin.Manifest.Status == PluginStatus.Superceded && !ChangePluginState(previousVersion, PluginStatus.Active))
+ {
+ _logger.LogError("Unable to supercede version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
+ }
+
+ // This value is memory only - so that the web will show restart required.
+ plugin.Manifest.Status = PluginStatus.Restart;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Plugins/PluginManifest.cs b/Emby.Server.Implementations/Plugins/PluginManifest.cs
deleted file mode 100644
index 33762791b..000000000
--- a/Emby.Server.Implementations/Plugins/PluginManifest.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System;
-
-namespace Emby.Server.Implementations.Plugins
-{
- /// <summary>
- /// Defines a Plugin manifest file.
- /// </summary>
- public class PluginManifest
- {
- /// <summary>
- /// Gets or sets the category of the plugin.
- /// </summary>
- public string Category { get; set; }
-
- /// <summary>
- /// Gets or sets the changelog information.
- /// </summary>
- public string Changelog { get; set; }
-
- /// <summary>
- /// Gets or sets the description of the plugin.
- /// </summary>
- public string Description { get; set; }
-
- /// <summary>
- /// Gets or sets the Global Unique Identifier for the plugin.
- /// </summary>
- public Guid Guid { get; set; }
-
- /// <summary>
- /// Gets or sets the Name of the plugin.
- /// </summary>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets an overview of the plugin.
- /// </summary>
- public string Overview { get; set; }
-
- /// <summary>
- /// Gets or sets the owner of the plugin.
- /// </summary>
- public string Owner { get; set; }
-
- /// <summary>
- /// Gets or sets the compatibility version for the plugin.
- /// </summary>
- public string TargetAbi { get; set; }
-
- /// <summary>
- /// Gets or sets the timestamp of the plugin.
- /// </summary>
- public DateTime Timestamp { get; set; }
-
- /// <summary>
- /// Gets or sets the Version number of the plugin.
- /// </summary>
- public string Version { get; set; }
- }
-}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 3a9e28458..29440b64a 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -4,13 +4,15 @@ using System;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Progress;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -21,11 +23,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
public class ScheduledTaskWorker : IScheduledTaskWorker
{
- /// <summary>
- /// Gets or sets the json serializer.
- /// </summary>
- /// <value>The json serializer.</value>
- private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Gets or sets the application paths.
@@ -70,12 +67,16 @@ namespace Emby.Server.Implementations.ScheduledTasks
private string _id;
/// <summary>
+ /// The options for the json Serializer.
+ /// </summary>
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+
+ /// <summary>
/// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
/// </summary>
/// <param name="scheduledTask">The scheduled task.</param>
/// <param name="applicationPaths">The application paths.</param>
/// <param name="taskManager">The task manager.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
/// <param name="logger">The logger.</param>
/// <exception cref="ArgumentNullException">
/// scheduledTask
@@ -88,7 +89,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// or
/// logger.
/// </exception>
- public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger)
+ public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger)
{
if (scheduledTask == null)
{
@@ -105,11 +106,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentNullException(nameof(taskManager));
}
- if (jsonSerializer == null)
- {
- throw new ArgumentNullException(nameof(jsonSerializer));
- }
-
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
@@ -118,7 +114,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
ScheduledTask = scheduledTask;
_applicationPaths = applicationPaths;
_taskManager = taskManager;
- _jsonSerializer = jsonSerializer;
_logger = logger;
InitTriggerEvents();
@@ -150,7 +145,15 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
- _lastExecutionResult = _jsonSerializer.DeserializeFromFile<TaskResult>(path);
+ var jsonString = File.ReadAllText(path, Encoding.UTF8);
+ if (!string.IsNullOrWhiteSpace(jsonString))
+ {
+ _lastExecutionResult = JsonSerializer.Deserialize<TaskResult>(jsonString, _jsonOptions);
+ }
+ else
+ {
+ _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path);
+ }
}
catch (Exception ex)
{
@@ -174,7 +177,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
lock (_lastExecutionResultSyncLock)
{
- _jsonSerializer.SerializeToFile(value, path);
+ using FileStream createStream = File.OpenWrite(path);
+ JsonSerializer.SerializeAsync(createStream, value, _jsonOptions);
}
}
}
@@ -537,7 +541,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
TaskTriggerInfo[] list = null;
if (File.Exists(path))
{
- list = _jsonSerializer.DeserializeFromFile<TaskTriggerInfo[]>(path);
+ var jsonString = File.ReadAllText(path, Encoding.UTF8);
+ list = JsonSerializer.Deserialize<TaskTriggerInfo[]>(jsonString, _jsonOptions);
}
// Return defaults if file doesn't exist.
@@ -573,7 +578,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(triggers, path);
+ var json = JsonSerializer.Serialize(triggers, _jsonOptions);
+ File.WriteAllText(path, json, Encoding.UTF8);
}
/// <summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index cfbf03ddc..af316e108 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -19,6 +18,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
public class TaskManager : ITaskManager
{
public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+
public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
/// <summary>
@@ -33,7 +33,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue =
new ConcurrentQueue<Tuple<Type, TaskOptions>>();
- private readonly IJsonSerializer _jsonSerializer;
private readonly IApplicationPaths _applicationPaths;
private readonly ILogger<TaskManager> _logger;
@@ -41,15 +40,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// Initializes a new instance of the <see cref="TaskManager" /> class.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
/// <param name="logger">The logger.</param>
public TaskManager(
IApplicationPaths applicationPaths,
- IJsonSerializer jsonSerializer,
ILogger<TaskManager> logger)
{
_applicationPaths = applicationPaths;
- _jsonSerializer = jsonSerializer;
_logger = logger;
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
@@ -196,7 +192,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <param name="tasks">The tasks.</param>
public void AddTasks(IEnumerable<IScheduledTask> tasks)
{
- var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _jsonSerializer, _logger));
+ var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _logger));
ScheduledTasks = ScheduledTasks.Concat(list).ToArray();
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index 161fa0580..a69380cbb 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -8,10 +8,10 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Updates;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks
{
diff --git a/Emby.Server.Implementations/Serialization/JsonSerializer.cs b/Emby.Server.Implementations/Serialization/JsonSerializer.cs
deleted file mode 100644
index 5ec3a735a..000000000
--- a/Emby.Server.Implementations/Serialization/JsonSerializer.cs
+++ /dev/null
@@ -1,281 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.IO;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Serialization;
-
-namespace Emby.Server.Implementations.Serialization
-{
- /// <summary>
- /// Provides a wrapper around third party json serialization.
- /// </summary>
- public class JsonSerializer : IJsonSerializer
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="JsonSerializer" /> class.
- /// </summary>
- public JsonSerializer()
- {
- ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;
- ServiceStack.Text.JsConfig.ExcludeTypeInfo = true;
- ServiceStack.Text.JsConfig.IncludeNullValues = false;
- ServiceStack.Text.JsConfig.AlwaysUseUtc = true;
- ServiceStack.Text.JsConfig.AssumeUtc = true;
-
- ServiceStack.Text.JsConfig<Guid>.SerializeFn = SerializeGuid;
- }
-
- /// <summary>
- /// Serializes to stream.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="stream">The stream.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- public void SerializeToStream(object obj, Stream stream)
- {
- if (obj == null)
- {
- throw new ArgumentNullException(nameof(obj));
- }
-
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- ServiceStack.Text.JsonSerializer.SerializeToStream(obj, obj.GetType(), stream);
- }
-
- /// <summary>
- /// Serializes to stream.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="stream">The stream.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- public void SerializeToStream<T>(T obj, Stream stream)
- {
- if (obj == null)
- {
- throw new ArgumentNullException(nameof(obj));
- }
-
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- ServiceStack.Text.JsonSerializer.SerializeToStream<T>(obj, stream);
- }
-
- /// <summary>
- /// Serializes to file.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="file">The file.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- public void SerializeToFile(object obj, string file)
- {
- if (obj == null)
- {
- throw new ArgumentNullException(nameof(obj));
- }
-
- if (string.IsNullOrEmpty(file))
- {
- throw new ArgumentNullException(nameof(file));
- }
-
- using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
- {
- SerializeToStream(obj, stream);
- }
- }
-
- private static Stream OpenFile(string path)
- {
- return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 131072);
- }
-
- /// <summary>
- /// Deserializes from file.
- /// </summary>
- /// <param name="type">The type.</param>
- /// <param name="file">The file.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">type</exception>
- public object DeserializeFromFile(Type type, string file)
- {
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- if (string.IsNullOrEmpty(file))
- {
- throw new ArgumentNullException(nameof(file));
- }
-
- using (var stream = OpenFile(file))
- {
- return DeserializeFromStream(stream, type);
- }
- }
-
- /// <summary>
- /// Deserializes from file.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="file">The file.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">file</exception>
- public T DeserializeFromFile<T>(string file)
- where T : class
- {
- if (string.IsNullOrEmpty(file))
- {
- throw new ArgumentNullException(nameof(file));
- }
-
- using (var stream = OpenFile(file))
- {
- return DeserializeFromStream<T>(stream);
- }
- }
-
- /// <summary>
- /// Deserializes from stream.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="stream">The stream.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">stream</exception>
- public T DeserializeFromStream<T>(Stream stream)
- {
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromStream<T>(stream);
- }
-
- public Task<T> DeserializeFromStreamAsync<T>(Stream stream)
- {
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromStreamAsync<T>(stream);
- }
-
- /// <summary>
- /// Deserializes from string.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="text">The text.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">text</exception>
- public T DeserializeFromString<T>(string text)
- {
- if (string.IsNullOrEmpty(text))
- {
- throw new ArgumentNullException(nameof(text));
- }
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromString<T>(text);
- }
-
- /// <summary>
- /// Deserializes from stream.
- /// </summary>
- /// <param name="stream">The stream.</param>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">stream</exception>
- public object DeserializeFromStream(Stream stream, Type type)
- {
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromStream(type, stream);
- }
-
- public async Task<object> DeserializeFromStreamAsync(Stream stream, Type type)
- {
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- using (var reader = new StreamReader(stream))
- {
- var json = await reader.ReadToEndAsync().ConfigureAwait(false);
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromString(json, type);
- }
- }
-
- private static string SerializeGuid(Guid guid)
- {
- if (guid.Equals(Guid.Empty))
- {
- return null;
- }
-
- return guid.ToString("N", CultureInfo.InvariantCulture);
- }
-
- /// <summary>
- /// Deserializes from string.
- /// </summary>
- /// <param name="json">The json.</param>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">json</exception>
- public object DeserializeFromString(string json, Type type)
- {
- if (string.IsNullOrEmpty(json))
- {
- throw new ArgumentNullException(nameof(json));
- }
-
- if (type == null)
- {
- throw new ArgumentNullException(nameof(type));
- }
-
- return ServiceStack.Text.JsonSerializer.DeserializeFromString(json, type);
- }
-
- /// <summary>
- /// Serializes to string.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">obj</exception>
- public string SerializeToString(object obj)
- {
- if (obj == null)
- {
- throw new ArgumentNullException(nameof(obj));
- }
-
- return ServiceStack.Text.JsonSerializer.SerializeToString(obj, obj.GetType());
- }
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 885f65c64..4e026a0e6 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1310,7 +1310,7 @@ namespace Emby.Server.Implementations.Session
}
}
- return SendMessageToSession(session, SessionMessageType.PlayState, command, cancellationToken);
+ return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
}
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index ae2fa3ce1..abcb4313f 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#nullable enable
using System;
using System.Collections.Concurrent;
@@ -40,17 +40,15 @@ namespace Emby.Server.Implementations.Updates
private readonly IEventManager _eventManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
- private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonSerializerOptions;
+ private readonly IPluginManager _pluginManager;
/// <summary>
/// Gets the application host.
/// </summary>
/// <value>The application host.</value>
private readonly IServerApplicationHost _applicationHost;
-
private readonly IZipClient _zipClient;
-
private readonly object _currentInstallationsLock = new object();
/// <summary>
@@ -63,6 +61,17 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InstallationManager"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger{InstallationManager}"/>.</param>
+ /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
+ /// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
+ /// <param name="eventManager">The <see cref="IEventManager"/>.</param>
+ /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
+ /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="zipClient">The <see cref="IZipClient"/>.</param>
+ /// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
public InstallationManager(
ILogger<InstallationManager> logger,
IServerApplicationHost appHost,
@@ -70,8 +79,8 @@ namespace Emby.Server.Implementations.Updates
IEventManager eventManager,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
- IFileSystem fileSystem,
- IZipClient zipClient)
+ IZipClient zipClient,
+ IPluginManager pluginManager)
{
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
@@ -82,38 +91,65 @@ namespace Emby.Server.Implementations.Updates
_eventManager = eventManager;
_httpClientFactory = httpClientFactory;
_config = config;
- _fileSystem = fileSystem;
_zipClient = zipClient;
_jsonSerializerOptions = JsonDefaults.GetOptions();
+ _pluginManager = pluginManager;
}
/// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
- public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default)
+ public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
{
try
{
- var packages = await _httpClientFactory.CreateClient(NamedClient.Default)
- .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
+ List<PackageInfo>? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
+
if (packages == null)
{
return Array.Empty<PackageInfo>();
}
+ var minimumVersion = new Version(0, 0, 0, 1);
// Store the repository and repository url with each version, as they may be spread apart.
foreach (var entry in packages)
{
- foreach (var ver in entry.versions)
+ for (int a = entry.Versions.Count - 1; a >= 0; a--)
{
- ver.repositoryName = manifestName;
- ver.repositoryUrl = manifest;
+ var ver = entry.Versions[a];
+ ver.RepositoryName = manifestName;
+ ver.RepositoryUrl = manifest;
+
+ if (!filterIncompatible)
+ {
+ continue;
+ }
+
+ if (!Version.TryParse(ver.TargetAbi, out var targetAbi))
+ {
+ targetAbi = minimumVersion;
+ }
+
+ // Only show plugins that are greater than or equal to targetAbi.
+ if (_applicationHost.ApplicationVersion >= targetAbi)
+ {
+ continue;
+ }
+
+ // Not compatible with this version so remove it.
+ entry.Versions.Remove(ver);
}
}
return packages;
}
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Cannot locate the plugin manifest {Manifest}", manifest);
+ return Array.Empty<PackageInfo>();
+ }
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
@@ -131,85 +167,58 @@ namespace Emby.Server.Implementations.Updates
}
}
- private static void MergeSort(IList<VersionInfo> source, IList<VersionInfo> dest)
- {
- int sLength = source.Count - 1;
- int dLength = dest.Count;
- int s = 0, d = 0;
- var sourceVersion = source[0].VersionNumber;
- var destVersion = dest[0].VersionNumber;
-
- while (d < dLength)
- {
- if (sourceVersion.CompareTo(destVersion) >= 0)
- {
- if (s < sLength)
- {
- sourceVersion = source[++s].VersionNumber;
- }
- else
- {
- // Append all of destination to the end of source.
- while (d < dLength)
- {
- source.Add(dest[d++]);
- }
-
- break;
- }
- }
- else
- {
- source.Insert(s++, dest[d++]);
- if (d >= dLength)
- {
- break;
- }
-
- sLength++;
- destVersion = dest[d].VersionNumber;
- }
- }
- }
-
/// <inheritdoc />
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
{
var result = new List<PackageInfo>();
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
{
- if (repository.Enabled)
+ if (repository.Enabled && repository.Url != null)
{
- // Where repositories have the same content, the details of the first is taken.
- foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true))
+ // Where repositories have the same content, the details from the first is taken.
+ foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
{
- if (!Guid.TryParse(package.guid, out var packageGuid))
+ if (!Guid.TryParse(package.Id, out var packageGuid))
{
// Package doesn't have a valid GUID, skip.
continue;
}
- for (var i = package.versions.Count - 1; i >= 0; i--)
+ var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault();
+
+ // Remove invalid versions from the valid package.
+ for (var i = package.Versions.Count - 1; i >= 0; i--)
{
+ var version = package.Versions[i];
+
+ var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
+ // Update the manifests, if anything changes.
+ if (plugin != null)
+ {
+ if (!string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal))
+ {
+ plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
+ _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
+ }
+ }
+
// Remove versions with a target abi that is greater then the current application version.
- if (Version.TryParse(package.versions[i].targetAbi, out var targetAbi)
- && _applicationHost.ApplicationVersion < targetAbi)
+ if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
{
- package.versions.RemoveAt(i);
+ package.Versions.RemoveAt(i);
}
}
// Don't add a package that doesn't have any compatible versions.
- if (package.versions.Count == 0)
+ if (package.Versions.Count == 0)
{
continue;
}
- var existing = FilterPackages(result, package.name, packageGuid).FirstOrDefault();
if (existing != null)
{
// Assumption is both lists are ordered, so slot these into the correct place.
- MergeSort(existing.versions, package.versions);
+ MergeSortedList(existing.Versions, package.Versions);
}
else
{
@@ -225,23 +234,23 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc />
public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
- string name = null,
- Guid guid = default,
- Version specificVersion = null)
+ string? name = null,
+ Guid? id = default,
+ Version? specificVersion = null)
{
if (name != null)
{
- availablePackages = availablePackages.Where(x => x.name.Equals(name, StringComparison.OrdinalIgnoreCase));
+ availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
- if (guid != Guid.Empty)
+ if (id != Guid.Empty)
{
- availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid);
+ availablePackages = availablePackages.Where(x => Guid.Parse(x.Id) == id);
}
if (specificVersion != null)
{
- availablePackages = availablePackages.Where(x => x.versions.Where(y => y.VersionNumber.Equals(specificVersion)).Any());
+ availablePackages = availablePackages.Where(x => x.Versions.Any(y => y.VersionNumber.Equals(specificVersion)));
}
return availablePackages;
@@ -250,12 +259,12 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc />
public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
- string name = null,
- Guid guid = default,
- Version minVersion = null,
- Version specificVersion = null)
+ string? name = null,
+ Guid? id = default,
+ Version? minVersion = null,
+ Version? specificVersion = null)
{
- var package = FilterPackages(availablePackages, name, guid, specificVersion).FirstOrDefault();
+ var package = FilterPackages(availablePackages, name, id, specificVersion).FirstOrDefault();
// Package not found in repository
if (package == null)
@@ -264,8 +273,8 @@ namespace Emby.Server.Implementations.Updates
}
var appVer = _applicationHost.ApplicationVersion;
- var availableVersions = package.versions
- .Where(x => Version.Parse(x.targetAbi) <= appVer);
+ var availableVersions = package.Versions
+ .Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer);
if (specificVersion != null)
{
@@ -280,12 +289,12 @@ namespace Emby.Server.Implementations.Updates
{
yield return new InstallationInfo
{
- Changelog = v.changelog,
- Guid = new Guid(package.guid),
- Name = package.name,
+ Changelog = v.Changelog,
+ Id = new Guid(package.Id),
+ Name = package.Name,
Version = v.VersionNumber,
- SourceUrl = v.sourceUrl,
- Checksum = v.checksum
+ SourceUrl = v.SourceUrl,
+ Checksum = v.Checksum
};
}
}
@@ -297,20 +306,6 @@ namespace Emby.Server.Implementations.Updates
return GetAvailablePluginUpdates(catalog);
}
- private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
- {
- var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
- foreach (var plugin in plugins)
- {
- var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
- var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
- if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
- {
- yield return version;
- }
- }
- }
-
/// <inheritdoc />
public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
{
@@ -388,24 +383,140 @@ namespace Emby.Server.Implementations.Updates
}
/// <summary>
- /// Installs the package internal.
+ /// Uninstalls a plugin.
/// </summary>
- /// <param name="package">The package.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns><see cref="Task" />.</returns>
- private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
+ /// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
+ public void UninstallPlugin(LocalPlugin plugin)
{
- // Set last update time if we were installed before
- IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
- ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase));
+ if (plugin == null)
+ {
+ return;
+ }
- // Do the install
- await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
+ if (plugin.Instance?.CanUninstall == false)
+ {
+ _logger.LogWarning("Attempt to delete non removable plugin {PluginName}, ignoring request", plugin.Name);
+ return;
+ }
- // Do plugin-specific processing
- _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
+ plugin.Instance?.OnUninstalling();
- return plugin != null;
+ // Remove it the quick way for now
+ _pluginManager.RemovePlugin(plugin);
+
+ _eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo()));
+
+ _applicationHost.NotifyPendingRestart();
+ }
+
+ /// <inheritdoc/>
+ public bool CancelInstallation(Guid id)
+ {
+ lock (_currentInstallationsLock)
+ {
+ var install = _currentInstallations.Find(x => x.info.Id == id);
+ if (install == default((InstallationInfo, CancellationTokenSource)))
+ {
+ return false;
+ }
+
+ install.token.Cancel();
+ _currentInstallations.Remove(install);
+ return true;
+ }
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and optionally managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources or <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ lock (_currentInstallationsLock)
+ {
+ foreach (var (info, token) in _currentInstallations)
+ {
+ token.Dispose();
+ }
+
+ _currentInstallations.Clear();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Merges two sorted lists.
+ /// </summary>
+ /// <param name="source">The source <see cref="IList{VersionInfo}"/> instance to merge.</param>
+ /// <param name="dest">The destination <see cref="IList{VersionInfo}"/> instance to merge with.</param>
+ private static void MergeSortedList(IList<VersionInfo> source, IList<VersionInfo> dest)
+ {
+ int sLength = source.Count - 1;
+ int dLength = dest.Count;
+ int s = 0, d = 0;
+ var sourceVersion = source[0].VersionNumber;
+ var destVersion = dest[0].VersionNumber;
+
+ while (d < dLength)
+ {
+ if (sourceVersion.CompareTo(destVersion) >= 0)
+ {
+ if (s < sLength)
+ {
+ sourceVersion = source[++s].VersionNumber;
+ }
+ else
+ {
+ // Append all of destination to the end of source.
+ while (d < dLength)
+ {
+ source.Add(dest[d++]);
+ }
+
+ break;
+ }
+ }
+ else
+ {
+ source.Insert(s++, dest[d++]);
+ if (d >= dLength)
+ {
+ break;
+ }
+
+ sLength++;
+ destVersion = dest[d].VersionNumber;
+ }
+ }
+ }
+
+ private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
+ {
+ var plugins = _pluginManager.Plugins;
+ foreach (var plugin in plugins)
+ {
+ if (plugin.Manifest?.AutoUpdate == false)
+ {
+ continue;
+ }
+
+ var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
+ var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
+
+ if (version != null && CompletedInstallations.All(x => x.Id != version.Id))
+ {
+ yield return version;
+ }
+ }
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
@@ -450,7 +561,9 @@ namespace Emby.Server.Implementations.Updates
{
Directory.Delete(targetDir, true);
}
+#pragma warning disable CA1031 // Do not catch general exception types
catch
+#pragma warning restore CA1031 // Do not catch general exception types
{
// Ignore any exceptions.
}
@@ -458,119 +571,27 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
-
-#pragma warning restore CA5351
- }
-
- /// <summary>
- /// Uninstalls a plugin.
- /// </summary>
- /// <param name="plugin">The plugin.</param>
- public void UninstallPlugin(IPlugin plugin)
- {
- if (!plugin.CanUninstall)
- {
- _logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
- return;
- }
-
- plugin.OnUninstalling();
-
- // Remove it the quick way for now
- _applicationHost.RemovePlugin(plugin);
-
- var path = plugin.AssemblyFilePath;
- bool isDirectory = false;
- // Check if we have a plugin directory we should remove too
- if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath)
- {
- path = Path.GetDirectoryName(plugin.AssemblyFilePath);
- isDirectory = true;
- }
-
- // Make this case-insensitive to account for possible incorrect assembly naming
- var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
- .FirstOrDefault(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase));
-
- if (!string.IsNullOrWhiteSpace(file))
- {
- path = file;
- }
-
- try
- {
- if (isDirectory)
- {
- _logger.LogInformation("Deleting plugin directory {0}", path);
- Directory.Delete(path, true);
- }
- else
- {
- _logger.LogInformation("Deleting plugin file {0}", path);
- _fileSystem.DeleteFile(path);
- }
- }
- catch
- {
- // Ignore file errors.
- }
-
- var list = _config.Configuration.UninstalledPlugins.ToList();
- var filename = Path.GetFileName(path);
- if (!list.Contains(filename, StringComparer.OrdinalIgnoreCase))
- {
- list.Add(filename);
- _config.Configuration.UninstalledPlugins = list.ToArray();
- _config.SaveConfiguration();
- }
-
- _eventManager.Publish(new PluginUninstalledEventArgs(plugin));
-
- _applicationHost.NotifyPendingRestart();
+ _pluginManager.ImportPluginFrom(targetDir);
}
- /// <inheritdoc/>
- public bool CancelInstallation(Guid id)
+ private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
- lock (_currentInstallationsLock)
+ // Set last update time if we were installed before
+ LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
+ ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
+ if (plugin != null)
{
- var install = _currentInstallations.Find(x => x.info.Guid == id);
- if (install == default((InstallationInfo, CancellationTokenSource)))
- {
- return false;
- }
-
- install.token.Cancel();
- _currentInstallations.Remove(install);
- return true;
+ plugin.Manifest.Timestamp = DateTime.UtcNow;
+ _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
}
- }
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
+ // Do the install
+ await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
- /// <summary>
- /// Releases unmanaged and optionally managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources or <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (dispose)
- {
- lock (_currentInstallationsLock)
- {
- foreach (var tuple in _currentInstallations)
- {
- tuple.token.Dispose();
- }
+ // Do plugin-specific processing
+ _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
- _currentInstallations.Clear();
- }
- }
+ return plugin != null;
}
}
}
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index ccc81dfc5..b77d79209 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -29,18 +29,22 @@ namespace Jellyfin.Api.Controllers
{
private readonly ILogger<DashboardController> _logger;
private readonly IServerApplicationHost _appHost;
+ private readonly IPluginManager _pluginManager;
/// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> class.
/// </summary>
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
public DashboardController(
ILogger<DashboardController> logger,
- IServerApplicationHost appHost)
+ IServerApplicationHost appHost,
+ IPluginManager pluginManager)
{
_logger = logger;
_appHost = appHost;
+ _pluginManager = pluginManager;
}
/// <summary>
@@ -83,7 +87,7 @@ namespace Jellyfin.Api.Controllers
.Where(i => i != null)
.ToList();
- configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
+ configPages.AddRange(_pluginManager.Plugins.SelectMany(GetConfigPages));
if (pageType.HasValue)
{
@@ -155,24 +159,24 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
+ private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
{
- return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
+ return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
}
- private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
+ private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin? plugin)
{
- if (!(plugin is IHasWebPages hasWebPages))
+ if (plugin?.Instance is not IHasWebPages hasWebPages)
{
return new List<Tuple<PluginPageInfo, IPlugin>>();
}
- return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
+ return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
}
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
{
- return _appHost.Plugins.SelectMany(GetPluginPages);
+ return _pluginManager.Plugins.SelectMany(GetPluginPages);
}
}
}
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 6295dfc05..9ab8e0bdc 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Updates;
@@ -99,7 +100,7 @@ namespace Jellyfin.Api.Controllers
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
if (!string.IsNullOrEmpty(repositoryUrl))
{
- packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any())
+ packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)))
.ToList();
}
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 98f1bc2d2..b73611c97 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,15 +1,21 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.IO;
using System.Linq;
+using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.PluginDtos;
-using MediaBrowser.Common;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -23,22 +29,81 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
public class PluginsController : BaseJellyfinApiController
{
- private readonly IApplicationHost _appHost;
private readonly IInstallationManager _installationManager;
-
- private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
+ private readonly IPluginManager _pluginManager;
+ private readonly IConfigurationManager _config;
+ private readonly JsonSerializerOptions _serializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="PluginsController"/> class.
/// </summary>
- /// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param>
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
+ /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
public PluginsController(
- IApplicationHost appHost,
- IInstallationManager installationManager)
+ IInstallationManager installationManager,
+ IPluginManager pluginManager,
+ IConfigurationManager config)
{
- _appHost = appHost;
_installationManager = installationManager;
+ _pluginManager = pluginManager;
+ _serializerOptions = JsonDefaults.GetOptions();
+ _config = config;
+ }
+
+ /// <summary>
+ /// Get plugin security info.
+ /// </summary>
+ /// <response code="200">Plugin security info returned.</response>
+ /// <returns>Plugin security info.</returns>
+ [Obsolete("This endpoint should not be used.")]
+ [HttpGet("SecurityInfo")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
+ {
+ return new PluginSecurityInfo
+ {
+ IsMbSupporter = true,
+ SupporterKey = "IAmTotallyLegit"
+ };
+ }
+
+ /// <summary>
+ /// Gets registration status for a feature.
+ /// </summary>
+ /// <param name="name">Feature name.</param>
+ /// <response code="200">Registration status returned.</response>
+ /// <returns>Mb registration record.</returns>
+ [Obsolete("This endpoint should not be used.")]
+ [HttpPost("RegistrationRecords/{name}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
+ {
+ return new MBRegistrationRecord
+ {
+ IsRegistered = true,
+ RegChecked = true,
+ TrialVersion = false,
+ IsValid = true,
+ RegError = false
+ };
+ }
+
+ /// <summary>
+ /// Gets registration status for a feature.
+ /// </summary>
+ /// <param name="name">Feature name.</param>
+ /// <response code="501">Not implemented.</response>
+ /// <returns>Not Implemented.</returns>
+ /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception>
+ [Obsolete("Paid plugins are not supported")]
+ [HttpGet("Registrations/{name}")]
+ [ProducesResponseType(StatusCodes.Status501NotImplemented)]
+ public static ActionResult GetRegistration([FromRoute, Required] string name)
+ {
+ // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
+ // delete all these registration endpoints. They are only kept for compatibility.
+ throw new NotImplementedException();
}
/// <summary>
@@ -50,23 +115,74 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
{
- return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
+ return Ok(_pluginManager.Plugins
+ .OrderBy(p => p.Name)
+ .Select(p => p.GetPluginInfo()));
}
/// <summary>
- /// Uninstalls a plugin.
+ /// Enables a disabled plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
+ /// <param name="version">Plugin version.</param>
+ /// <response code="204">Plugin enabled.</response>
+ /// <response code="404">Plugin not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
+ [HttpPost("{pluginId}/{version}/Enable")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId, version);
+ if (plugin == null)
+ {
+ return NotFound();
+ }
+
+ _pluginManager.EnablePlugin(plugin);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Disable a plugin.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <param name="version">Plugin version.</param>
+ /// <response code="204">Plugin disabled.</response>
+ /// <response code="404">Plugin not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
+ [HttpPost("{pluginId}/{version}/Disable")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId, version);
+ if (plugin == null)
+ {
+ return NotFound();
+ }
+
+ _pluginManager.DisablePlugin(plugin);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Uninstalls a plugin by version.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <param name="version">Plugin version.</param>
/// <response code="204">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</response>
- /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
- [HttpDelete("{pluginId}")]
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
+ [HttpDelete("{pluginId}/{version}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
+ public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{
- var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
+ var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null)
{
return NotFound();
@@ -77,6 +193,40 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
+ /// Uninstalls a plugin.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <response code="204">Plugin uninstalled.</response>
+ /// <response code="404">Plugin not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
+ [HttpDelete("{pluginId}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("Please use the UninstallPluginByVersion API.")]
+ public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
+ {
+ // If no version is given, return the current instance.
+ var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId));
+
+ // Select the un-instanced one first.
+ var plugin = plugins.FirstOrDefault(p => p.Instance == null);
+ if (plugin == null)
+ {
+ // Then by the status.
+ plugin = plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
+ }
+
+ if (plugin != null)
+ {
+ _installationManager.UninstallPlugin(plugin);
+ return NoContent();
+ }
+
+ return NotFound();
+ }
+
+ /// <summary>
/// Gets plugin configuration.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
@@ -88,12 +238,13 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
{
- if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
+ var plugin = _pluginManager.GetPlugin(pluginId);
+ if (plugin?.Instance is IHasPluginConfiguration configPlugin)
{
- return NotFound();
+ return configPlugin.Configuration;
}
- return plugin.Configuration;
+ return NotFound();
}
/// <summary>
@@ -105,47 +256,81 @@ namespace Jellyfin.Api.Controllers
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin configuration updated.</response>
/// <response code="404">Plugin not found or plugin does not have configuration.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration.
- /// The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/>
- /// when plugin not found or plugin doesn't have configuration.
- /// </returns>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
{
- if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
+ var plugin = _pluginManager.GetPlugin(pluginId);
+ if (plugin?.Instance is not IHasPluginConfiguration configPlugin)
{
return NotFound();
}
- var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
+ var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
.ConfigureAwait(false);
if (configuration != null)
{
- plugin.UpdateConfiguration(configuration);
+ configPlugin.UpdateConfiguration(configuration);
}
return NoContent();
}
/// <summary>
- /// Get plugin security info.
+ /// Gets a plugin's image.
/// </summary>
- /// <response code="200">Plugin security info returned.</response>
- /// <returns>Plugin security info.</returns>
- [Obsolete("This endpoint should not be used.")]
- [HttpGet("SecurityInfo")]
+ /// <param name="pluginId">Plugin id.</param>
+ /// <param name="version">Plugin version.</param>
+ /// <response code="200">Plugin image returned.</response>
+ /// <returns>Plugin's image.</returns>
+ [HttpGet("{pluginId}/{version}/Image")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ [AllowAnonymous]
+ public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{
- return new PluginSecurityInfo
+ var plugin = _pluginManager.GetPlugin(pluginId, version);
+ if (plugin == null)
{
- IsMbSupporter = true,
- SupporterKey = "IAmTotallyLegit"
- };
+ return NotFound();
+ }
+
+ var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
+ if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages
+ || plugin.Manifest.ImagePath == null
+ || !System.IO.File.Exists(imagePath))
+ {
+ return NotFound();
+ }
+
+ imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
+ return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
+ }
+
+ /// <summary>
+ /// Gets a plugin's manifest.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <response code="204">Plugin manifest returned.</response>
+ /// <response code="404">Plugin not found.</response>
+ /// <returns>A <see cref="PluginManifest"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
+ [HttpPost("{pluginId}/Manifest")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<PluginManifest> GetPluginManifest([FromRoute, Required] Guid pluginId)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId);
+
+ if (plugin != null)
+ {
+ return plugin.Manifest;
+ }
+
+ return NotFound();
}
/// <summary>
@@ -162,43 +347,5 @@ namespace Jellyfin.Api.Controllers
{
return NoContent();
}
-
- /// <summary>
- /// Gets registration status for a feature.
- /// </summary>
- /// <param name="name">Feature name.</param>
- /// <response code="200">Registration status returned.</response>
- /// <returns>Mb registration record.</returns>
- [Obsolete("This endpoint should not be used.")]
- [HttpPost("RegistrationRecords/{name}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
- {
- return new MBRegistrationRecord
- {
- IsRegistered = true,
- RegChecked = true,
- TrialVersion = false,
- IsValid = true,
- RegError = false
- };
- }
-
- /// <summary>
- /// Gets registration status for a feature.
- /// </summary>
- /// <param name="name">Feature name.</param>
- /// <response code="501">Not implemented.</response>
- /// <returns>Not Implemented.</returns>
- /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception>
- [Obsolete("Paid plugins are not supported")]
- [HttpGet("Registrations/{name}")]
- [ProducesResponseType(StatusCodes.Status501NotImplemented)]
- public ActionResult GetRegistration([FromRoute, Required] string name)
- {
- // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
- // delete all these registration endpoints. They are only kept for compatibility.
- throw new NotImplementedException();
- }
}
}
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index d79bea985..e67a27ae3 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -203,7 +203,7 @@ namespace Jellyfin.Api.Controllers
// For older files, assume fully static
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare);
- return File(stream, "text/plain");
+ return File(stream, "text/plain; charset=utf-8");
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 03fd1846d..ca18901e5 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -267,7 +267,7 @@ namespace Jellyfin.Api.Controllers
if (startItemId.HasValue)
{
episodes = episodes
- .SkipWhile(i => startItemId.Value.Equals(i.Id))
+ .SkipWhile(i => !startItemId.Value.Equals(i.Id))
.ToList();
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index c6d844c4f..4957ee8b8 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -210,6 +210,7 @@ namespace Jellyfin.Api.Helpers
&& !state.VideoRequest.MaxHeight.HasValue;
if (isVideoResolutionNotRequested
+ && state.VideoStream != null
&& state.VideoRequest.VideoBitRate.HasValue
&& state.VideoStream.BitRate.HasValue
&& state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
index e90f48d2f..c04f3c721 100644
--- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.ModelBinders
}
catch (FormatException e)
{
- _logger.LogWarning(e, "Error converting value.");
+ _logger.LogDebug(e, "Error converting value.");
}
}
diff --git a/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs b/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
index 5d296227e..be2045fba 100644
--- a/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
@@ -37,7 +37,7 @@ namespace Jellyfin.Api.ModelBinders
}
catch (FormatException e)
{
- _logger.LogWarning(e, "Error converting value.");
+ _logger.LogDebug(e, "Error converting value.");
}
}
diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
index a42e0e4da..639ab0793 100644
--- a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.ModelBinders
}
catch (FormatException e)
{
- _logger.LogWarning(e, "Error converting value.");
+ _logger.LogDebug(e, "Error converting value.");
}
}
diff --git a/Jellyfin.Api/Models/ConfigurationPageInfo.cs b/Jellyfin.Api/Models/ConfigurationPageInfo.cs
index 2aa6373aa..f56ef5976 100644
--- a/Jellyfin.Api/Models/ConfigurationPageInfo.cs
+++ b/Jellyfin.Api/Models/ConfigurationPageInfo.cs
@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Plugins;
+using System;
+using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins;
@@ -22,8 +23,7 @@ namespace Jellyfin.Api.Models
if (page.Plugin != null)
{
DisplayName = page.Plugin.Name;
- // Don't use "N" because it needs to match Plugin.Id
- PluginId = page.Plugin.Id.ToString();
+ PluginId = page.Plugin.Id;
}
}
@@ -32,16 +32,14 @@ namespace Jellyfin.Api.Models
/// </summary>
/// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param>
/// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param>
- public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page)
+ public ConfigurationPageInfo(IPlugin? plugin, PluginPageInfo page)
{
Name = page.Name;
EnableInMainMenu = page.EnableInMainMenu;
MenuSection = page.MenuSection;
MenuIcon = page.MenuIcon;
- DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin.Name : page.DisplayName;
-
- // Don't use "N" because it needs to match Plugin.Id
- PluginId = plugin.Id.ToString();
+ DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin?.Name : page.DisplayName;
+ PluginId = plugin?.Id;
}
/// <summary>
@@ -80,6 +78,6 @@ namespace Jellyfin.Api.Models
/// Gets or sets the plugin id.
/// </summary>
/// <value>The plugin id.</value>
- public string? PluginId { get; set; }
+ public Guid? PluginId { get; set; }
}
}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 572038d00..4fb5594d4 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index 43f2f7add..60b899519 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -784,7 +784,7 @@ namespace Jellyfin.Networking.Manager
}
else
{
- _logger.LogDebug("Invalid or unknown network {Token}.", token);
+ _logger.LogDebug("Invalid or unknown object {Token}.", token);
}
}
@@ -913,15 +913,6 @@ namespace Jellyfin.Networking.Manager
{
string[] lanAddresses = config.LocalNetworkAddresses;
- // TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334
-
- if (lanAddresses.Length == 1 && lanAddresses[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1)
- {
- lanAddresses = lanAddresses[0].Split(',');
- }
-
- // TODO: end fix: https://github.com/jellyfin/jellyfin-web/issues/1334
-
// Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
if (config.IgnoreVirtualInterfaces)
{
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index cd594b5c5..bbfc4fbd4 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -227,6 +227,7 @@ namespace Jellyfin.Server.Extensions
options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
+ options.JsonSerializerOptions.PropertyNameCaseInsensitive = jsonOptions.PropertyNameCaseInsensitive;
options.JsonSerializerOptions.Converters.Clear();
foreach (var converter in jsonOptions.Converters)
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 97fb56ba1..5940cf938 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -42,8 +42,8 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.1" />
- <PackageReference Include="prometheus-net" Version="4.0.0" />
- <PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
+ <PackageReference Include="prometheus-net" Version="4.1.1" />
+ <PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
@@ -52,7 +52,6 @@
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.2" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.4" />
- <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index a1a7a3053..f05cdfe9b 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -598,7 +598,8 @@ namespace Jellyfin.Server
.WriteTo.Async(x => x.File(
Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
rollingInterval: RollingInterval.Day,
- outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}"))
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}",
+ encoding: Encoding.UTF8))
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.CreateLogger();
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 849037ac4..ddcf2ac17 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -2,12 +2,17 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
-using MediaBrowser.Common.Plugins;
-using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common
{
/// <summary>
+ /// Delegate used with GetExports{T}.
+ /// </summary>
+ /// <param name="type">Type to create.</param>
+ /// <returns>New instance of type <param>type</param>.</returns>
+ public delegate object CreationDelegate(Type type);
+
+ /// <summary>
/// An interface to be implemented by the applications hosting a kernel.
/// </summary>
public interface IApplicationHost
@@ -54,6 +59,11 @@ namespace MediaBrowser.Common
Version ApplicationVersion { get; }
/// <summary>
+ /// Gets or sets the service provider.
+ /// </summary>
+ IServiceProvider ServiceProvider { get; set; }
+
+ /// <summary>
/// Gets the application version.
/// </summary>
/// <value>The application version.</value>
@@ -72,12 +82,6 @@ namespace MediaBrowser.Common
string ApplicationUserAgentAddress { get; }
/// <summary>
- /// Gets the plugins.
- /// </summary>
- /// <value>The plugins.</value>
- IReadOnlyList<IPlugin> Plugins { get; }
-
- /// <summary>
/// Gets all plugin assemblies which implement a custom rest api.
/// </summary>
/// <returns>An <see cref="IEnumerable{Assembly}"/> containing the plugin assemblies.</returns>
@@ -102,6 +106,22 @@ namespace MediaBrowser.Common
IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true);
/// <summary>
+ /// Gets the exports.
+ /// </summary>
+ /// <typeparam name="T">The type.</typeparam>
+ /// <param name="defaultFunc">Delegate function that gets called to create the object.</param>
+ /// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param>
+ /// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
+ IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true);
+
+ /// <summary>
+ /// Gets the export types.
+ /// </summary>
+ /// <typeparam name="T">The type.</typeparam>
+ /// <returns>IEnumerable{Type}.</returns>
+ IEnumerable<Type> GetExportTypes<T>();
+
+ /// <summary>
/// Resolves this instance.
/// </summary>
/// <typeparam name="T">The <c>Type</c>.</typeparam>
@@ -115,12 +135,6 @@ namespace MediaBrowser.Common
Task Shutdown();
/// <summary>
- /// Removes the plugin.
- /// </summary>
- /// <param name="plugin">The plugin.</param>
- void RemovePlugin(IPlugin plugin);
-
- /// <summary>
/// Initializes this instance.
/// </summary>
void Init();
diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
index a259cb7bc..38a7e1d20 100644
--- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
@@ -45,7 +45,7 @@ namespace MediaBrowser.Common.Json.Converters
{
// TODO log when upgraded to .Net6
// https://github.com/dotnet/runtime/issues/42975
- // _logger.LogWarning(e, "Error converting value.");
+ // _logger.LogDebug(e, "Error converting value.");
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs
new file mode 100644
index 000000000..6d96d5496
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Converts a GUID object or value to/from JSON.
+ /// </summary>
+ public class JsonNullableGuidConverter : JsonConverter<Guid?>
+ {
+ /// <inheritdoc />
+ public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var guidStr = reader.GetString();
+ return guidStr == null ? null : new Guid(guidStr);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options)
+ {
+ if (value == null || value == Guid.Empty)
+ {
+ writer.WriteNullValue();
+ }
+ else
+ {
+ writer.WriteStringValue(value.Value.ToString("N", CultureInfo.InvariantCulture));
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs
new file mode 100644
index 000000000..cb3d83f58
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs
@@ -0,0 +1,44 @@
+using System;
+using System.ComponentModel;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Converts a string <c>N/A</c> to <c>string.Empty</c>.
+ /// </summary>
+ public class JsonOmdbNotAvailableInt32Converter : JsonConverter<int?>
+ {
+ /// <inheritdoc />
+ public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var str = reader.GetString();
+ if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ var converter = TypeDescriptor.GetConverter(typeToConvert);
+ return (int?)converter.ConvertFromString(str);
+ }
+
+ return JsonSerializer.Deserialize<int?>(ref reader, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
+ {
+ if (value.HasValue)
+ {
+ writer.WriteNumberValue(value.Value);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs
new file mode 100644
index 000000000..6a8790374
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Converts a string <c>N/A</c> to <c>string.Empty</c>.
+ /// </summary>
+ public class JsonOmdbNotAvailableStringConverter : JsonConverter<string>
+ {
+ /// <inheritdoc />
+ public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var str = reader.GetString();
+ if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ return str;
+ }
+
+ return JsonSerializer.Deserialize<string>(ref reader, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
index 75fbcea1f..377db1a44 100644
--- a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
@@ -45,7 +45,7 @@ namespace MediaBrowser.Common.Json.Converters
{
// TODO log when upgraded to .Net6
// https://github.com/dotnet/runtime/issues/42975
- // _logger.LogWarning(e, "Error converting value.");
+ // _logger.LogDebug(e, "Error converting value.");
}
}
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 1ec2a87c5..9a2ea6875 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -31,9 +31,11 @@ namespace MediaBrowser.Common.Json
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ PropertyNameCaseInsensitive = true,
Converters =
{
new JsonGuidConverter(),
+ new JsonNullableGuidConverter(),
new JsonVersionConverter(),
new JsonStringEnumConverter(),
new JsonNullableStructConverterFactory(),
diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs
index a6f5fe4b3..5fab52eac 100644
--- a/MediaBrowser.Common/Net/IPNetAddress.cs
+++ b/MediaBrowser.Common/Net/IPNetAddress.cs
@@ -33,7 +33,7 @@ namespace MediaBrowser.Common.Net
/// <summary>
/// IP4Loopback address host.
/// </summary>
- public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/32");
+ public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/8");
/// <summary>
/// IP6Loopback address host.
diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs
index 084e91d50..e228ae7ec 100644
--- a/MediaBrowser.Common/Plugins/BasePlugin.cs
+++ b/MediaBrowser.Common/Plugins/BasePlugin.cs
@@ -1,5 +1,3 @@
-#pragma warning disable SA1402
-
using System;
using System.IO;
using System.Reflection;
@@ -7,7 +5,6 @@ using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins
{
@@ -64,14 +61,12 @@ namespace MediaBrowser.Common.Plugins
/// <returns>PluginInfo.</returns>
public virtual PluginInfo GetPluginInfo()
{
- var info = new PluginInfo
- {
- Name = Name,
- Version = Version.ToString(),
- Description = Description,
- Id = Id.ToString(),
- CanUninstall = CanUninstall
- };
+ var info = new PluginInfo(
+ Name,
+ Version,
+ Description,
+ Id,
+ CanUninstall);
return info;
}
@@ -97,207 +92,4 @@ namespace MediaBrowser.Common.Plugins
Id = assemblyId;
}
}
-
- /// <summary>
- /// Provides a common base class for all plugins.
- /// </summary>
- /// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam>
- public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration
- where TConfigurationType : BasePluginConfiguration
- {
- /// <summary>
- /// The configuration sync lock.
- /// </summary>
- private readonly object _configurationSyncLock = new object();
-
- /// <summary>
- /// The configuration save lock.
- /// </summary>
- private readonly object _configurationSaveLock = new object();
-
- private Action<string> _directoryCreateFn;
-
- /// <summary>
- /// The configuration.
- /// </summary>
- private TConfigurationType _configuration;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
- /// </summary>
- /// <param name="applicationPaths">The application paths.</param>
- /// <param name="xmlSerializer">The XML serializer.</param>
- protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
- {
- ApplicationPaths = applicationPaths;
- XmlSerializer = xmlSerializer;
- if (this is IPluginAssembly assemblyPlugin)
- {
- var assembly = GetType().Assembly;
- var assemblyName = assembly.GetName();
- var assemblyFilePath = assembly.Location;
-
- var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
-
- assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
-
- var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
- if (idAttributes.Length > 0)
- {
- var attribute = (GuidAttribute)idAttributes[0];
- var assemblyId = new Guid(attribute.Value);
-
- assemblyPlugin.SetId(assemblyId);
- }
- }
-
- if (this is IHasPluginConfiguration hasPluginConfiguration)
- {
- hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
- }
- }
-
- /// <summary>
- /// Gets the application paths.
- /// </summary>
- /// <value>The application paths.</value>
- protected IApplicationPaths ApplicationPaths { get; private set; }
-
- /// <summary>
- /// Gets the XML serializer.
- /// </summary>
- /// <value>The XML serializer.</value>
- protected IXmlSerializer XmlSerializer { get; private set; }
-
- /// <summary>
- /// Gets the type of configuration this plugin uses.
- /// </summary>
- /// <value>The type of the configuration.</value>
- public Type ConfigurationType => typeof(TConfigurationType);
-
- /// <summary>
- /// Gets or sets the event handler that is triggered when this configuration changes.
- /// </summary>
- public EventHandler<BasePluginConfiguration> ConfigurationChanged { get; set; }
-
- /// <summary>
- /// Gets the name the assembly file.
- /// </summary>
- /// <value>The name of the assembly file.</value>
- protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath);
-
- /// <summary>
- /// Gets or sets the plugin configuration.
- /// </summary>
- /// <value>The configuration.</value>
- public TConfigurationType Configuration
- {
- get
- {
- // Lazy load
- if (_configuration == null)
- {
- lock (_configurationSyncLock)
- {
- if (_configuration == null)
- {
- _configuration = LoadConfiguration();
- }
- }
- }
-
- return _configuration;
- }
-
- protected set => _configuration = value;
- }
-
- /// <summary>
- /// Gets the name of the configuration file. Subclasses should override.
- /// </summary>
- /// <value>The name of the configuration file.</value>
- public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml");
-
- /// <summary>
- /// Gets the full path to the configuration file.
- /// </summary>
- /// <value>The configuration file path.</value>
- public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
-
- /// <summary>
- /// Gets the plugin configuration.
- /// </summary>
- /// <value>The configuration.</value>
- BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration;
-
- /// <inheritdoc />
- public void SetStartupInfo(Action<string> directoryCreateFn)
- {
- // hack alert, until the .net core transition is complete
- _directoryCreateFn = directoryCreateFn;
- }
-
- private TConfigurationType LoadConfiguration()
- {
- var path = ConfigurationFilePath;
-
- try
- {
- return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
- }
- catch
- {
- var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
- SaveConfiguration(config);
- return config;
- }
- }
-
- /// <summary>
- /// Saves the current configuration to the file system.
- /// </summary>
- /// <param name="config">Configuration to save.</param>
- public virtual void SaveConfiguration(TConfigurationType config)
- {
- lock (_configurationSaveLock)
- {
- _directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
-
- XmlSerializer.SerializeToFile(config, ConfigurationFilePath);
- }
- }
-
- /// <summary>
- /// Saves the current configuration to the file system.
- /// </summary>
- public virtual void SaveConfiguration()
- {
- SaveConfiguration(Configuration);
- }
-
- /// <inheritdoc />
- public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
- {
- if (configuration == null)
- {
- throw new ArgumentNullException(nameof(configuration));
- }
-
- Configuration = (TConfigurationType)configuration;
-
- SaveConfiguration(Configuration);
-
- ConfigurationChanged?.Invoke(this, configuration);
- }
-
- /// <inheritdoc />
- public override PluginInfo GetPluginInfo()
- {
- var info = base.GetPluginInfo();
-
- info.ConfigurationFileName = ConfigurationFileName;
-
- return info;
- }
- }
}
diff --git a/MediaBrowser.Common/Plugins/BasePluginOfT.cs b/MediaBrowser.Common/Plugins/BasePluginOfT.cs
new file mode 100644
index 000000000..d5c780851
--- /dev/null
+++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs
@@ -0,0 +1,208 @@
+#pragma warning disable SA1649 // File name should match first type name
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Common.Plugins
+{
+ /// <summary>
+ /// Provides a common base class for all plugins.
+ /// </summary>
+ /// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam>
+ public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration
+ where TConfigurationType : BasePluginConfiguration
+ {
+ /// <summary>
+ /// The configuration sync lock.
+ /// </summary>
+ private readonly object _configurationSyncLock = new object();
+
+ /// <summary>
+ /// The configuration save lock.
+ /// </summary>
+ private readonly object _configurationSaveLock = new object();
+
+ /// <summary>
+ /// The configuration.
+ /// </summary>
+ private TConfigurationType _configuration;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
+ /// </summary>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="xmlSerializer">The XML serializer.</param>
+ protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ {
+ ApplicationPaths = applicationPaths;
+ XmlSerializer = xmlSerializer;
+ if (this is IPluginAssembly assemblyPlugin)
+ {
+ var assembly = GetType().Assembly;
+ var assemblyName = assembly.GetName();
+ var assemblyFilePath = assembly.Location;
+
+ var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
+ if (!Directory.Exists(dataFolderPath) && Version != null)
+ {
+ // Try again with the version number appended to the folder name.
+ dataFolderPath = dataFolderPath + "_" + Version.ToString();
+ }
+
+ assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
+
+ var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
+ if (idAttributes.Length > 0)
+ {
+ var attribute = (GuidAttribute)idAttributes[0];
+ var assemblyId = new Guid(attribute.Value);
+
+ assemblyPlugin.SetId(assemblyId);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ protected IApplicationPaths ApplicationPaths { get; private set; }
+
+ /// <summary>
+ /// Gets the XML serializer.
+ /// </summary>
+ /// <value>The XML serializer.</value>
+ protected IXmlSerializer XmlSerializer { get; private set; }
+
+ /// <summary>
+ /// Gets the type of configuration this plugin uses.
+ /// </summary>
+ /// <value>The type of the configuration.</value>
+ public Type ConfigurationType => typeof(TConfigurationType);
+
+ /// <summary>
+ /// Gets or sets the event handler that is triggered when this configuration changes.
+ /// </summary>
+ public EventHandler<BasePluginConfiguration> ConfigurationChanged { get; set; }
+
+ /// <summary>
+ /// Gets the name the assembly file.
+ /// </summary>
+ /// <value>The name of the assembly file.</value>
+ protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath);
+
+ /// <summary>
+ /// Gets or sets the plugin configuration.
+ /// </summary>
+ /// <value>The configuration.</value>
+ public TConfigurationType Configuration
+ {
+ get
+ {
+ // Lazy load
+ if (_configuration == null)
+ {
+ lock (_configurationSyncLock)
+ {
+ if (_configuration == null)
+ {
+ _configuration = LoadConfiguration();
+ }
+ }
+ }
+
+ return _configuration;
+ }
+
+ protected set => _configuration = value;
+ }
+
+ /// <summary>
+ /// Gets the name of the configuration file. Subclasses should override.
+ /// </summary>
+ /// <value>The name of the configuration file.</value>
+ public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml");
+
+ /// <summary>
+ /// Gets the full path to the configuration file.
+ /// </summary>
+ /// <value>The configuration file path.</value>
+ public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
+
+ /// <summary>
+ /// Gets the plugin configuration.
+ /// </summary>
+ /// <value>The configuration.</value>
+ BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration;
+
+ /// <summary>
+ /// Saves the current configuration to the file system.
+ /// </summary>
+ /// <param name="config">Configuration to save.</param>
+ public virtual void SaveConfiguration(TConfigurationType config)
+ {
+ lock (_configurationSaveLock)
+ {
+ var folder = Path.GetDirectoryName(ConfigurationFilePath);
+ if (!Directory.Exists(folder))
+ {
+ Directory.CreateDirectory(folder);
+ }
+
+ XmlSerializer.SerializeToFile(config, ConfigurationFilePath);
+ }
+ }
+
+ /// <summary>
+ /// Saves the current configuration to the file system.
+ /// </summary>
+ public virtual void SaveConfiguration()
+ {
+ SaveConfiguration(Configuration);
+ }
+
+ /// <inheritdoc />
+ public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ Configuration = (TConfigurationType)configuration;
+
+ SaveConfiguration(Configuration);
+
+ ConfigurationChanged?.Invoke(this, configuration);
+ }
+
+ /// <inheritdoc />
+ public override PluginInfo GetPluginInfo()
+ {
+ var info = base.GetPluginInfo();
+
+ info.ConfigurationFileName = ConfigurationFileName;
+
+ return info;
+ }
+
+ private TConfigurationType LoadConfiguration()
+ {
+ var path = ConfigurationFilePath;
+
+ try
+ {
+ return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
+ }
+ catch
+ {
+ var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
+ SaveConfiguration(config);
+ return config;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs b/MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs
new file mode 100644
index 000000000..af9272caa
--- /dev/null
+++ b/MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs
@@ -0,0 +1,27 @@
+using System;
+using MediaBrowser.Model.Plugins;
+
+namespace MediaBrowser.Common.Plugins
+{
+ /// <summary>
+ /// Defines the <see cref="IHasPluginConfiguration" />.
+ /// </summary>
+ public interface IHasPluginConfiguration
+ {
+ /// <summary>
+ /// Gets the type of configuration this plugin uses.
+ /// </summary>
+ Type ConfigurationType { get; }
+
+ /// <summary>
+ /// Gets the plugin's configuration.
+ /// </summary>
+ BasePluginConfiguration Configuration { get; }
+
+ /// <summary>
+ /// Completely overwrites the current configuration with a new copy.
+ /// </summary>
+ /// <param name="configuration">The configuration.</param>
+ void UpdateConfiguration(BasePluginConfiguration configuration);
+ }
+}
diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs
index d583a5887..b2ba1179c 100644
--- a/MediaBrowser.Common/Plugins/IPlugin.cs
+++ b/MediaBrowser.Common/Plugins/IPlugin.cs
@@ -1,44 +1,36 @@
-#pragma warning disable CS1591
-
using System;
using MediaBrowser.Model.Plugins;
-using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
- /// Interface IPlugin.
+ /// Defines the <see cref="IPlugin" />.
/// </summary>
public interface IPlugin
{
/// <summary>
/// Gets the name of the plugin.
/// </summary>
- /// <value>The name.</value>
string Name { get; }
/// <summary>
- /// Gets the description.
+ /// Gets the Description.
/// </summary>
- /// <value>The description.</value>
string Description { get; }
/// <summary>
/// Gets the unique id.
/// </summary>
- /// <value>The unique id.</value>
Guid Id { get; }
/// <summary>
/// Gets the plugin version.
/// </summary>
- /// <value>The version.</value>
Version Version { get; }
/// <summary>
/// Gets the path to the assembly file.
/// </summary>
- /// <value>The assembly file path.</value>
string AssemblyFilePath { get; }
/// <summary>
@@ -49,11 +41,10 @@ namespace MediaBrowser.Common.Plugins
/// <summary>
/// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.
/// </summary>
- /// <value>The data folder path.</value>
string DataFolderPath { get; }
/// <summary>
- /// Gets the plugin info.
+ /// Gets the <see cref="PluginInfo"/>.
/// </summary>
/// <returns>PluginInfo.</returns>
PluginInfo GetPluginInfo();
@@ -63,29 +54,4 @@ namespace MediaBrowser.Common.Plugins
/// </summary>
void OnUninstalling();
}
-
- public interface IHasPluginConfiguration
- {
- /// <summary>
- /// Gets the type of configuration this plugin uses.
- /// </summary>
- /// <value>The type of the configuration.</value>
- Type ConfigurationType { get; }
-
- /// <summary>
- /// Gets the plugin's configuration.
- /// </summary>
- /// <value>The configuration.</value>
- BasePluginConfiguration Configuration { get; }
-
- /// <summary>
- /// Completely overwrites the current configuration with a new copy
- /// Returns true or false indicating success or failure.
- /// </summary>
- /// <param name="configuration">The configuration.</param>
- /// <exception cref="ArgumentNullException"><c>configuration</c> is <c>null</c>.</exception>
- void UpdateConfiguration(BasePluginConfiguration configuration);
-
- void SetStartupInfo(Action<string> directoryCreateFn);
- }
}
diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs
new file mode 100644
index 000000000..3da34d8bb
--- /dev/null
+++ b/MediaBrowser.Common/Plugins/IPluginManager.cs
@@ -0,0 +1,86 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace MediaBrowser.Common.Plugins
+{
+ /// <summary>
+ /// Defines the <see cref="IPluginManager" />.
+ /// </summary>
+ public interface IPluginManager
+ {
+ /// <summary>
+ /// Gets the Plugins.
+ /// </summary>
+ IList<LocalPlugin> Plugins { get; }
+
+ /// <summary>
+ /// Creates the plugins.
+ /// </summary>
+ void CreatePlugins();
+
+ /// <summary>
+ /// Returns all the assemblies.
+ /// </summary>
+ /// <returns>An IEnumerable{Assembly}.</returns>
+ IEnumerable<Assembly> LoadAssemblies();
+
+ /// <summary>
+ /// Registers the plugin's services with the DI.
+ /// Note: DI is not yet instantiated yet.
+ /// </summary>
+ /// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param>
+ void RegisterServices(IServiceCollection serviceCollection);
+
+ /// <summary>
+ /// Saves the manifest back to disk.
+ /// </summary>
+ /// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
+ /// <param name="path">The path where to save the manifest.</param>
+ /// <returns>True if successful.</returns>
+ bool SaveManifest(PluginManifest manifest, string path);
+
+ /// <summary>
+ /// Imports plugin details from a folder.
+ /// </summary>
+ /// <param name="folder">Folder of the plugin.</param>
+ void ImportPluginFrom(string folder);
+
+ /// <summary>
+ /// Disable the plugin.
+ /// </summary>
+ /// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param>
+ void FailPlugin(Assembly assembly);
+
+ /// <summary>
+ /// Disable the plugin.
+ /// </summary>
+ /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
+ void DisablePlugin(LocalPlugin plugin);
+
+ /// <summary>
+ /// Enables the plugin, disabling all other versions.
+ /// </summary>
+ /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
+ void EnablePlugin(LocalPlugin plugin);
+
+ /// <summary>
+ /// Attempts to find the plugin with and id of <paramref name="id"/>.
+ /// </summary>
+ /// <param name="id">Id of plugin.</param>
+ /// <param name="version">The version of the plugin to locate.</param>
+ /// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns>
+ LocalPlugin? GetPlugin(Guid id, Version? version = null);
+
+ /// <summary>
+ /// Removes the plugin.
+ /// </summary>
+ /// <param name="plugin">The plugin.</param>
+ /// <returns>Outcome of the operation.</returns>
+ bool RemovePlugin(LocalPlugin plugin);
+ }
+}
diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs
index c97e75a3b..23b6cfa81 100644
--- a/MediaBrowser.Common/Plugins/LocalPlugin.cs
+++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs
@@ -1,6 +1,7 @@
+#nullable enable
using System;
using System.Collections.Generic;
-using System.Globalization;
+using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
@@ -9,36 +10,48 @@ namespace MediaBrowser.Common.Plugins
/// </summary>
public class LocalPlugin : IEquatable<LocalPlugin>
{
+ private readonly bool _supported;
+ private Version? _version;
+
/// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
/// </summary>
- /// <param name="id">The plugin id.</param>
- /// <param name="name">The plugin name.</param>
- /// <param name="version">The plugin version.</param>
/// <param name="path">The plugin path.</param>
- public LocalPlugin(Guid id, string name, Version version, string path)
+ /// <param name="isSupported"><b>True</b> if Jellyfin supports this version of the plugin.</param>
+ /// <param name="manifest">The manifest record for this plugin, or null if one does not exist.</param>
+ public LocalPlugin(string path, bool isSupported, PluginManifest manifest)
{
- Id = id;
- Name = name;
- Version = version;
Path = path;
DllFiles = new List<string>();
+ _supported = isSupported;
+ Manifest = manifest;
}
/// <summary>
/// Gets the plugin id.
/// </summary>
- public Guid Id { get; }
+ public Guid Id => Manifest.Id;
/// <summary>
/// Gets the plugin name.
/// </summary>
- public string Name { get; }
+ public string Name => Manifest.Name;
/// <summary>
/// Gets the plugin version.
/// </summary>
- public Version Version { get; }
+ public Version Version
+ {
+ get
+ {
+ if (_version == null)
+ {
+ _version = Version.Parse(Manifest.Version);
+ }
+
+ return _version;
+ }
+ }
/// <summary>
/// Gets the plugin path.
@@ -51,26 +64,19 @@ namespace MediaBrowser.Common.Plugins
public List<string> DllFiles { get; }
/// <summary>
- /// == operator.
+ /// Gets or sets the instance of this plugin.
/// </summary>
- /// <param name="left">Left item.</param>
- /// <param name="right">Right item.</param>
- /// <returns>Comparison result.</returns>
- public static bool operator ==(LocalPlugin left, LocalPlugin right)
- {
- return left.Equals(right);
- }
+ public IPlugin? Instance { get; set; }
/// <summary>
- /// != operator.
+ /// Gets a value indicating whether Jellyfin supports this version of the plugin, and it's enabled.
/// </summary>
- /// <param name="left">Left item.</param>
- /// <param name="right">Right item.</param>
- /// <returns>Comparison result.</returns>
- public static bool operator !=(LocalPlugin left, LocalPlugin right)
- {
- return !left.Equals(right);
- }
+ public bool IsEnabledAndSupported => _supported && Manifest.Status >= PluginStatus.Active;
+
+ /// <summary>
+ /// Gets a value indicating whether the plugin has a manifest.
+ /// </summary>
+ public PluginManifest Manifest { get; }
/// <summary>
/// Compare two <see cref="LocalPlugin"/>.
@@ -80,10 +86,15 @@ namespace MediaBrowser.Common.Plugins
/// <returns>Comparison result.</returns>
public static int Compare(LocalPlugin a, LocalPlugin b)
{
- var compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
+ if (a == null || b == null)
+ {
+ throw new ArgumentNullException(a == null ? nameof(a) : nameof(b));
+ }
+
+ var compare = string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
// Id is not equal but name is.
- if (a.Id != b.Id && compare == 0)
+ if (!a.Id.Equals(b.Id) && compare == 0)
{
compare = a.Id.CompareTo(b.Id);
}
@@ -91,8 +102,20 @@ namespace MediaBrowser.Common.Plugins
return compare == 0 ? a.Version.CompareTo(b.Version) : compare;
}
+ /// <summary>
+ /// Returns the plugin information.
+ /// </summary>
+ /// <returns>A <see cref="PluginInfo"/> instance containing the information.</returns>
+ public PluginInfo GetPluginInfo()
+ {
+ var inst = Instance?.GetPluginInfo() ?? new PluginInfo(Manifest.Name, Version, Manifest.Description, Manifest.Id, true);
+ inst.Status = Manifest.Status;
+ inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath);
+ return inst;
+ }
+
/// <inheritdoc />
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
return obj is LocalPlugin other && this.Equals(other);
}
@@ -104,16 +127,14 @@ namespace MediaBrowser.Common.Plugins
}
/// <inheritdoc />
- public bool Equals(LocalPlugin other)
+ public bool Equals(LocalPlugin? other)
{
- // Do not use == or != for comparison as this class overrides the operators.
- if (object.ReferenceEquals(other, null))
+ if (other == null)
{
return false;
}
- return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase)
- && Id.Equals(other.Id);
+ return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && Id.Equals(other.Id) && Version.Equals(other.Version);
}
}
}
diff --git a/MediaBrowser.Common/Plugins/PluginManifest.cs b/MediaBrowser.Common/Plugins/PluginManifest.cs
new file mode 100644
index 000000000..4c724f694
--- /dev/null
+++ b/MediaBrowser.Common/Plugins/PluginManifest.cs
@@ -0,0 +1,110 @@
+#nullable enable
+
+using System;
+using System.Text.Json.Serialization;
+using MediaBrowser.Model.Plugins;
+
+namespace MediaBrowser.Common.Plugins
+{
+ /// <summary>
+ /// Defines a Plugin manifest file.
+ /// </summary>
+ public class PluginManifest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginManifest"/> class.
+ /// </summary>
+ public PluginManifest()
+ {
+ Category = string.Empty;
+ Changelog = string.Empty;
+ Description = string.Empty;
+ Id = Guid.Empty;
+ Name = string.Empty;
+ Owner = string.Empty;
+ Overview = string.Empty;
+ TargetAbi = string.Empty;
+ Version = string.Empty;
+ }
+
+ /// <summary>
+ /// Gets or sets the category of the plugin.
+ /// </summary>
+ [JsonPropertyName("category")]
+ public string Category { get; set; }
+
+ /// <summary>
+ /// Gets or sets the changelog information.
+ /// </summary>
+ [JsonPropertyName("changelog")]
+ public string Changelog { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description of the plugin.
+ /// </summary>
+ [JsonPropertyName("description")]
+ public string Description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Global Unique Identifier for the plugin.
+ /// </summary>
+ [JsonPropertyName("guid")]
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Name of the plugin.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets an overview of the plugin.
+ /// </summary>
+ [JsonPropertyName("overview")]
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the owner of the plugin.
+ /// </summary>
+ [JsonPropertyName("owner")]
+ public string Owner { get; set; }
+
+ /// <summary>
+ /// Gets or sets the compatibility version for the plugin.
+ /// </summary>
+ [JsonPropertyName("targetAbi")]
+ public string TargetAbi { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timestamp of the plugin.
+ /// </summary>
+ [JsonPropertyName("timestamp")]
+ public DateTime Timestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Version number of the plugin.
+ /// </summary>
+ [JsonPropertyName("version")]
+ public string Version { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the operational status of this plugin.
+ /// </summary>
+ [JsonPropertyName("status")]
+ public PluginStatus Status { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this plugin should automatically update.
+ /// </summary>
+ [JsonPropertyName("autoUpdate")]
+ public bool AutoUpdate { get; set; } = true; // DO NOT MOVE THIS INTO THE CONSTRUCTOR.
+
+ /// <summary>
+ /// Gets or sets the ImagePath
+ /// Gets or sets a value indicating whether this plugin has an image.
+ /// Image must be located in the local plugin folder.
+ /// </summary>
+ [JsonPropertyName("imagePath")]
+ public string? ImagePath { get; set; }
+ }
+}
diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs
index 585b1ee19..0844c2d79 100644
--- a/MediaBrowser.Common/Updates/IInstallationManager.cs
+++ b/MediaBrowser.Common/Updates/IInstallationManager.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#nullable enable
using System;
using System.Collections.Generic;
@@ -9,6 +9,9 @@ using MediaBrowser.Model.Updates;
namespace MediaBrowser.Common.Updates
{
+ /// <summary>
+ /// Defines the <see cref="IInstallationManager" />.
+ /// </summary>
public interface IInstallationManager : IDisposable
{
/// <summary>
@@ -21,12 +24,13 @@ namespace MediaBrowser.Common.Updates
/// </summary>
/// <param name="manifestName">Name of the repository.</param>
/// <param name="manifest">The URL to query.</param>
+ /// <param name="filterIncompatible">Filter out incompatible plugins.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
- Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default);
+ Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
/// <summary>
- /// Gets all available packages.
+ /// Gets all available packages that are supported by this version.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
@@ -37,33 +41,33 @@ namespace MediaBrowser.Common.Updates
/// </summary>
/// <param name="availablePackages">The available packages.</param>
/// <param name="name">The name of the plugin.</param>
- /// <param name="guid">The id of the plugin.</param>
+ /// <param name="id">The id of the plugin.</param>
/// <param name="specificVersion">The version of the plugin.</param>
/// <returns>All plugins matching the requirements.</returns>
IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
- string name = null,
- Guid guid = default,
- Version specificVersion = null);
+ string? name = null,
+ Guid? id = default,
+ Version? specificVersion = null);
/// <summary>
/// Returns all compatible versions ordered from newest to oldest.
/// </summary>
/// <param name="availablePackages">The available packages.</param>
/// <param name="name">The name.</param>
- /// <param name="guid">The guid of the plugin.</param>
+ /// <param name="id">The id of the plugin.</param>
/// <param name="minVersion">The minimum required version of the plugin.</param>
/// <param name="specificVersion">The specific version of the plugin to install.</param>
/// <returns>All compatible versions ordered from newest to oldest.</returns>
IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
- string name = null,
- Guid guid = default,
- Version minVersion = null,
- Version specificVersion = null);
+ string? name = null,
+ Guid? id = default,
+ Version? minVersion = null,
+ Version? specificVersion = null);
/// <summary>
- /// Returns the available plugin updates.
+ /// Returns the available compatible plugin updates.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The available plugin updates.</returns>
@@ -81,7 +85,7 @@ namespace MediaBrowser.Common.Updates
/// Uninstalls a plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
- void UninstallPlugin(IPlugin plugin);
+ void UninstallPlugin(LocalPlugin plugin);
/// <summary>
/// Cancels the installation.
diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs
index 61178f631..adf336313 100644
--- a/MediaBrowser.Common/Updates/InstallationEventArgs.cs
+++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs
@@ -1,14 +1,21 @@
-#pragma warning disable CS1591
-
using System;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Common.Updates
{
+ /// <summary>
+ /// Defines the <see cref="InstallationEventArgs" />.
+ /// </summary>
public class InstallationEventArgs : EventArgs
{
+ /// <summary>
+ /// Gets or sets the <see cref="InstallationInfo"/>.
+ /// </summary>
public InstallationInfo InstallationInfo { get; set; }
+ /// <summary>
+ /// Gets or sets the <see cref="VersionInfo"/>.
+ /// </summary>
public VersionInfo VersionInfo { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index d25545a2f..c3b6af76e 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -4,9 +4,11 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -24,10 +26,9 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class CollectionFolder : Folder, ICollectionFolder
{
+ private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
public static IXmlSerializer XmlSerializer { get; set; }
- public static IJsonSerializer JsonSerializer { get; set; }
-
public static IServerApplicationHost ApplicationHost { get; set; }
public CollectionFolder()
@@ -122,7 +123,7 @@ namespace MediaBrowser.Controller.Entities
{
LibraryOptions[path] = options;
- var clone = JsonSerializer.DeserializeFromString<LibraryOptions>(JsonSerializer.SerializeToString(options));
+ var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.Serialize(options, _jsonOptions), _jsonOptions);
foreach (var mediaPath in clone.PathInfos)
{
if (!string.IsNullOrEmpty(mediaPath.Path))
diff --git a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs
index 7510b62b8..a111e6d82 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs
@@ -1,18 +1,19 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Controller.Events.Updates
{
/// <summary>
/// An event that occurs when a plugin is uninstalled.
/// </summary>
- public class PluginUninstalledEventArgs : GenericEventArgs<IPlugin>
+ public class PluginUninstalledEventArgs : GenericEventArgs<PluginInfo>
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginUninstalledEventArgs"/> class.
/// </summary>
/// <param name="arg">The plugin.</param>
- public PluginUninstalledEventArgs(IPlugin arg) : base(arg)
+ public PluginUninstalledEventArgs(PluginInfo arg) : base(arg)
{
}
}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 2456da826..92b2d43ce 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -19,8 +19,6 @@ namespace MediaBrowser.Controller
{
event EventHandler HasUpdateAvailableChanged;
- IServiceProvider ServiceProvider { get; }
-
bool CoreStartupHasCompleted { get; }
bool CanLaunchWebBrowser { get; }
@@ -122,13 +120,5 @@ namespace MediaBrowser.Controller
string ExpandVirtualPath(string path);
string ReverseVirtualPath(string path);
-
- /// <summary>
- /// Gets the list of local plugins.
- /// </summary>
- /// <param name="path">Plugin base directory.</param>
- /// <param name="cleanup">Cleanup old plugins.</param>
- /// <returns>Enumerable of local plugins.</returns>
- IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true);
}
}
diff --git a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs
index ac73a5ea8..d4f975b6d 100644
--- a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs
+++ b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs
@@ -26,8 +26,13 @@ namespace MediaBrowser.Controller.Resolvers
Fourth = 4,
/// <summary>
+ /// The Fifth.
+ /// </summary>
+ Fifth = 5,
+
+ /// <summary>
/// The last.
/// </summary>
- Last = 5
+ Last = 6
}
}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 0dbd51bdc..0f0ad0f9a 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -49,8 +49,6 @@ namespace MediaBrowser.Model.Configuration
new MetadataOptions
{
ItemType = "Series",
- DisabledMetadataFetchers = new[] { "TheMovieDb" },
- DisabledImageFetchers = new[] { "TheMovieDb" }
},
new MetadataOptions
{
@@ -69,13 +67,10 @@ namespace MediaBrowser.Model.Configuration
new MetadataOptions
{
ItemType = "Season",
- DisabledMetadataFetchers = new[] { "TheMovieDb" },
},
new MetadataOptions
{
ItemType = "Episode",
- DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" },
- DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" }
}
};
}
@@ -304,6 +299,18 @@ namespace MediaBrowser.Model.Configuration
public int MinResumeDurationSeconds { get; set; } = 300;
/// <summary>
+ /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated.
+ /// </summary>
+ /// <value>The min resume in minutes.</value>
+ public int MinAudiobookResume { get; set; } = 5;
+
+ /// <summary>
+ /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
+ /// </summary>
+ /// <value>The remaining time in minutes.</value>
+ public int MaxAudiobookResume { get; set; } = 5;
+
+ /// <summary>
/// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
/// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
/// different directories and files.
@@ -449,5 +456,15 @@ namespace MediaBrowser.Model.Configuration
/// Gets or sets the how many metadata refreshes can run concurrently.
/// </summary>
public int LibraryMetadataRefreshConcurrency { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
+ /// </summary>
+ public bool RemoveOldPlugins { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether plugin image should be disabled.
+ /// </summary>
+ public bool DisablePluginImages { get; set; }
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 334fe8209..c271a9cf8 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
diff --git a/MediaBrowser.Model/Plugins/PluginInfo.cs b/MediaBrowser.Model/Plugins/PluginInfo.cs
index dd215192f..25216610d 100644
--- a/MediaBrowser.Model/Plugins/PluginInfo.cs
+++ b/MediaBrowser.Model/Plugins/PluginInfo.cs
@@ -1,4 +1,7 @@
-#nullable disable
+#nullable enable
+
+using System;
+
namespace MediaBrowser.Model.Plugins
{
/// <summary>
@@ -7,34 +10,46 @@ namespace MediaBrowser.Model.Plugins
public class PluginInfo
{
/// <summary>
+ /// Initializes a new instance of the <see cref="PluginInfo"/> class.
+ /// </summary>
+ /// <param name="name">The plugin name.</param>
+ /// <param name="version">The plugin <see cref="Version"/>.</param>
+ /// <param name="description">The plugin description.</param>
+ /// <param name="id">The <see cref="Guid"/>.</param>
+ /// <param name="canUninstall">True if this plugin can be uninstalled.</param>
+ public PluginInfo(string name, Version version, string description, Guid id, bool canUninstall)
+ {
+ Name = name;
+ Version = version;
+ Description = description;
+ Id = id;
+ CanUninstall = canUninstall;
+ }
+
+ /// <summary>
/// Gets or sets the name.
/// </summary>
- /// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the version.
/// </summary>
- /// <value>The version.</value>
- public string Version { get; set; }
+ public Version Version { get; set; }
/// <summary>
/// Gets or sets the name of the configuration file.
/// </summary>
- /// <value>The name of the configuration file.</value>
- public string ConfigurationFileName { get; set; }
+ public string? ConfigurationFileName { get; set; }
/// <summary>
/// Gets or sets the description.
/// </summary>
- /// <value>The description.</value>
public string Description { get; set; }
/// <summary>
/// Gets or sets the unique id.
/// </summary>
- /// <value>The unique id.</value>
- public string Id { get; set; }
+ public Guid Id { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the plugin can be uninstalled.
@@ -42,9 +57,13 @@ namespace MediaBrowser.Model.Plugins
public bool CanUninstall { get; set; }
/// <summary>
- /// Gets or sets the image URL.
+ /// Gets or sets a value indicating whether this plugin has a valid image.
+ /// </summary>
+ public bool HasImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the status of the plugin.
/// </summary>
- /// <value>The image URL.</value>
- public string ImageUrl { get; set; }
+ public PluginStatus Status { get; set; }
}
}
diff --git a/MediaBrowser.Model/Plugins/PluginPageInfo.cs b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
index ca72e19ee..85c0aa204 100644
--- a/MediaBrowser.Model/Plugins/PluginPageInfo.cs
+++ b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
@@ -1,20 +1,40 @@
-#nullable disable
-#pragma warning disable CS1591
+#nullable enable
namespace MediaBrowser.Model.Plugins
{
+ /// <summary>
+ /// Defines the <see cref="PluginPageInfo" />.
+ /// </summary>
public class PluginPageInfo
{
- public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ public string Name { get; set; } = string.Empty;
- public string DisplayName { get; set; }
+ /// <summary>
+ /// Gets or sets the display name.
+ /// </summary>
+ public string? DisplayName { get; set; }
- public string EmbeddedResourcePath { get; set; }
+ /// <summary>
+ /// Gets or sets the resource path.
+ /// </summary>
+ public string EmbeddedResourcePath { get; set; } = string.Empty;
+ /// <summary>
+ /// Gets or sets a value indicating whether this plugin should appear in the main menu.
+ /// </summary>
public bool EnableInMainMenu { get; set; }
- public string MenuSection { get; set; }
+ /// <summary>
+ /// Gets or sets the menu section.
+ /// </summary>
+ public string? MenuSection { get; set; }
- public string MenuIcon { get; set; }
+ /// <summary>
+ /// Gets or sets the menu icon.
+ /// </summary>
+ public string? MenuIcon { get; set; }
}
}
diff --git a/MediaBrowser.Model/Plugins/PluginStatus.cs b/MediaBrowser.Model/Plugins/PluginStatus.cs
new file mode 100644
index 000000000..4b9b9bbee
--- /dev/null
+++ b/MediaBrowser.Model/Plugins/PluginStatus.cs
@@ -0,0 +1,47 @@
+namespace MediaBrowser.Model.Plugins
+{
+ /// <summary>
+ /// Plugin load status.
+ /// </summary>
+ public enum PluginStatus
+ {
+ /// <summary>
+ /// This plugin requires a restart in order for it to load. This is a memory only status.
+ /// The actual status of the plugin after reload is present in the manifest.
+ /// eg. A disabled plugin will still be active until the next restart, and so will have a memory status of Restart,
+ /// but a disk manifest status of Disabled.
+ /// </summary>
+ Restart = 1,
+
+ /// <summary>
+ /// This plugin is currently running.
+ /// </summary>
+ Active = 0,
+
+ /// <summary>
+ /// This plugin has been marked as disabled.
+ /// </summary>
+ Disabled = -1,
+
+ /// <summary>
+ /// This plugin does not meet the TargetAbi requirements.
+ /// </summary>
+ NotSupported = -2,
+
+ /// <summary>
+ /// This plugin caused an error when instantiated. (Either DI loop, or exception)
+ /// </summary>
+ Malfunctioned = -3,
+
+ /// <summary>
+ /// This plugin has been superceded by another version.
+ /// </summary>
+ Superceded = -4,
+
+ /// <summary>
+ /// An attempt to remove this plugin from disk will happen at every restart.
+ /// It will not be loaded, if unable to do so.
+ /// </summary>
+ Deleted = -5
+ }
+}
diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
index 01784554f..afe95e6ee 100644
--- a/MediaBrowser.Model/Providers/ExternalIdInfo.cs
+++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
@@ -6,16 +6,31 @@ namespace MediaBrowser.Model.Providers
public class ExternalIdInfo
{
/// <summary>
+ /// Represents the external id information for serialization to the client.
+ /// </summary>
+ /// <param name="name">Name of the external id provider (IE: IMDB, MusicBrainz, etc).</param>
+ /// <param name="key">Key for this id. This key should be unique across all providers.</param>
+ /// <param name="type">Specific media type for this id</param>
+ /// <param name="urlFormatString">URL format string.</param>
+ public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string urlFormatString)
+ {
+ Name = name;
+ Key = key;
+ Type = type;
+ UrlFormatString = urlFormatString;
+ }
+
+ /// <summary>
/// Gets or sets the display name of the external id provider (IE: IMDB, MusicBrainz, etc).
/// </summary>
// TODO: This should be renamed to ProviderName
- public string? Name { get; set; }
+ public string Name { get; set; }
/// <summary>
/// Gets or sets the unique key for this id. This key should be unique across all providers.
/// </summary>
// TODO: This property is not actually unique across the concrete types at the moment. It should be updated to be unique.
- public string? Key { get; set; }
+ public string Key { get; set; }
/// <summary>
/// Gets or sets the specific media type for this id. This is used to distinguish between the different
@@ -31,6 +46,6 @@ namespace MediaBrowser.Model.Providers
/// <summary>
/// Gets or sets the URL format string.
/// </summary>
- public string? UrlFormatString { get; set; }
+ public string UrlFormatString { get; set; }
}
}
diff --git a/MediaBrowser.Model/Serialization/IJsonSerializer.cs b/MediaBrowser.Model/Serialization/IJsonSerializer.cs
deleted file mode 100644
index 09b6ff9b5..000000000
--- a/MediaBrowser.Model/Serialization/IJsonSerializer.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Model.Serialization
-{
- public interface IJsonSerializer
- {
- /// <summary>
- /// Serializes to stream.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="stream">The stream.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- void SerializeToStream(object obj, Stream stream);
-
- /// <summary>
- /// Serializes to stream.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="stream">The stream.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- void SerializeToStream<T>(T obj, Stream stream);
-
- /// <summary>
- /// Serializes to file.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <param name="file">The file.</param>
- /// <exception cref="ArgumentNullException">obj</exception>
- void SerializeToFile(object obj, string file);
-
- /// <summary>
- /// Deserializes from file.
- /// </summary>
- /// <param name="type">The type.</param>
- /// <param name="file">The file.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">type</exception>
- object DeserializeFromFile(Type type, string file);
-
- /// <summary>
- /// Deserializes from file.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="file">The file.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">file</exception>
- T DeserializeFromFile<T>(string file)
- where T : class;
-
- /// <summary>
- /// Deserializes from stream.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="stream">The stream.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">stream</exception>
- T DeserializeFromStream<T>(Stream stream);
-
- /// <summary>
- /// Deserializes from string.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="text">The text.</param>
- /// <returns>``0.</returns>
- /// <exception cref="ArgumentNullException">text</exception>
- T DeserializeFromString<T>(string text);
-
- /// <summary>
- /// Deserializes from stream.
- /// </summary>
- /// <param name="stream">The stream.</param>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">stream</exception>
- object DeserializeFromStream(Stream stream, Type type);
-
- /// <summary>
- /// Deserializes from string.
- /// </summary>
- /// <param name="json">The json.</param>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="ArgumentNullException">json</exception>
- object DeserializeFromString(string json, Type type);
-
- /// <summary>
- /// Serializes to string.
- /// </summary>
- /// <param name="obj">The obj.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">obj</exception>
- string SerializeToString(object obj);
-
- Task<object> DeserializeFromStreamAsync(Stream stream, Type type);
- Task<T> DeserializeFromStreamAsync<T>(Stream stream);
- }
-}
diff --git a/MediaBrowser.Model/Session/SessionMessageType.cs b/MediaBrowser.Model/Session/SessionMessageType.cs
index 23c41026d..84f4716b4 100644
--- a/MediaBrowser.Model/Session/SessionMessageType.cs
+++ b/MediaBrowser.Model/Session/SessionMessageType.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Session
Play,
SyncPlayCommand,
SyncPlayGroupUpdate,
- PlayState,
+ Playstate,
RestartRequired,
ServerShuttingDown,
ServerRestarting,
diff --git a/MediaBrowser.Model/Updates/InstallationInfo.cs b/MediaBrowser.Model/Updates/InstallationInfo.cs
index a6d80dba6..eebe1a903 100644
--- a/MediaBrowser.Model/Updates/InstallationInfo.cs
+++ b/MediaBrowser.Model/Updates/InstallationInfo.cs
@@ -1,5 +1,6 @@
#nullable disable
using System;
+using System.Text.Json.Serialization;
namespace MediaBrowser.Model.Updates
{
@@ -9,10 +10,11 @@ namespace MediaBrowser.Model.Updates
public class InstallationInfo
{
/// <summary>
- /// Gets or sets the guid.
+ /// Gets or sets the Id.
/// </summary>
- /// <value>The guid.</value>
- public Guid Guid { get; set; }
+ /// <value>The Id.</value>
+ [JsonPropertyName("Guid")]
+ public Guid Id { get; set; }
/// <summary>
/// Gets or sets the name.
diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs
index 5e9304363..7a82685f0 100644
--- a/MediaBrowser.Model/Updates/PackageInfo.cs
+++ b/MediaBrowser.Model/Updates/PackageInfo.cs
@@ -1,6 +1,7 @@
-#nullable disable
+#nullable enable
using System;
using System.Collections.Generic;
+using System.Text.Json.Serialization;
namespace MediaBrowser.Model.Updates
{
@@ -10,54 +11,75 @@ namespace MediaBrowser.Model.Updates
public class PackageInfo
{
/// <summary>
+ /// Initializes a new instance of the <see cref="PackageInfo"/> class.
+ /// </summary>
+ public PackageInfo()
+ {
+ Versions = Array.Empty<VersionInfo>();
+ Id = string.Empty;
+ Category = string.Empty;
+ Name = string.Empty;
+ Overview = string.Empty;
+ Owner = string.Empty;
+ Description = string.Empty;
+ }
+
+ /// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
- public string name { get; set; }
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
/// <summary>
/// Gets or sets a long description of the plugin containing features or helpful explanations.
/// </summary>
/// <value>The description.</value>
- public string description { get; set; }
+ [JsonPropertyName("description")]
+ public string Description { get; set; }
/// <summary>
/// Gets or sets a short overview of what the plugin does.
/// </summary>
/// <value>The overview.</value>
- public string overview { get; set; }
+ [JsonPropertyName("overview")]
+ public string Overview { get; set; }
/// <summary>
/// Gets or sets the owner.
/// </summary>
/// <value>The owner.</value>
- public string owner { get; set; }
+ [JsonPropertyName("owner")]
+ public string Owner { get; set; }
/// <summary>
/// Gets or sets the category.
/// </summary>
/// <value>The category.</value>
- public string category { get; set; }
+ [JsonPropertyName("category")]
+ public string Category { get; set; }
/// <summary>
- /// The guid of the assembly associated with this plugin.
+ /// Gets or sets the guid of the assembly associated with this plugin.
/// This is used to identify the proper item for automatic updates.
/// </summary>
/// <value>The name.</value>
- public string guid { get; set; }
+ [JsonPropertyName("guid")]
+ public string Id { get; set; }
/// <summary>
/// Gets or sets the versions.
/// </summary>
/// <value>The versions.</value>
- public IList<VersionInfo> versions { get; set; }
+ [JsonPropertyName("versions")]
+#pragma warning disable CA2227 // Collection properties should be read only
+ public IList<VersionInfo> Versions { get; set; }
+#pragma warning restore CA2227 // Collection properties should be read only
/// <summary>
- /// Initializes a new instance of the <see cref="PackageInfo"/> class.
+ /// Gets or sets the image url for the package.
/// </summary>
- public PackageInfo()
- {
- versions = Array.Empty<VersionInfo>();
- }
+ [JsonPropertyName("imageUrl")]
+ public string? ImageUrl { get; set; }
}
}
diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs
index 844170999..209092265 100644
--- a/MediaBrowser.Model/Updates/VersionInfo.cs
+++ b/MediaBrowser.Model/Updates/VersionInfo.cs
@@ -1,76 +1,79 @@
-#nullable disable
+#nullable enable
-using System;
+using System.Text.Json.Serialization;
+using SysVersion = System.Version;
namespace MediaBrowser.Model.Updates
{
/// <summary>
- /// Class PackageVersionInfo.
+ /// Defines the <see cref="VersionInfo"/> class.
/// </summary>
public class VersionInfo
{
- private Version _version;
+ private SysVersion? _version;
/// <summary>
/// Gets or sets the version.
/// </summary>
/// <value>The version.</value>
- public string version
+ [JsonPropertyName("version")]
+ public string Version
{
- get
- {
- return _version == null ? string.Empty : _version.ToString();
- }
+ get => _version == null ? string.Empty : _version.ToString();
- set
- {
- _version = Version.Parse(value);
- }
+ set => _version = SysVersion.Parse(value);
}
/// <summary>
- /// Gets the version as a <see cref="Version"/>.
+ /// Gets the version as a <see cref="SysVersion"/>.
/// </summary>
- public Version VersionNumber => _version;
+ public SysVersion VersionNumber => _version ?? new SysVersion(0, 0, 0);
/// <summary>
/// Gets or sets the changelog for this version.
/// </summary>
/// <value>The changelog.</value>
- public string changelog { get; set; }
+ [JsonPropertyName("changelog")]
+ public string? Changelog { get; set; }
/// <summary>
/// Gets or sets the ABI that this version was built against.
/// </summary>
/// <value>The target ABI version.</value>
- public string targetAbi { get; set; }
+ [JsonPropertyName("targetAbi")]
+ public string? TargetAbi { get; set; }
/// <summary>
/// Gets or sets the source URL.
/// </summary>
/// <value>The source URL.</value>
- public string sourceUrl { get; set; }
+ [JsonPropertyName("sourceUrl")]
+ public string? SourceUrl { get; set; }
/// <summary>
/// Gets or sets a checksum for the binary.
/// </summary>
/// <value>The checksum.</value>
- public string checksum { get; set; }
+ [JsonPropertyName("checksum")]
+ public string? Checksum { get; set; }
/// <summary>
/// Gets or sets a timestamp of when the binary was built.
/// </summary>
/// <value>The timestamp.</value>
- public string timestamp { get; set; }
+ [JsonPropertyName("timestamp")]
+ public string? Timestamp { get; set; }
/// <summary>
/// Gets or sets the repository name.
/// </summary>
- public string repositoryName { get; set; }
+ [JsonPropertyName("repositoryName")]
+ public string RepositoryName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the repository url.
/// </summary>
- public string repositoryUrl { get; set; }
+ [JsonPropertyName("repositoryUrl")]
+ public string RepositoryUrl { get; set; } = string.Empty;
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index a20c47cf2..913f14d9b 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -960,13 +960,11 @@ namespace MediaBrowser.Providers.Manager
public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item)
{
return GetExternalIds(item)
- .Select(i => new ExternalIdInfo
- {
- Name = i.ProviderName,
- Key = i.Key,
- Type = i.Type,
- UrlFormatString = i.UrlFormatString
- });
+ .Select(i => new ExternalIdInfo(
+ name: i.ProviderName,
+ key: i.Key,
+ type: i.Type,
+ urlFormatString: i.UrlFormatString));
}
/// <inheritdoc/>
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index fc8eb8c4e..071a149db 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -19,6 +19,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
+ <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.3" />
<PackageReference Include="TMDbLib" Version="1.7.3-alpha" />
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
index 293087da7..cd9e47743 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
@@ -1,9 +1,12 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.IO;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -19,13 +22,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IJsonSerializer _json;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
- public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IJsonSerializer json)
+ public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory)
{
_config = config;
_httpClientFactory = httpClientFactory;
- _json = json;
}
/// <inheritdoc />
@@ -56,7 +58,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbAlbumProvider.GetAlbumInfoPath(_config.ApplicationPaths, id);
- var obj = _json.DeserializeFromFile<AudioDbAlbumProvider.RootObject>(path);
+ await using FileStream jsonStream = File.OpenRead(path);
+ var obj = await JsonSerializer.DeserializeAsync<AudioDbAlbumProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.album != null && obj.album.Count > 0)
{
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
index 97bba10ba..f463a3566 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
@@ -6,10 +6,12 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
@@ -27,16 +29,15 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IJsonSerializer _json;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
public static AudioDbAlbumProvider Current;
- public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json)
+ public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory)
{
_config = config;
_fileSystem = fileSystem;
_httpClientFactory = httpClientFactory;
- _json = json;
Current = this;
}
@@ -64,7 +65,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetAlbumInfoPath(_config.ApplicationPaths, id);
- var obj = _json.DeserializeFromFile<RootObject>(path);
+ await using FileStream jsonStream = File.OpenRead(path);
+ var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.album != null && obj.album.Count > 0)
{
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
index d250acfa8..36700d191 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
@@ -1,9 +1,12 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.IO;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -19,12 +22,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IJsonSerializer _json;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
- public AudioDbArtistImageProvider(IServerConfigurationManager config, IJsonSerializer json, IHttpClientFactory httpClientFactory)
+ public AudioDbArtistImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory)
{
_config = config;
- _json = json;
_httpClientFactory = httpClientFactory;
}
@@ -58,7 +60,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id);
- var obj = _json.DeserializeFromFile<AudioDbArtistProvider.RootObject>(path);
+ await using FileStream jsonStream = File.OpenRead(path);
+ var obj = await JsonSerializer.DeserializeAsync<AudioDbArtistProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.artists != null && obj.artists.Count > 0)
{
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
index a2a03e1f9..7a15adb8e 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
@@ -5,10 +5,12 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
@@ -29,14 +31,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IJsonSerializer _json;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
- public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json)
+ public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory)
{
_config = config;
_fileSystem = fileSystem;
_httpClientFactory = httpClientFactory;
- _json = json;
Current = this;
}
@@ -65,7 +66,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetArtistInfoPath(_config.ApplicationPaths, id);
- var obj = _json.DeserializeFromFile<RootObject>(path);
+ await using FileStream jsonStream = File.OpenRead(path);
+ var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.artists != null && obj.artists.Count > 0)
{
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
index b5bd72ff0..ba0d7b569 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
index 90266e440..43bd3a472 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
index bfc840ea5..24ef80a35 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
@@ -12,13 +12,11 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
private readonly OmdbItemProvider _itemProvider;
private readonly IFileSystem _fileSystem;
@@ -26,19 +24,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb
private readonly IApplicationHost _appHost;
public OmdbEpisodeProvider(
- IJsonSerializer jsonSerializer,
IApplicationHost appHost,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IFileSystem fileSystem,
IServerConfigurationManager configurationManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
- _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClientFactory, libraryManager, fileSystem, configurationManager);
+ _itemProvider = new OmdbItemProvider(_appHost, httpClientFactory, libraryManager, fileSystem, configurationManager);
}
// After TheTvDb
@@ -69,7 +65,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
if (info.IndexNumber.HasValue && info.ParentIndexNumber.HasValue)
{
- result.HasMetadata = await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager)
+ result.HasMetadata = await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager)
.FetchEpisodeData(result, info.IndexNumber.Value, info.ParentIndexNumber.Value, info.GetProviderId(MetadataProvider.Imdb), seriesImdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
index 8f4240dc1..df67aff31 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
@@ -1,8 +1,8 @@
#pragma warning disable CS1591
using System.Collections.Generic;
-using System.Net.Http;
using System.Globalization;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
@@ -15,21 +15,18 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IApplicationHost _appHost;
- public OmdbImageProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
+ public OmdbImageProvider(IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
@@ -56,7 +53,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var list = new List<RemoteImageInfo>();
- var provider = new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager);
+ var provider = new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager);
if (!string.IsNullOrWhiteSpace(imdbId))
{
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 43d8af75f..97fcbfb6f 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -6,9 +6,12 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Common.Json.Converters;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -19,34 +22,35 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbItemProvider : IRemoteMetadataProvider<Series, SeriesInfo>,
IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IApplicationHost _appHost;
+ private readonly JsonSerializerOptions _jsonOptions;
public OmdbItemProvider(
- IJsonSerializer jsonSerializer,
IApplicationHost appHost,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IFileSystem fileSystem,
IServerConfigurationManager configurationManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
+
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions());
+ _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter());
+ _jsonOptions.Converters.Add(new JsonOmdbNotAvailableInt32Converter());
}
public string Name => "The Open Movie Database";
@@ -138,7 +142,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (isSearch)
{
- var searchResultList = await _jsonSerializer.DeserializeFromStreamAsync<SearchResultList>(stream).ConfigureAwait(false);
+ var searchResultList = await JsonSerializer.DeserializeAsync<SearchResultList>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (searchResultList != null && searchResultList.Search != null)
{
resultList.AddRange(searchResultList.Search);
@@ -146,7 +150,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
else
{
- var result = await _jsonSerializer.DeserializeFromStreamAsync<SearchResult>(stream).ConfigureAwait(false);
+ var result = await JsonSerializer.DeserializeAsync<SearchResult>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase))
{
resultList.Add(result);
@@ -221,7 +225,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
result.HasMetadata = true;
- await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
return result;
@@ -253,7 +257,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
result.HasMetadata = true;
- await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
return result;
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index e6c605072..3da999ad0 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -7,35 +7,41 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Common.Json.Converters;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbProvider
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IApplicationHost _appHost;
+ private readonly JsonSerializerOptions _jsonOptions;
- public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
+ public OmdbProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
+
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions());
+ _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter());
+ _jsonOptions.Converters.Add(new JsonOmdbNotAvailableInt32Converter());
}
public async Task Fetch<T>(MetadataResult<T> itemResult, string imdbId, string language, string country, CancellationToken cancellationToken)
@@ -208,39 +214,15 @@ namespace MediaBrowser.Providers.Plugins.Omdb
internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
{
var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false);
-
- string resultString;
-
- using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
- {
- using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
- {
- resultString = reader.ReadToEnd();
- resultString = resultString.Replace("\"N/A\"", "\"\"");
- }
- }
-
- var result = _jsonSerializer.DeserializeFromString<RootObject>(resultString);
- return result;
+ await using var stream = File.OpenRead(path);
+ return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken);
}
internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken)
{
var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false);
-
- string resultString;
-
- using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
- {
- using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
- {
- resultString = reader.ReadToEnd();
- resultString = resultString.Replace("\"N/A\"", "\"\"");
- }
- }
-
- var result = _jsonSerializer.DeserializeFromString<SeasonRootObject>(resultString);
- return result;
+ await using var stream = File.OpenRead(path);
+ return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken);
}
internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
@@ -297,11 +279,10 @@ namespace MediaBrowser.Providers.Plugins.Omdb
"i={0}&plot=short&tomatoes=true&r=json",
imdbParam));
- using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false);
+ var rootObject = await GetDeserializedOmdbResponse<RootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(rootObject, path);
+ await using FileStream jsonFileStream = File.OpenWrite(path);
+ await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
}
@@ -335,15 +316,22 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam,
seasonId));
- using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false);
+ var rootObject = await GetDeserializedOmdbResponse<SeasonRootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(path));
- _jsonSerializer.SerializeToFile(rootObject, path);
+ await using FileStream jsonFileStream = File.OpenWrite(path);
+ await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
}
+ public async Task<T> GetDeserializedOmdbResponse<T>(HttpClient httpClient, string url, CancellationToken cancellationToken)
+ {
+ using var response = await GetOmdbResponse(httpClient, url, cancellationToken).ConfigureAwait(false);
+ await using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+
+ return await JsonSerializer.DeserializeAsync<T>(content, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
+
public static Task<HttpResponseMessage> GetOmdbResponse(HttpClient httpClient, string url, CancellationToken cancellationToken)
{
return httpClient.GetAsync(url, cancellationToken);
@@ -465,7 +453,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public string seriesID { get; set; }
- public int Season { get; set; }
+ public int? Season { get; set; }
public int? totalSeasons { get; set; }
@@ -526,7 +514,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public string Response { get; set; }
- public int Episode { get; set; }
+ public int? Episode { get; set; }
public float? GetRottenTomatoScore()
{
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
index 41ca56164..d7f6781e5 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Providers/Properties/AssemblyInfo.cs b/MediaBrowser.Providers/Properties/AssemblyInfo.cs
index f1c46899c..fe4749c79 100644
--- a/MediaBrowser.Providers/Properties/AssemblyInfo.cs
+++ b/MediaBrowser.Providers/Properties/AssemblyInfo.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("Jellyfin.Common.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs
index 11b36285c..981b7b9d2 100644
--- a/MediaBrowser.XbmcMetadata/EntryPoint.cs
+++ b/MediaBrowser.XbmcMetadata/EntryPoint.cs
@@ -41,7 +41,7 @@ namespace MediaBrowser.XbmcMetadata
return Task.CompletedTask;
}
- private void OnUserDataSaved(object sender, UserDataSaveEventArgs e)
+ private void OnUserDataSaved(object? sender, UserDataSaveEventArgs e)
{
if (e.SaveReason == UserDataSaveReason.PlaybackFinished || e.SaveReason == UserDataSaveReason.TogglePlayed || e.SaveReason == UserDataSaveReason.UpdateUserRating)
{
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 87d1e9464..40f06c731 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -19,6 +19,7 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
</PropertyGroup>
<!-- Code Analyzers-->
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index b06464409..c287113c5 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -37,6 +37,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
Logger = logger;
_config = config;
ProviderManager = providerManager;
+ _validProviderIds = new Dictionary<string, string>();
}
protected CultureInfo UsCulture { get; } = new CultureInfo("en-US");
@@ -72,7 +73,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
throw new ArgumentException("The metadata file was empty or null.", nameof(metadataFile));
}
- _validProviderIds = _validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ _validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var idInfos = ProviderManager.GetExternalIdInfos(item.Item);
@@ -376,7 +377,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
return null;
- }).Where(i => i.HasValue).Select(i => i.Value).ToArray();
+ }).OfType<MetadataField>().ToArray();
}
break;
@@ -711,10 +712,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
default:
string readerName = reader.Name;
- if (_validProviderIds.TryGetValue(readerName, out string providerIdValue))
+ if (_validProviderIds.TryGetValue(readerName, out string? providerIdValue))
{
var id = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(id))
+ if (!string.IsNullOrWhiteSpace(providerIdValue) && !string.IsNullOrWhiteSpace(id))
{
item.SetProviderId(providerIdValue, id);
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
index b74a9fd8a..15a2fb63e 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
@@ -39,8 +39,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
case "id":
{
- string imdbId = reader.GetAttribute("IMDB");
- string tmdbId = reader.GetAttribute("TMDB");
+ string? imdbId = reader.GetAttribute("IMDB");
+ string? tmdbId = reader.GetAttribute("TMDB");
if (string.IsNullOrWhiteSpace(imdbId))
{
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
index f079d4a7e..74a724989 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
@@ -40,9 +40,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
case "id":
{
- string imdbId = reader.GetAttribute("IMDB");
- string tmdbId = reader.GetAttribute("TMDB");
- string tvdbId = reader.GetAttribute("TVDB");
+ string? imdbId = reader.GetAttribute("IMDB");
+ string? tmdbId = reader.GetAttribute("TMDB");
+ string? tvdbId = reader.GetAttribute("TVDB");
if (string.IsNullOrWhiteSpace(tvdbId))
{
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
index 6ad6c18a5..abd3e78d7 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
@@ -74,6 +74,6 @@ namespace MediaBrowser.XbmcMetadata.Providers
protected abstract void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken);
- protected abstract FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService);
+ protected abstract FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService);
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
index 2b1589d47..e7aa3ca07 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
@@ -50,7 +50,7 @@ namespace MediaBrowser.XbmcMetadata.Providers
}
/// <inheritdoc />
- protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
{
return MovieNfoSaver.GetMovieSavePaths(info)
.Select(directoryService.GetFile)
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index d8230d188..1adc5029d 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -200,7 +200,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
private void SaveToFile(Stream stream, string path)
{
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
+ Directory.CreateDirectory(directory);
// On Windows, savint the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
index dca796415..841121735 100644
--- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
@@ -41,7 +41,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
/// <inheritdoc />
protected override string GetLocalSavePath(BaseItem item)
- => GetMovieSavePaths(new ItemInfo(item)).FirstOrDefault();
+ => GetMovieSavePaths(new ItemInfo(item)).FirstOrDefault() ?? Path.ChangeExtension(item.Path, ".nfo");
internal static IEnumerable<string> GetMovieSavePaths(ItemInfo item)
{
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index 5a807372d..c654e8ef3 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30503.244
MinimumVisualStudioVersion = 10.0.40219.1
@@ -70,7 +70,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jell
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index b5e8e521c..45c93987b 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -13,9 +13,9 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.14.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
- <PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
+ <PackageReference Include="AutoFixture" Version="4.15.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" />
+ <PackageReference Include="AutoFixture.Xunit2" Version="4.15.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index af4684f56..19c5612c0 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -29,6 +29,7 @@
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+ <ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
new file mode 100644
index 000000000..efc0c4af9
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Globalization;
+using System.Text.Json;
+using MediaBrowser.Common.Json.Converters;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Json
+{
+ public class JsonNullableGuidConverterTests
+ {
+ private readonly JsonSerializerOptions _options;
+
+ public JsonNullableGuidConverterTests()
+ {
+ _options = new JsonSerializerOptions();
+ _options.Converters.Add(new JsonNullableGuidConverter());
+ }
+
+ [Fact]
+ public void Deserialize_Valid_Success()
+ {
+ Guid? value = JsonSerializer.Deserialize<Guid?>(@"""a852a27afe324084ae66db579ee3ee18""", _options);
+ Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value);
+ }
+
+ [Fact]
+ public void Deserialize_ValidDashed_Success()
+ {
+ Guid? value = JsonSerializer.Deserialize<Guid?>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", _options);
+ Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value);
+ }
+
+ [Fact]
+ public void Roundtrip_Valid_Success()
+ {
+ Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18");
+ string value = JsonSerializer.Serialize(guid, _options);
+ Assert.Equal(guid, JsonSerializer.Deserialize<Guid?>(value, _options));
+ }
+
+ [Fact]
+ public void Deserialize_Null_EmptyGuid()
+ {
+ Assert.Null(JsonSerializer.Deserialize<Guid?>("null", _options));
+ }
+
+ [Fact]
+ public void Serialize_EmptyGuid_EmptyGuid()
+ {
+ Assert.Equal("null", JsonSerializer.Serialize((Guid?)Guid.Empty, _options));
+ }
+
+ [Fact]
+ public void Serialize_Valid_NoDash_Success()
+ {
+ var guid = (Guid?)new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
+ var str = JsonSerializer.Serialize(guid, _options);
+ Assert.Equal($"\"{guid:N}\"", str);
+ }
+
+ [Fact]
+ public void Serialize_Nullable_Success()
+ {
+ Guid? guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
+ var str = JsonSerializer.Serialize(guid, _options);
+ Assert.Equal($"\"{guid:N}\"", str);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs
new file mode 100644
index 000000000..faed086a1
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs
@@ -0,0 +1,88 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MediaBrowser.Common.Json.Converters;
+using MediaBrowser.Providers.Plugins.Omdb;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Json
+{
+ public class JsonOmdbConverterTests
+ {
+ private readonly JsonSerializerOptions _options;
+
+ public JsonOmdbConverterTests()
+ {
+ _options = new JsonSerializerOptions();
+ _options.Converters.Add(new JsonOmdbNotAvailableStringConverter());
+ _options.Converters.Add(new JsonOmdbNotAvailableInt32Converter());
+ _options.NumberHandling = JsonNumberHandling.AllowReadingFromString;
+ }
+
+ [Fact]
+ public void Deserialize_Omdb_Response_Not_Available_Success()
+ {
+ const string Input = "{\"Title\":\"Chapter 1\",\"Year\":\"2013\",\"Rated\":\"TV-MA\",\"Released\":\"01 Feb 2013\",\"Season\":\"N/A\",\"Episode\":\"N/A\",\"Runtime\":\"55 min\",\"Genre\":\"Drama\",\"Director\":\"David Fincher\",\"Writer\":\"Michael Dobbs (based on the novels by), Andrew Davies (based on the mini-series by), Beau Willimon (created for television by), Beau Willimon, Sam Forman (staff writer)\",\"Actors\":\"Kevin Spacey, Robin Wright, Kate Mara, Corey Stoll\",\"Plot\":\"Congressman Francis Underwood has been declined the chair for Secretary of State. He's now gathering his own team to plot his revenge. Zoe Barnes, a reporter for the Washington Herald, will do anything to get her big break.\",\"Language\":\"English\",\"Country\":\"USA\",\"Awards\":\"N/A\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMTY5MTU4NDQzNV5BMl5BanBnXkFtZTgwMzk2ODcxMzE@._V1_SX300.jpg\",\"Ratings\":[{\"Source\":\"Internet Movie Database\",\"Value\":\"8.7/10\"}],\"Metascore\":\"N/A\",\"imdbRating\":\"8.7\",\"imdbVotes\":\"6736\",\"imdbID\":\"tt2161930\",\"seriesID\":\"N/A\",\"Type\":\"episode\",\"Response\":\"True\"}";
+ var seasonRootObject = JsonSerializer.Deserialize<OmdbProvider.RootObject>(Input, _options);
+ Assert.NotNull(seasonRootObject);
+ Assert.Null(seasonRootObject?.Awards);
+ Assert.Null(seasonRootObject?.Episode);
+ Assert.Null(seasonRootObject?.Metascore);
+ }
+
+ [Theory]
+ [InlineData("\"N/A\"")]
+ [InlineData("null")]
+ public void Deserialization_To_Nullable_Int_Shoud_Be_Null(string input)
+ {
+ var result = JsonSerializer.Deserialize<int?>(input, _options);
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("\"N/A\"")]
+ [InlineData("null")]
+ public void Deserialization_To_Nullable_String_Shoud_Be_Null(string input)
+ {
+ var result = JsonSerializer.Deserialize<string?>(input, _options);
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("\"8\"", 8)]
+ [InlineData("8", 8)]
+ public void Deserialize_Int_Success(string input, int expected)
+ {
+ var result = JsonSerializer.Deserialize<int>(input, _options);
+ Assert.Equal(result, expected);
+ }
+
+ [Fact]
+ public void Deserialize_Normal_String_Success()
+ {
+ const string Input = "\"Jellyfin\"";
+ const string Expected = "Jellyfin";
+ var result = JsonSerializer.Deserialize<string>(Input, _options);
+ Assert.Equal(Expected, result);
+ }
+
+ [Fact]
+ public void Roundtrip_Valid_Success()
+ {
+ const string Input = "{\"Title\":\"Chapter 1\",\"Year\":\"2013\",\"Rated\":\"TV-MA\",\"Released\":\"01 Feb 2013\",\"Season\":\"N/A\",\"Episode\":\"N/A\",\"Runtime\":\"55 min\",\"Genre\":\"Drama\",\"Director\":\"David Fincher\",\"Writer\":\"Michael Dobbs (based on the novels by), Andrew Davies (based on the mini-series by), Beau Willimon (created for television by), Beau Willimon, Sam Forman (staff writer)\",\"Actors\":\"Kevin Spacey, Robin Wright, Kate Mara, Corey Stoll\",\"Plot\":\"Congressman Francis Underwood has been declined the chair for Secretary of State. He's now gathering his own team to plot his revenge. Zoe Barnes, a reporter for the Washington Herald, will do anything to get her big break.\",\"Language\":\"English\",\"Country\":\"USA\",\"Awards\":\"N/A\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMTY5MTU4NDQzNV5BMl5BanBnXkFtZTgwMzk2ODcxMzE@._V1_SX300.jpg\",\"Ratings\":[{\"Source\":\"Internet Movie Database\",\"Value\":\"8.7/10\"}],\"Metascore\":\"N/A\",\"imdbRating\":\"8.7\",\"imdbVotes\":\"6736\",\"imdbID\":\"tt2161930\",\"seriesID\":\"N/A\",\"Type\":\"episode\",\"Response\":\"True\"}";
+ var trip1 = JsonSerializer.Deserialize<OmdbProvider.RootObject>(Input, _options);
+ Assert.NotNull(trip1);
+ Assert.NotNull(trip1?.Title);
+ Assert.Null(trip1?.Awards);
+ Assert.Null(trip1?.Episode);
+ Assert.Null(trip1?.Metascore);
+
+ var serializedTrip1 = JsonSerializer.Serialize(trip1!, _options);
+ var trip2 = JsonSerializer.Deserialize<OmdbProvider.RootObject>(serializedTrip1, _options);
+ Assert.NotNull(trip2);
+ Assert.NotNull(trip2?.Title);
+ Assert.Null(trip2?.Awards);
+ Assert.Null(trip2?.Episode);
+ Assert.Null(trip2?.Metascore);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index bcd12deaf..80259a55f 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -14,8 +14,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.14.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
+ <PackageReference Include="AutoFixture" Version="4.15.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Moq" Version="4.15.2" />
<PackageReference Include="xunit" Version="2.4.1" />