mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-04-24 05:57:20 -04:00
Merge pull request #4709 from BaronGreenback/PluginDowngrade
(cherry picked from commit 406ae3e43a
)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
This commit is contained in:
parent
83dd3e2201
commit
1ad8e54035
35 changed files with 2017 additions and 993 deletions
|
@ -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
|
||||
|
|
|
@ -74,5 +74,4 @@
|
|||
<EmbeddedResource Include="Localization\Core\*.json" />
|
||||
<EmbeddedResource Include="Localization\Ratings\*.csv" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
688
Emby.Server.Implementations/Plugins/PluginManager.cs
Normal file
688
Emby.Server.Implementations/Plugins/PluginManager.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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--)
|
||||
{
|
||||
// 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)
|
||||
var version = package.Versions[i];
|
||||
|
||||
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
|
||||
// Update the manifests, if anything changes.
|
||||
if (plugin != null)
|
||||
{
|
||||
package.versions.RemoveAt(i);
|
||||
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(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
|
||||
{
|
||||
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
|
||||
_pluginManager.ImportPluginFrom(targetDir);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstalls a plugin.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin.</param>
|
||||
public void UninstallPlugin(IPlugin plugin)
|
||||
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!plugin.CanUninstall)
|
||||
// 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)
|
||||
{
|
||||
_logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
|
||||
return;
|
||||
plugin.Manifest.Timestamp = DateTime.UtcNow;
|
||||
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
|
||||
}
|
||||
|
||||
plugin.OnUninstalling();
|
||||
// Do the install
|
||||
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Remove it the quick way for now
|
||||
_applicationHost.RemovePlugin(plugin);
|
||||
// Do plugin-specific processing
|
||||
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CancelInstallation(Guid id)
|
||||
{
|
||||
lock (_currentInstallationsLock)
|
||||
{
|
||||
var install = _currentInstallations.Find(x => x.info.Guid == 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 tuple in _currentInstallations)
|
||||
{
|
||||
tuple.token.Dispose();
|
||||
}
|
||||
|
||||
_currentInstallations.Clear();
|
||||
}
|
||||
}
|
||||
return plugin != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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,112 +29,26 @@ 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of currently installed plugins.
|
||||
/// </summary>
|
||||
/// <response code="200">Installed plugins returned.</response>
|
||||
/// <returns>List of currently installed plugins.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
|
||||
{
|
||||
return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
|
||||
}
|
||||
|
||||
/// <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 file could not be found.</returns>
|
||||
[HttpDelete("{pluginId}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
|
||||
{
|
||||
var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
|
||||
if (plugin == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_installationManager.UninstallPlugin(plugin);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets plugin configuration.
|
||||
/// </summary>
|
||||
/// <param name="pluginId">Plugin id.</param>
|
||||
/// <response code="200">Plugin configuration returned.</response>
|
||||
/// <response code="404">Plugin not found or plugin configuration not found.</response>
|
||||
/// <returns>Plugin configuration.</returns>
|
||||
[HttpGet("{pluginId}/Configuration")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
|
||||
{
|
||||
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return plugin.Configuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates plugin configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Accepts plugin configuration as JSON body.
|
||||
/// </remarks>
|
||||
/// <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>
|
||||
[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))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (configuration != null)
|
||||
{
|
||||
plugin.UpdateConfiguration(configuration);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
_pluginManager = pluginManager;
|
||||
_serializerOptions = JsonDefaults.GetOptions();
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -139,7 +59,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[Obsolete("This endpoint should not be used.")]
|
||||
[HttpGet("SecurityInfo")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
|
||||
public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
|
||||
{
|
||||
return new PluginSecurityInfo
|
||||
{
|
||||
|
@ -148,21 +68,6 @@ namespace Jellyfin.Api.Controllers
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates plugin security info.
|
||||
/// </summary>
|
||||
/// <param name="pluginSecurityInfo">Plugin security info.</param>
|
||||
/// <response code="204">Plugin security info updated.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/>.</returns>
|
||||
[Obsolete("This endpoint should not be used.")]
|
||||
[HttpPost("SecurityInfo")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets registration status for a feature.
|
||||
/// </summary>
|
||||
|
@ -172,7 +77,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[Obsolete("This endpoint should not be used.")]
|
||||
[HttpPost("RegistrationRecords/{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
|
||||
public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
|
||||
{
|
||||
return new MBRegistrationRecord
|
||||
{
|
||||
|
@ -194,11 +99,253 @@ namespace Jellyfin.Api.Controllers
|
|||
[Obsolete("Paid plugins are not supported")]
|
||||
[HttpGet("Registrations/{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
|
||||
public ActionResult GetRegistration([FromRoute, Required] string name)
|
||||
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>
|
||||
/// Gets a list of currently installed plugins.
|
||||
/// </summary>
|
||||
/// <response code="200">Installed plugins returned.</response>
|
||||
/// <returns>List of currently installed plugins.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
|
||||
{
|
||||
return Ok(_pluginManager.Plugins
|
||||
.OrderBy(p => p.Name)
|
||||
.Select(p => p.GetPluginInfo()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 plugin could not be found.</returns>
|
||||
[HttpDelete("{pluginId}/{version}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
|
||||
{
|
||||
var plugin = _pluginManager.GetPlugin(pluginId, version);
|
||||
if (plugin == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_installationManager.UninstallPlugin(plugin);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <response code="200">Plugin configuration returned.</response>
|
||||
/// <response code="404">Plugin not found or plugin configuration not found.</response>
|
||||
/// <returns>Plugin configuration.</returns>
|
||||
[HttpGet("{pluginId}/Configuration")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
|
||||
{
|
||||
var plugin = _pluginManager.GetPlugin(pluginId);
|
||||
if (plugin?.Instance is IHasPluginConfiguration configPlugin)
|
||||
{
|
||||
return configPlugin.Configuration;
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates plugin configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Accepts plugin configuration as JSON body.
|
||||
/// </remarks>
|
||||
/// <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>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)
|
||||
{
|
||||
var plugin = _pluginManager.GetPlugin(pluginId);
|
||||
if (plugin?.Instance is not IHasPluginConfiguration configPlugin)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (configuration != null)
|
||||
{
|
||||
configPlugin.UpdateConfiguration(configuration);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a plugin's image.
|
||||
/// </summary>
|
||||
/// <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)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
[AllowAnonymous]
|
||||
public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
|
||||
{
|
||||
var plugin = _pluginManager.GetPlugin(pluginId, version);
|
||||
if (plugin == null)
|
||||
{
|
||||
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>
|
||||
/// Updates plugin security info.
|
||||
/// </summary>
|
||||
/// <param name="pluginSecurityInfo">Plugin security info.</param>
|
||||
/// <response code="204">Plugin security info updated.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/>.</returns>
|
||||
[Obsolete("This endpoint should not be used.")]
|
||||
[HttpPost("SecurityInfo")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -2,11 +2,16 @@ 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>
|
||||
|
@ -53,6 +58,11 @@ namespace MediaBrowser.Common
|
|||
/// <value>The application version.</value>
|
||||
Version ApplicationVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the service provider.
|
||||
/// </summary>
|
||||
IServiceProvider ServiceProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the application version.
|
||||
/// </summary>
|
||||
|
@ -71,12 +81,6 @@ namespace MediaBrowser.Common
|
|||
/// </summary>
|
||||
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>
|
||||
|
@ -101,6 +105,22 @@ namespace MediaBrowser.Common
|
|||
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
|
||||
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>
|
||||
|
@ -114,12 +134,6 @@ namespace MediaBrowser.Common
|
|||
/// <returns>A task.</returns>
|
||||
Task Shutdown();
|
||||
|
||||
/// <summary>
|
||||
/// Removes the plugin.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin.</param>
|
||||
void RemovePlugin(IPlugin plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this instance.
|
||||
/// </summary>
|
||||
|
|
|
@ -31,6 +31,7 @@ namespace MediaBrowser.Common.Json
|
|||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters =
|
||||
{
|
||||
new JsonGuidConverter(),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
208
MediaBrowser.Common/Plugins/BasePluginOfT.cs
Normal file
208
MediaBrowser.Common/Plugins/BasePluginOfT.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs
Normal file
27
MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
86
MediaBrowser.Common/Plugins/IPluginManager.cs
Normal file
86
MediaBrowser.Common/Plugins/IPluginManager.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
110
MediaBrowser.Common/Plugins/PluginManifest.cs
Normal file
110
MediaBrowser.Common/Plugins/PluginManifest.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
#nullable disable
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -6,35 +9,47 @@ namespace MediaBrowser.Model.Plugins
|
|||
/// </summary>
|
||||
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>
|
||||
/// <value>The image URL.</value>
|
||||
public string ImageUrl { get; set; }
|
||||
public bool HasImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the status of the plugin.
|
||||
/// </summary>
|
||||
public PluginStatus Status { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
47
MediaBrowser.Model/Plugins/PluginStatus.cs
Normal file
47
MediaBrowser.Model/Plugins/PluginStatus.cs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#nullable disable
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.Model.Updates
|
||||
{
|
||||
|
@ -9,55 +10,76 @@ namespace MediaBrowser.Model.Updates
|
|||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue