aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoshua M. Boniface <joshua@boniface.me>2020-12-31 18:47:05 -0500
committerJoshua M. Boniface <joshua@boniface.me>2020-12-31 18:49:53 -0500
commit1ad8e540353701555aeb483ccd4e9edad460e7d8 (patch)
treece51ff3860d69b7ee23e82e671a034aa3d4661e9
parent83dd3e22018d8b9065137c5444b3027f90ee0ae8 (diff)
Merge pull request #4709 from BaronGreenback/PluginDowngrade
(cherry picked from commit 406ae3e43a20216292c554151fa2d0e2a09edfa3) Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs251
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj1
-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/Tasks/PluginUpdateTask.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/Models/ConfigurationPageInfo.cs16
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs1
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs42
-rw-r--r--MediaBrowser.Common/Json/JsonDefaults.cs1
-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/Events/Updates/PluginUninstalledEventArgs.cs7
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs10
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs10
-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/Updates/InstallationInfo.cs8
-rw-r--r--MediaBrowser.Model/Updates/PackageInfo.cs50
-rw-r--r--MediaBrowser.Model/Updates/VersionInfo.cs47
-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/Plugin.cs2
-rw-r--r--MediaBrowser.sln4
35 files changed, 1973 insertions, 949 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 50ef71a46..a24f941d0 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -120,7 +120,9 @@ namespace Emby.Server.Implementations
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;
@@ -182,16 +184,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>
@@ -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,7 +566,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this);
-
+ ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
@@ -770,34 +811,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 +850,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 +862,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;
}
@@ -1031,129 +1031,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 +1281,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/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 9e9452f32..0c94f937c 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -74,5 +74,4 @@
<EmbeddedResource Include="Localization\Core\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.csv" />
</ItemGroup>
-
</Project>
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/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/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/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.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/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/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 1ec2a87c5..67c3dfe54 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -31,6 +31,7 @@ namespace MediaBrowser.Common.Json
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ PropertyNameCaseInsensitive = true,
Converters =
{
new JsonGuidConverter(),
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/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.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 9fb978e9b..0f0ad0f9a 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -456,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/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/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/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/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.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