Merge pull request #4709 from BaronGreenback/PluginDowngrade

(cherry picked from commit 406ae3e43a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
pull/5296/head
Joshua M. Boniface 4 years ago
parent 83dd3e2201
commit 1ad8e54035

@ -120,7 +120,9 @@ namespace Emby.Server.Implementations
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions; private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder; private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
private string[] _urlPrefixes; private string[] _urlPrefixes;
@ -182,16 +184,6 @@ namespace Emby.Server.Implementations
protected IServiceCollection ServiceCollection { get; } 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> /// <summary>
/// Gets the logger factory. /// Gets the logger factory.
/// </summary> /// </summary>
@ -288,6 +280,13 @@ namespace Emby.Server.Implementations
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3); ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ServerConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
} }
/// <summary> /// <summary>
@ -387,16 +386,41 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns> /// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type) 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 try
{ {
_creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type); Logger.LogDebug("Creating instance of {Type}", type);
return ActivatorUtilities.CreateInstance(ServiceProvider, type); return ActivatorUtilities.CreateInstance(ServiceProvider, type);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error creating {Type}", type); Logger.LogError(ex, "Error creating {Type}", type);
// If this is a plugin fail it.
_pluginManager.FailPlugin(type.Assembly);
return null; return null;
} }
finally
{
_creatingInstances.Remove(type);
}
} }
/// <summary> /// <summary>
@ -406,11 +430,7 @@ namespace Emby.Server.Implementations
/// <returns>``0.</returns> /// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>(); public T Resolve<T>() => ServiceProvider.GetService<T>();
/// <summary> /// <inheritdoc/>
/// Gets the export types.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>IEnumerable{Type}.</returns>
public IEnumerable<Type> GetExportTypes<T>() public IEnumerable<Type> GetExportTypes<T>()
{ {
var currentType = typeof(T); var currentType = typeof(T);
@ -439,6 +459,27 @@ namespace Emby.Server.Implementations
return parts; 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> /// <summary>
/// Runs the startup tasks. /// Runs the startup tasks.
/// </summary> /// </summary>
@ -511,7 +552,7 @@ namespace Emby.Server.Implementations
RegisterServices(); RegisterServices();
RegisterPluginServices(); _pluginManager.RegisterServices(ServiceCollection);
} }
/// <summary> /// <summary>
@ -525,7 +566,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(ConfigurationManager); ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this); ServiceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>(); ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
@ -770,34 +811,7 @@ namespace Emby.Server.Implementations
} }
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
_plugins = GetExports<IPlugin>() _pluginManager.CreatePlugins();
.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);
}
}
_urlPrefixes = GetUrlPrefixes().ToArray(); _urlPrefixes = GetUrlPrefixes().ToArray();
@ -836,22 +850,6 @@ namespace Emby.Server.Implementations
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray(); _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) private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{ {
foreach (var ass in assemblies) foreach (var ass in assemblies)
@ -864,11 +862,13 @@ namespace Emby.Server.Implementations
catch (FileNotFoundException ex) catch (FileNotFoundException ex)
{ {
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
_pluginManager.FailPlugin(ass);
continue; continue;
} }
catch (TypeLoadException ex) catch (TypeLoadException ex)
{ {
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName); Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
_pluginManager.FailPlugin(ass);
continue; continue;
} }
@ -1031,129 +1031,15 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal(); 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> /// <summary>
/// Gets the composable part assemblies. /// Gets the composable part assemblies.
/// </summary> /// </summary>
/// <returns>IEnumerable{Assembly}.</returns> /// <returns>IEnumerable{Assembly}.</returns>
protected IEnumerable<Assembly> GetComposablePartAssemblies() protected IEnumerable<Assembly> GetComposablePartAssemblies()
{ {
if (Directory.Exists(ApplicationPaths.PluginsPath)) foreach (var p in _pluginManager.LoadAssemblies())
{ {
_pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList(); yield return p;
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;
}
}
} }
// Include composable parts in the Model assembly // 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() public IEnumerable<Assembly> GetApiPluginAssemblies()
{ {
var assemblies = _allConcreteTypes var assemblies = _allConcreteTypes

@ -74,5 +74,4 @@
<EmbeddedResource Include="Localization\Core\*.json" /> <EmbeddedResource Include="Localization\Core\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.csv" /> <EmbeddedResource Include="Localization\Ratings\*.csv" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -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;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks namespace Emby.Server.Implementations.ScheduledTasks
{ {

@ -1,4 +1,4 @@
#pragma warning disable CS1591 #nullable enable
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@ -40,17 +40,15 @@ namespace Emby.Server.Implementations.Updates
private readonly IEventManager _eventManager; private readonly IEventManager _eventManager;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonSerializerOptions; private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly IPluginManager _pluginManager;
/// <summary> /// <summary>
/// Gets the application host. /// Gets the application host.
/// </summary> /// </summary>
/// <value>The application host.</value> /// <value>The application host.</value>
private readonly IServerApplicationHost _applicationHost; private readonly IServerApplicationHost _applicationHost;
private readonly IZipClient _zipClient; private readonly IZipClient _zipClient;
private readonly object _currentInstallationsLock = new object(); private readonly object _currentInstallationsLock = new object();
/// <summary> /// <summary>
@ -63,6 +61,17 @@ namespace Emby.Server.Implementations.Updates
/// </summary> /// </summary>
private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal; 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( public InstallationManager(
ILogger<InstallationManager> logger, ILogger<InstallationManager> logger,
IServerApplicationHost appHost, IServerApplicationHost appHost,
@ -70,8 +79,8 @@ namespace Emby.Server.Implementations.Updates
IEventManager eventManager, IEventManager eventManager,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IServerConfigurationManager config, IServerConfigurationManager config,
IFileSystem fileSystem, IZipClient zipClient,
IZipClient zipClient) IPluginManager pluginManager)
{ {
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>(); _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>(); _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
@ -82,38 +91,65 @@ namespace Emby.Server.Implementations.Updates
_eventManager = eventManager; _eventManager = eventManager;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_config = config; _config = config;
_fileSystem = fileSystem;
_zipClient = zipClient; _zipClient = zipClient;
_jsonSerializerOptions = JsonDefaults.GetOptions(); _jsonSerializerOptions = JsonDefaults.GetOptions();
_pluginManager = pluginManager;
} }
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal; public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc /> /// <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 try
{ {
var packages = await _httpClientFactory.CreateClient(NamedClient.Default) List<PackageInfo>? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
if (packages == null) if (packages == null)
{ {
return Array.Empty<PackageInfo>(); 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. // Store the repository and repository url with each version, as they may be spread apart.
foreach (var entry in packages) foreach (var entry in packages)
{ {
foreach (var ver in entry.versions) for (int a = entry.Versions.Count - 1; a >= 0; a--)
{ {
ver.repositoryName = manifestName; var ver = entry.Versions[a];
ver.repositoryUrl = manifest; 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; return packages;
} }
catch (IOException ex)
{
_logger.LogError(ex, "Cannot locate the plugin manifest {Manifest}", manifest);
return Array.Empty<PackageInfo>();
}
catch (JsonException ex) catch (JsonException ex)
{ {
_logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest); _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 /> /// <inheritdoc />
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default) public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
{ {
var result = new List<PackageInfo>(); var result = new List<PackageInfo>();
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories) 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. // Where repositories have the same content, the details from the first is taken.
foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true)) 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. // Package doesn't have a valid GUID, skip.
continue; continue;
} }
for (var i = package.versions.Count - 1; i >= 0; i--) var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault();
// Remove invalid versions from the valid package.
for (var i = package.Versions.Count - 1; i >= 0; i--)
{ {
var version = package.Versions[i];
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
// Update the manifests, if anything changes.
if (plugin != null)
{
if (!string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal))
{
plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
}
}
// Remove versions with a target abi that is greater then the current application version. // Remove versions with a target abi that is greater then the current application version.
if (Version.TryParse(package.versions[i].targetAbi, out var targetAbi) if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
&& _applicationHost.ApplicationVersion < targetAbi)
{ {
package.versions.RemoveAt(i); package.Versions.RemoveAt(i);
} }
} }
// Don't add a package that doesn't have any compatible versions. // Don't add a package that doesn't have any compatible versions.
if (package.versions.Count == 0) if (package.Versions.Count == 0)
{ {
continue; continue;
} }
var existing = FilterPackages(result, package.name, packageGuid).FirstOrDefault();
if (existing != null) if (existing != null)
{ {
// Assumption is both lists are ordered, so slot these into the correct place. // Assumption is both lists are ordered, so slot these into the correct place.
MergeSort(existing.versions, package.versions); MergeSortedList(existing.Versions, package.Versions);
} }
else else
{ {
@ -225,23 +234,23 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<PackageInfo> FilterPackages( public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages, IEnumerable<PackageInfo> availablePackages,
string name = null, string? name = null,
Guid guid = default, Guid? id = default,
Version specificVersion = null) Version? specificVersion = null)
{ {
if (name != 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) 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; return availablePackages;
@ -250,12 +259,12 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<InstallationInfo> GetCompatibleVersions( public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages, IEnumerable<PackageInfo> availablePackages,
string name = null, string? name = null,
Guid guid = default, Guid? id = default,
Version minVersion = null, Version? minVersion = null,
Version specificVersion = 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 // Package not found in repository
if (package == null) if (package == null)
@ -264,8 +273,8 @@ namespace Emby.Server.Implementations.Updates
} }
var appVer = _applicationHost.ApplicationVersion; var appVer = _applicationHost.ApplicationVersion;
var availableVersions = package.versions var availableVersions = package.Versions
.Where(x => Version.Parse(x.targetAbi) <= appVer); .Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer);
if (specificVersion != null) if (specificVersion != null)
{ {
@ -280,12 +289,12 @@ namespace Emby.Server.Implementations.Updates
{ {
yield return new InstallationInfo yield return new InstallationInfo
{ {
Changelog = v.changelog, Changelog = v.Changelog,
Guid = new Guid(package.guid), Id = new Guid(package.Id),
Name = package.name, Name = package.Name,
Version = v.VersionNumber, Version = v.VersionNumber,
SourceUrl = v.sourceUrl, SourceUrl = v.SourceUrl,
Checksum = v.checksum Checksum = v.Checksum
}; };
} }
} }
@ -297,20 +306,6 @@ namespace Emby.Server.Implementations.Updates
return GetAvailablePluginUpdates(catalog); 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 /> /// <inheritdoc />
public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken) public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
{ {
@ -388,24 +383,140 @@ namespace Emby.Server.Implementations.Updates
} }
/// <summary> /// <summary>
/// Installs the package internal. /// Uninstalls a plugin.
/// </summary> /// </summary>
/// <param name="package">The package.</param> /// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
/// <param name="cancellationToken">The cancellation token.</param> public void UninstallPlugin(LocalPlugin plugin)
/// <returns><see cref="Task" />.</returns>
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{ {
// Set last update time if we were installed before if (plugin == null)
IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid) {
?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase)); return;
}
// Do the install if (plugin.Instance?.CanUninstall == false)
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false); {
_logger.LogWarning("Attempt to delete non removable plugin {PluginName}, ignoring request", plugin.Name);
return;
}
// Do plugin-specific processing plugin.Instance?.OnUninstalling();
_logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
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) private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
@ -450,7 +561,9 @@ namespace Emby.Server.Implementations.Updates
{ {
Directory.Delete(targetDir, true); Directory.Delete(targetDir, true);
} }
#pragma warning disable CA1031 // Do not catch general exception types
catch catch
#pragma warning restore CA1031 // Do not catch general exception types
{ {
// Ignore any exceptions. // Ignore any exceptions.
} }
@ -458,119 +571,27 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0; stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true); _zipClient.ExtractAllFromZip(stream, targetDir, true);
_pluginManager.ImportPluginFrom(targetDir);
#pragma warning restore CA5351
}
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
public void UninstallPlugin(IPlugin plugin)
{
if (!plugin.CanUninstall)
{
_logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
return;
}
plugin.OnUninstalling();
// Remove it the quick way for now
_applicationHost.RemovePlugin(plugin);
var path = plugin.AssemblyFilePath;
bool isDirectory = false;
// Check if we have a plugin directory we should remove too
if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath)
{
path = Path.GetDirectoryName(plugin.AssemblyFilePath);
isDirectory = true;
}
// Make this case-insensitive to account for possible incorrect assembly naming
var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
.FirstOrDefault(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(file))
{
path = file;
}
try
{
if (isDirectory)
{
_logger.LogInformation("Deleting plugin directory {0}", path);
Directory.Delete(path, true);
}
else
{
_logger.LogInformation("Deleting plugin file {0}", path);
_fileSystem.DeleteFile(path);
}
}
catch
{
// Ignore file errors.
}
var list = _config.Configuration.UninstalledPlugins.ToList();
var filename = Path.GetFileName(path);
if (!list.Contains(filename, StringComparer.OrdinalIgnoreCase))
{
list.Add(filename);
_config.Configuration.UninstalledPlugins = list.ToArray();
_config.SaveConfiguration();
}
_eventManager.Publish(new PluginUninstalledEventArgs(plugin));
_applicationHost.NotifyPendingRestart();
} }
/// <inheritdoc/> private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
public bool CancelInstallation(Guid id)
{ {
lock (_currentInstallationsLock) // Set last update time if we were installed before
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
if (plugin != null)
{ {
var install = _currentInstallations.Find(x => x.info.Guid == id); plugin.Manifest.Timestamp = DateTime.UtcNow;
if (install == default((InstallationInfo, CancellationTokenSource))) _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
{
return false;
}
install.token.Cancel();
_currentInstallations.Remove(install);
return true;
} }
}
/// <inheritdoc /> // Do the install
public void Dispose() await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary> // Do plugin-specific processing
/// Releases unmanaged and optionally managed resources. _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
/// </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 ILogger<DashboardController> _logger;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IPluginManager _pluginManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> class. /// Initializes a new instance of the <see cref="DashboardController"/> class.
/// </summary> /// </summary>
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param> /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
public DashboardController( public DashboardController(
ILogger<DashboardController> logger, ILogger<DashboardController> logger,
IServerApplicationHost appHost) IServerApplicationHost appHost,
IPluginManager pluginManager)
{ {
_logger = logger; _logger = logger;
_appHost = appHost; _appHost = appHost;
_pluginManager = pluginManager;
} }
/// <summary> /// <summary>
@ -83,7 +87,7 @@ namespace Jellyfin.Api.Controllers
.Where(i => i != null) .Where(i => i != null)
.ToList(); .ToList();
configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); configPages.AddRange(_pluginManager.Plugins.SelectMany(GetConfigPages));
if (pageType.HasValue) if (pageType.HasValue)
{ {
@ -155,24 +159,24 @@ namespace Jellyfin.Api.Controllers
return NotFound(); 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 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() 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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Updates; using MediaBrowser.Model.Updates;
@ -99,7 +100,7 @@ namespace Jellyfin.Api.Controllers
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
if (!string.IsNullOrEmpty(repositoryUrl)) 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(); .ToList();
} }

@ -1,15 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.PluginDtos; using Jellyfin.Api.Models.PluginDtos;
using MediaBrowser.Common; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json; using MediaBrowser.Common.Json;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -23,22 +29,81 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
public class PluginsController : BaseJellyfinApiController public class PluginsController : BaseJellyfinApiController
{ {
private readonly IApplicationHost _appHost;
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;
private readonly IPluginManager _pluginManager;
private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); private readonly IConfigurationManager _config;
private readonly JsonSerializerOptions _serializerOptions;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PluginsController"/> class. /// Initializes a new instance of the <see cref="PluginsController"/> class.
/// </summary> /// </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="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( public PluginsController(
IApplicationHost appHost, IInstallationManager installationManager,
IInstallationManager installationManager) IPluginManager pluginManager,
IConfigurationManager config)
{ {
_appHost = appHost;
_installationManager = installationManager; _installationManager = installationManager;
_pluginManager = pluginManager;
_serializerOptions = JsonDefaults.GetOptions();
_config = config;
}
/// <summary>
/// Get plugin security info.
/// </summary>
/// <response code="200">Plugin security info returned.</response>
/// <returns>Plugin security info.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpGet("SecurityInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
{
return new PluginSecurityInfo
{
IsMbSupporter = true,
SupporterKey = "IAmTotallyLegit"
};
}
/// <summary>
/// Gets registration status for a feature.
/// </summary>
/// <param name="name">Feature name.</param>
/// <response code="200">Registration status returned.</response>
/// <returns>Mb registration record.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpPost("RegistrationRecords/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
{
return new MBRegistrationRecord
{
IsRegistered = true,
RegChecked = true,
TrialVersion = false,
IsValid = true,
RegError = false
};
}
/// <summary>
/// Gets registration status for a feature.
/// </summary>
/// <param name="name">Feature name.</param>
/// <response code="501">Not implemented.</response>
/// <returns>Not Implemented.</returns>
/// <exception cref="NotImplementedException">This endpoint is not implemented.</exception>
[Obsolete("Paid plugins are not supported")]
[HttpGet("Registrations/{name}")]
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
public static ActionResult GetRegistration([FromRoute, Required] string name)
{
// TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
// delete all these registration endpoints. They are only kept for compatibility.
throw new NotImplementedException();
} }
/// <summary> /// <summary>
@ -50,23 +115,74 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<PluginInfo>> GetPlugins() public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
{ {
return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo())); return Ok(_pluginManager.Plugins
.OrderBy(p => p.Name)
.Select(p => p.GetPluginInfo()));
} }
/// <summary> /// <summary>
/// Uninstalls a plugin. /// Enables a disabled plugin.
/// </summary> /// </summary>
/// <param name="pluginId">Plugin id.</param> /// <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="204">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</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> /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpDelete("{pluginId}")] [HttpDelete("{pluginId}/{version}")]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId) public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{ {
var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId); var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null) if (plugin == null)
{ {
return NotFound(); return NotFound();
@ -76,6 +192,40 @@ namespace Jellyfin.Api.Controllers
return NoContent(); 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> /// <summary>
/// Gets plugin configuration. /// Gets plugin configuration.
/// </summary> /// </summary>
@ -88,12 +238,13 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId) public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
{ {
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) var plugin = _pluginManager.GetPlugin(pluginId);
if (plugin?.Instance is IHasPluginConfiguration configPlugin)
{ {
return NotFound(); return configPlugin.Configuration;
} }
return plugin.Configuration; return NotFound();
} }
/// <summary> /// <summary>
@ -105,47 +256,81 @@ namespace Jellyfin.Api.Controllers
/// <param name="pluginId">Plugin id.</param> /// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin configuration updated.</response> /// <response code="204">Plugin configuration updated.</response>
/// <response code="404">Plugin not found or plugin does not have configuration.</response> /// <response code="404">Plugin not found or plugin does not have configuration.</response>
/// <returns> /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</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")] [HttpPost("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId) public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
{ {
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) var plugin = _pluginManager.GetPlugin(pluginId);
if (plugin?.Instance is not IHasPluginConfiguration configPlugin)
{ {
return NotFound(); return NotFound();
} }
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions) var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
.ConfigureAwait(false); .ConfigureAwait(false);
if (configuration != null) if (configuration != null)
{ {
plugin.UpdateConfiguration(configuration); configPlugin.UpdateConfiguration(configuration);
} }
return NoContent(); return NoContent();
} }
/// <summary> /// <summary>
/// Get plugin security info. /// Gets a plugin's image.
/// </summary> /// </summary>
/// <response code="200">Plugin security info returned.</response> /// <param name="pluginId">Plugin id.</param>
/// <returns>Plugin security info.</returns> /// <param name="version">Plugin version.</param>
[Obsolete("This endpoint should not be used.")] /// <response code="200">Plugin image returned.</response>
[HttpGet("SecurityInfo")] /// <returns>Plugin's image.</returns>
[HttpGet("{pluginId}/{version}/Image")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo() [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
[AllowAnonymous]
public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{ {
return new PluginSecurityInfo var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null)
{ {
IsMbSupporter = true, return NotFound();
SupporterKey = "IAmTotallyLegit" }
};
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> /// <summary>
@ -162,43 +347,5 @@ namespace Jellyfin.Api.Controllers
{ {
return NoContent(); return NoContent();
} }
/// <summary>
/// Gets registration status for a feature.
/// </summary>
/// <param name="name">Feature name.</param>
/// <response code="200">Registration status returned.</response>
/// <returns>Mb registration record.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpPost("RegistrationRecords/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
{
return new MBRegistrationRecord
{
IsRegistered = true,
RegChecked = true,
TrialVersion = false,
IsValid = true,
RegError = false
};
}
/// <summary>
/// Gets registration status for a feature.
/// </summary>
/// <param name="name">Feature name.</param>
/// <response code="501">Not implemented.</response>
/// <returns>Not Implemented.</returns>
/// <exception cref="NotImplementedException">This endpoint is not implemented.</exception>
[Obsolete("Paid plugins are not supported")]
[HttpGet("Registrations/{name}")]
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
public ActionResult GetRegistration([FromRoute, Required] string name)
{
// TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
// delete all these registration endpoints. They are only kept for compatibility.
throw new NotImplementedException();
}
} }
} }

@ -1,4 +1,5 @@
using MediaBrowser.Common.Plugins; using System;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
@ -22,8 +23,7 @@ namespace Jellyfin.Api.Models
if (page.Plugin != null) if (page.Plugin != null)
{ {
DisplayName = page.Plugin.Name; DisplayName = page.Plugin.Name;
// Don't use "N" because it needs to match Plugin.Id PluginId = page.Plugin.Id;
PluginId = page.Plugin.Id.ToString();
} }
} }
@ -32,16 +32,14 @@ namespace Jellyfin.Api.Models
/// </summary> /// </summary>
/// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param> /// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param>
/// <param name="page">Instance of <see cref="PluginPageInfo"/> 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; Name = page.Name;
EnableInMainMenu = page.EnableInMainMenu; EnableInMainMenu = page.EnableInMainMenu;
MenuSection = page.MenuSection; MenuSection = page.MenuSection;
MenuIcon = page.MenuIcon; MenuIcon = page.MenuIcon;
DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin.Name : page.DisplayName; DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin?.Name : page.DisplayName;
PluginId = plugin?.Id;
// Don't use "N" because it needs to match Plugin.Id
PluginId = plugin.Id.ToString();
} }
/// <summary> /// <summary>
@ -80,6 +78,6 @@ namespace Jellyfin.Api.Models
/// Gets or sets the plugin id. /// Gets or sets the plugin id.
/// </summary> /// </summary>
/// <value>The plugin id.</value> /// <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.WriteIndented = jsonOptions.WriteIndented;
options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition; options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling; options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
options.JsonSerializerOptions.PropertyNameCaseInsensitive = jsonOptions.PropertyNameCaseInsensitive;
options.JsonSerializerOptions.Converters.Clear(); options.JsonSerializerOptions.Converters.Clear();
foreach (var converter in jsonOptions.Converters) foreach (var converter in jsonOptions.Converters)

@ -2,11 +2,16 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common 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> /// <summary>
/// An interface to be implemented by the applications hosting a kernel. /// An interface to be implemented by the applications hosting a kernel.
/// </summary> /// </summary>
@ -53,6 +58,11 @@ namespace MediaBrowser.Common
/// <value>The application version.</value> /// <value>The application version.</value>
Version ApplicationVersion { get; } Version ApplicationVersion { get; }
/// <summary>
/// Gets or sets the service provider.
/// </summary>
IServiceProvider ServiceProvider { get; set; }
/// <summary> /// <summary>
/// Gets the application version. /// Gets the application version.
/// </summary> /// </summary>
@ -71,12 +81,6 @@ namespace MediaBrowser.Common
/// </summary> /// </summary>
string ApplicationUserAgentAddress { get; } string ApplicationUserAgentAddress { get; }
/// <summary>
/// Gets the plugins.
/// </summary>
/// <value>The plugins.</value>
IReadOnlyList<IPlugin> Plugins { get; }
/// <summary> /// <summary>
/// Gets all plugin assemblies which implement a custom rest api. /// Gets all plugin assemblies which implement a custom rest api.
/// </summary> /// </summary>
@ -101,6 +105,22 @@ namespace MediaBrowser.Common
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns> /// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true); 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> /// <summary>
/// Resolves this instance. /// Resolves this instance.
/// </summary> /// </summary>
@ -114,12 +134,6 @@ namespace MediaBrowser.Common
/// <returns>A task.</returns> /// <returns>A task.</returns>
Task Shutdown(); Task Shutdown();
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
void RemovePlugin(IPlugin plugin);
/// <summary> /// <summary>
/// Initializes this instance. /// Initializes this instance.
/// </summary> /// </summary>

@ -31,6 +31,7 @@ namespace MediaBrowser.Common.Json
WriteIndented = false, WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString, NumberHandling = JsonNumberHandling.AllowReadingFromString,
PropertyNameCaseInsensitive = true,
Converters = Converters =
{ {
new JsonGuidConverter(), new JsonGuidConverter(),

@ -1,5 +1,3 @@
#pragma warning disable SA1402
using System; using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
@ -7,7 +5,6 @@ using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins namespace MediaBrowser.Common.Plugins
{ {
@ -64,14 +61,12 @@ namespace MediaBrowser.Common.Plugins
/// <returns>PluginInfo.</returns> /// <returns>PluginInfo.</returns>
public virtual PluginInfo GetPluginInfo() public virtual PluginInfo GetPluginInfo()
{ {
var info = new PluginInfo var info = new PluginInfo(
{ Name,
Name = Name, Version,
Version = Version.ToString(), Description,
Description = Description, Id,
Id = Id.ToString(), CanUninstall);
CanUninstall = CanUninstall
};
return info; return info;
} }
@ -97,207 +92,4 @@ namespace MediaBrowser.Common.Plugins
Id = assemblyId; 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;
}
}
} }

@ -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;
}
}
}
}

@ -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 System;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins namespace MediaBrowser.Common.Plugins
{ {
/// <summary> /// <summary>
/// Interface IPlugin. /// Defines the <see cref="IPlugin" />.
/// </summary> /// </summary>
public interface IPlugin public interface IPlugin
{ {
/// <summary> /// <summary>
/// Gets the name of the plugin. /// Gets the name of the plugin.
/// </summary> /// </summary>
/// <value>The name.</value>
string Name { get; } string Name { get; }
/// <summary> /// <summary>
/// Gets the description. /// Gets the Description.
/// </summary> /// </summary>
/// <value>The description.</value>
string Description { get; } string Description { get; }
/// <summary> /// <summary>
/// Gets the unique id. /// Gets the unique id.
/// </summary> /// </summary>
/// <value>The unique id.</value>
Guid Id { get; } Guid Id { get; }
/// <summary> /// <summary>
/// Gets the plugin version. /// Gets the plugin version.
/// </summary> /// </summary>
/// <value>The version.</value>
Version Version { get; } Version Version { get; }
/// <summary> /// <summary>
/// Gets the path to the assembly file. /// Gets the path to the assembly file.
/// </summary> /// </summary>
/// <value>The assembly file path.</value>
string AssemblyFilePath { get; } string AssemblyFilePath { get; }
/// <summary> /// <summary>
@ -49,11 +41,10 @@ namespace MediaBrowser.Common.Plugins
/// <summary> /// <summary>
/// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed. /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.
/// </summary> /// </summary>
/// <value>The data folder path.</value>
string DataFolderPath { get; } string DataFolderPath { get; }
/// <summary> /// <summary>
/// Gets the plugin info. /// Gets the <see cref="PluginInfo"/>.
/// </summary> /// </summary>
/// <returns>PluginInfo.</returns> /// <returns>PluginInfo.</returns>
PluginInfo GetPluginInfo(); PluginInfo GetPluginInfo();
@ -63,29 +54,4 @@ namespace MediaBrowser.Common.Plugins
/// </summary> /// </summary>
void OnUninstalling(); 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);
}
} }

@ -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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins namespace MediaBrowser.Common.Plugins
{ {
@ -9,36 +10,48 @@ namespace MediaBrowser.Common.Plugins
/// </summary> /// </summary>
public class LocalPlugin : IEquatable<LocalPlugin> public class LocalPlugin : IEquatable<LocalPlugin>
{ {
private readonly bool _supported;
private Version? _version;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class. /// Initializes a new instance of the <see cref="LocalPlugin"/> class.
/// </summary> /// </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> /// <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; Path = path;
DllFiles = new List<string>(); DllFiles = new List<string>();
_supported = isSupported;
Manifest = manifest;
} }
/// <summary> /// <summary>
/// Gets the plugin id. /// Gets the plugin id.
/// </summary> /// </summary>
public Guid Id { get; } public Guid Id => Manifest.Id;
/// <summary> /// <summary>
/// Gets the plugin name. /// Gets the plugin name.
/// </summary> /// </summary>
public string Name { get; } public string Name => Manifest.Name;
/// <summary> /// <summary>
/// Gets the plugin version. /// Gets the plugin version.
/// </summary> /// </summary>
public Version Version { get; } public Version Version
{
get
{
if (_version == null)
{
_version = Version.Parse(Manifest.Version);
}
return _version;
}
}
/// <summary> /// <summary>
/// Gets the plugin path. /// Gets the plugin path.
@ -51,26 +64,19 @@ namespace MediaBrowser.Common.Plugins
public List<string> DllFiles { get; } public List<string> DllFiles { get; }
/// <summary> /// <summary>
/// == operator. /// Gets or sets the instance of this plugin.
/// </summary> /// </summary>
/// <param name="left">Left item.</param> public IPlugin? Instance { get; set; }
/// <param name="right">Right item.</param>
/// <returns>Comparison result.</returns>
public static bool operator ==(LocalPlugin left, LocalPlugin right)
{
return left.Equals(right);
}
/// <summary> /// <summary>
/// != operator. /// Gets a value indicating whether Jellyfin supports this version of the plugin, and it's enabled.
/// </summary> /// </summary>
/// <param name="left">Left item.</param> public bool IsEnabledAndSupported => _supported && Manifest.Status >= PluginStatus.Active;
/// <param name="right">Right item.</param>
/// <returns>Comparison result.</returns> /// <summary>
public static bool operator !=(LocalPlugin left, LocalPlugin right) /// Gets a value indicating whether the plugin has a manifest.
{ /// </summary>
return !left.Equals(right); public PluginManifest Manifest { get; }
}
/// <summary> /// <summary>
/// Compare two <see cref="LocalPlugin"/>. /// Compare two <see cref="LocalPlugin"/>.
@ -80,10 +86,15 @@ namespace MediaBrowser.Common.Plugins
/// <returns>Comparison result.</returns> /// <returns>Comparison result.</returns>
public static int Compare(LocalPlugin a, LocalPlugin b) 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. // 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); compare = a.Id.CompareTo(b.Id);
} }
@ -91,8 +102,20 @@ namespace MediaBrowser.Common.Plugins
return compare == 0 ? a.Version.CompareTo(b.Version) : compare; 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 /> /// <inheritdoc />
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
return obj is LocalPlugin other && this.Equals(other); return obj is LocalPlugin other && this.Equals(other);
} }
@ -104,16 +127,14 @@ namespace MediaBrowser.Common.Plugins
} }
/// <inheritdoc /> /// <inheritdoc />
public bool Equals(LocalPlugin other) public bool Equals(LocalPlugin? other)
{ {
// Do not use == or != for comparison as this class overrides the operators. if (other == null)
if (object.ReferenceEquals(other, null))
{ {
return false; return false;
} }
return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && Id.Equals(other.Id) && Version.Equals(other.Version);
&& Id.Equals(other.Id);
} }
} }
} }

@ -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;
using System.Collections.Generic; using System.Collections.Generic;
@ -9,6 +9,9 @@ using MediaBrowser.Model.Updates;
namespace MediaBrowser.Common.Updates namespace MediaBrowser.Common.Updates
{ {
/// <summary>
/// Defines the <see cref="IInstallationManager" />.
/// </summary>
public interface IInstallationManager : IDisposable public interface IInstallationManager : IDisposable
{ {
/// <summary> /// <summary>
@ -21,12 +24,13 @@ namespace MediaBrowser.Common.Updates
/// </summary> /// </summary>
/// <param name="manifestName">Name of the repository.</param> /// <param name="manifestName">Name of the repository.</param>
/// <param name="manifest">The URL to query.</param> /// <param name="manifest">The URL to query.</param>
/// <param name="filterIncompatible">Filter out incompatible plugins.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns> /// <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> /// <summary>
/// Gets all available packages. /// Gets all available packages that are supported by this version.
/// </summary> /// </summary>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns> /// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
@ -37,33 +41,33 @@ namespace MediaBrowser.Common.Updates
/// </summary> /// </summary>
/// <param name="availablePackages">The available packages.</param> /// <param name="availablePackages">The available packages.</param>
/// <param name="name">The name of the plugin.</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> /// <param name="specificVersion">The version of the plugin.</param>
/// <returns>All plugins matching the requirements.</returns> /// <returns>All plugins matching the requirements.</returns>
IEnumerable<PackageInfo> FilterPackages( IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages, IEnumerable<PackageInfo> availablePackages,
string name = null, string? name = null,
Guid guid = default, Guid? id = default,
Version specificVersion = null); Version? specificVersion = null);
/// <summary> /// <summary>
/// Returns all compatible versions ordered from newest to oldest. /// Returns all compatible versions ordered from newest to oldest.
/// </summary> /// </summary>
/// <param name="availablePackages">The available packages.</param> /// <param name="availablePackages">The available packages.</param>
/// <param name="name">The name.</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="minVersion">The minimum required version of the plugin.</param>
/// <param name="specificVersion">The specific version of the plugin to install.</param> /// <param name="specificVersion">The specific version of the plugin to install.</param>
/// <returns>All compatible versions ordered from newest to oldest.</returns> /// <returns>All compatible versions ordered from newest to oldest.</returns>
IEnumerable<InstallationInfo> GetCompatibleVersions( IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages, IEnumerable<PackageInfo> availablePackages,
string name = null, string? name = null,
Guid guid = default, Guid? id = default,
Version minVersion = null, Version? minVersion = null,
Version specificVersion = null); Version? specificVersion = null);
/// <summary> /// <summary>
/// Returns the available plugin updates. /// Returns the available compatible plugin updates.
/// </summary> /// </summary>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The available plugin updates.</returns> /// <returns>The available plugin updates.</returns>
@ -81,7 +85,7 @@ namespace MediaBrowser.Common.Updates
/// Uninstalls a plugin. /// Uninstalls a plugin.
/// </summary> /// </summary>
/// <param name="plugin">The plugin.</param> /// <param name="plugin">The plugin.</param>
void UninstallPlugin(IPlugin plugin); void UninstallPlugin(LocalPlugin plugin);
/// <summary> /// <summary>
/// Cancels the installation. /// Cancels the installation.

@ -1,14 +1,21 @@
#pragma warning disable CS1591
using System; using System;
using MediaBrowser.Model.Updates; using MediaBrowser.Model.Updates;
namespace MediaBrowser.Common.Updates namespace MediaBrowser.Common.Updates
{ {
/// <summary>
/// Defines the <see cref="InstallationEventArgs" />.
/// </summary>
public class InstallationEventArgs : EventArgs public class InstallationEventArgs : EventArgs
{ {
/// <summary>
/// Gets or sets the <see cref="InstallationInfo"/>.
/// </summary>
public InstallationInfo InstallationInfo { get; set; } public InstallationInfo InstallationInfo { get; set; }
/// <summary>
/// Gets or sets the <see cref="VersionInfo"/>.
/// </summary>
public VersionInfo VersionInfo { get; set; } public VersionInfo VersionInfo { get; set; }
} }
} }

@ -1,18 +1,19 @@
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Controller.Events.Updates namespace MediaBrowser.Controller.Events.Updates
{ {
/// <summary> /// <summary>
/// An event that occurs when a plugin is uninstalled. /// An event that occurs when a plugin is uninstalled.
/// </summary> /// </summary>
public class PluginUninstalledEventArgs : GenericEventArgs<IPlugin> public class PluginUninstalledEventArgs : GenericEventArgs<PluginInfo>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PluginUninstalledEventArgs"/> class. /// Initializes a new instance of the <see cref="PluginUninstalledEventArgs"/> class.
/// </summary> /// </summary>
/// <param name="arg">The plugin.</param> /// <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; event EventHandler HasUpdateAvailableChanged;
IServiceProvider ServiceProvider { get; }
bool CoreStartupHasCompleted { get; } bool CoreStartupHasCompleted { get; }
bool CanLaunchWebBrowser { get; } bool CanLaunchWebBrowser { get; }
@ -122,13 +120,5 @@ namespace MediaBrowser.Controller
string ExpandVirtualPath(string path); string ExpandVirtualPath(string path);
string ReverseVirtualPath(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. /// Gets or sets the how many metadata refreshes can run concurrently.
/// </summary> /// </summary>
public int LibraryMetadataRefreshConcurrency { get; set; } 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 namespace MediaBrowser.Model.Plugins
{ {
/// <summary> /// <summary>
@ -6,35 +9,47 @@ namespace MediaBrowser.Model.Plugins
/// </summary> /// </summary>
public class PluginInfo 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> /// <summary>
/// Gets or sets the name. /// Gets or sets the name.
/// </summary> /// </summary>
/// <value>The name.</value>
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// Gets or sets the version. /// Gets or sets the version.
/// </summary> /// </summary>
/// <value>The version.</value> public Version Version { get; set; }
public string Version { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the configuration file. /// Gets or sets the name of the configuration file.
/// </summary> /// </summary>
/// <value>The name of the configuration file.</value> public string? ConfigurationFileName { get; set; }
public string ConfigurationFileName { get; set; }
/// <summary> /// <summary>
/// Gets or sets the description. /// Gets or sets the description.
/// </summary> /// </summary>
/// <value>The description.</value>
public string Description { get; set; } public string Description { get; set; }
/// <summary> /// <summary>
/// Gets or sets the unique id. /// Gets or sets the unique id.
/// </summary> /// </summary>
/// <value>The unique id.</value> public Guid Id { get; set; }
public string Id { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the plugin can be uninstalled. /// 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; } public bool CanUninstall { get; set; }
/// <summary> /// <summary>
/// Gets or sets the image URL. /// Gets or sets a value indicating whether this plugin has a valid image.
/// </summary>
public bool HasImage { get; set; }
/// <summary>
/// Gets or sets a value indicating the status of the plugin.
/// </summary> /// </summary>
/// <value>The image URL.</value> public PluginStatus Status { get; set; }
public string ImageUrl { get; set; }
} }
} }

@ -1,20 +1,40 @@
#nullable disable #nullable enable
#pragma warning disable CS1591
namespace MediaBrowser.Model.Plugins namespace MediaBrowser.Model.Plugins
{ {
/// <summary>
/// Defines the <see cref="PluginPageInfo" />.
/// </summary>
public class PluginPageInfo 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 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; }
} }
} }

@ -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 #nullable disable
using System; using System;
using System.Text.Json.Serialization;
namespace MediaBrowser.Model.Updates namespace MediaBrowser.Model.Updates
{ {
@ -9,10 +10,11 @@ namespace MediaBrowser.Model.Updates
public class InstallationInfo public class InstallationInfo
{ {
/// <summary> /// <summary>
/// Gets or sets the guid. /// Gets or sets the Id.
/// </summary> /// </summary>
/// <value>The guid.</value> /// <value>The Id.</value>
public Guid Guid { get; set; } [JsonPropertyName("Guid")]
public Guid Id { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name. /// Gets or sets the name.

@ -1,6 +1,7 @@
#nullable disable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MediaBrowser.Model.Updates namespace MediaBrowser.Model.Updates
{ {
@ -9,55 +10,76 @@ namespace MediaBrowser.Model.Updates
/// </summary> /// </summary>
public class PackageInfo 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> /// <summary>
/// Gets or sets the name. /// Gets or sets the name.
/// </summary> /// </summary>
/// <value>The name.</value> /// <value>The name.</value>
public string name { get; set; } [JsonPropertyName("name")]
public string Name { get; set; }
/// <summary> /// <summary>
/// Gets or sets a long description of the plugin containing features or helpful explanations. /// Gets or sets a long description of the plugin containing features or helpful explanations.
/// </summary> /// </summary>
/// <value>The description.</value> /// <value>The description.</value>
public string description { get; set; } [JsonPropertyName("description")]
public string Description { get; set; }
/// <summary> /// <summary>
/// Gets or sets a short overview of what the plugin does. /// Gets or sets a short overview of what the plugin does.
/// </summary> /// </summary>
/// <value>The overview.</value> /// <value>The overview.</value>
public string overview { get; set; } [JsonPropertyName("overview")]
public string Overview { get; set; }
/// <summary> /// <summary>
/// Gets or sets the owner. /// Gets or sets the owner.
/// </summary> /// </summary>
/// <value>The owner.</value> /// <value>The owner.</value>
public string owner { get; set; } [JsonPropertyName("owner")]
public string Owner { get; set; }
/// <summary> /// <summary>
/// Gets or sets the category. /// Gets or sets the category.
/// </summary> /// </summary>
/// <value>The category.</value> /// <value>The category.</value>
public string category { get; set; } [JsonPropertyName("category")]
public string Category { get; set; }
/// <summary> /// <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. /// This is used to identify the proper item for automatic updates.
/// </summary> /// </summary>
/// <value>The name.</value> /// <value>The name.</value>
public string guid { get; set; } [JsonPropertyName("guid")]
public string Id { get; set; }
/// <summary> /// <summary>
/// Gets or sets the versions. /// Gets or sets the versions.
/// </summary> /// </summary>
/// <value>The versions.</value> /// <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> /// <summary>
/// Initializes a new instance of the <see cref="PackageInfo"/> class. /// Gets or sets the image url for the package.
/// </summary> /// </summary>
public PackageInfo() [JsonPropertyName("imageUrl")]
{ public string? ImageUrl { get; set; }
versions = Array.Empty<VersionInfo>();
}
} }
} }

@ -1,76 +1,79 @@
#nullable disable #nullable enable
using System; using System.Text.Json.Serialization;
using SysVersion = System.Version;
namespace MediaBrowser.Model.Updates namespace MediaBrowser.Model.Updates
{ {
/// <summary> /// <summary>
/// Class PackageVersionInfo. /// Defines the <see cref="VersionInfo"/> class.
/// </summary> /// </summary>
public class VersionInfo public class VersionInfo
{ {
private Version _version; private SysVersion? _version;
/// <summary> /// <summary>
/// Gets or sets the version. /// Gets or sets the version.
/// </summary> /// </summary>
/// <value>The version.</value> /// <value>The version.</value>
public string version [JsonPropertyName("version")]
public string Version
{ {
get get => _version == null ? string.Empty : _version.ToString();
{
return _version == null ? string.Empty : _version.ToString();
}
set set => _version = SysVersion.Parse(value);
{
_version = Version.Parse(value);
}
} }
/// <summary> /// <summary>
/// Gets the version as a <see cref="Version"/>. /// Gets the version as a <see cref="SysVersion"/>.
/// </summary> /// </summary>
public Version VersionNumber => _version; public SysVersion VersionNumber => _version ?? new SysVersion(0, 0, 0);
/// <summary> /// <summary>
/// Gets or sets the changelog for this version. /// Gets or sets the changelog for this version.
/// </summary> /// </summary>
/// <value>The changelog.</value> /// <value>The changelog.</value>
public string changelog { get; set; } [JsonPropertyName("changelog")]
public string? Changelog { get; set; }
/// <summary> /// <summary>
/// Gets or sets the ABI that this version was built against. /// Gets or sets the ABI that this version was built against.
/// </summary> /// </summary>
/// <value>The target ABI version.</value> /// <value>The target ABI version.</value>
public string targetAbi { get; set; } [JsonPropertyName("targetAbi")]
public string? TargetAbi { get; set; }
/// <summary> /// <summary>
/// Gets or sets the source URL. /// Gets or sets the source URL.
/// </summary> /// </summary>
/// <value>The source URL.</value> /// <value>The source URL.</value>
public string sourceUrl { get; set; } [JsonPropertyName("sourceUrl")]
public string? SourceUrl { get; set; }
/// <summary> /// <summary>
/// Gets or sets a checksum for the binary. /// Gets or sets a checksum for the binary.
/// </summary> /// </summary>
/// <value>The checksum.</value> /// <value>The checksum.</value>
public string checksum { get; set; } [JsonPropertyName("checksum")]
public string? Checksum { get; set; }
/// <summary> /// <summary>
/// Gets or sets a timestamp of when the binary was built. /// Gets or sets a timestamp of when the binary was built.
/// </summary> /// </summary>
/// <value>The timestamp.</value> /// <value>The timestamp.</value>
public string timestamp { get; set; } [JsonPropertyName("timestamp")]
public string? Timestamp { get; set; }
/// <summary> /// <summary>
/// Gets or sets the repository name. /// Gets or sets the repository name.
/// </summary> /// </summary>
public string repositoryName { get; set; } [JsonPropertyName("repositoryName")]
public string RepositoryName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Gets or sets the repository url. /// Gets or sets the repository url.
/// </summary> /// </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;
using System.Collections.Generic; using System.Collections.Generic;

@ -1,4 +1,4 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;

@ -1,4 +1,4 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; 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 # Visual Studio Version 16
VisualStudioVersion = 16.0.30503.244 VisualStudioVersion = 16.0.30503.244
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
@ -70,7 +70,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jell
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
EndProject 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

Loading…
Cancel
Save