Initial upload

This commit is contained in:
Greenback 2020-12-06 23:48:54 +00:00
parent f2c2beca0f
commit 7986465cf7
34 changed files with 1726 additions and 755 deletions

View File

@ -34,7 +34,6 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
@ -119,7 +118,9 @@ namespace Emby.Server.Implementations
private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private string[] _urlPrefixes;
@ -181,16 +182,6 @@ namespace Emby.Server.Implementations
protected IServiceCollection ServiceCollection { get; }
private IPlugin[] _plugins;
private IReadOnlyList<LocalPlugin> _pluginsManifests;
/// <summary>
/// Gets the plugins.
/// </summary>
/// <value>The plugins.</value>
public IReadOnlyList<IPlugin> Plugins => _plugins;
/// <summary>
/// Gets the logger factory.
/// </summary>
@ -294,6 +285,14 @@ namespace Emby.Server.Implementations
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_pluginManager = new PluginManager(
LoggerFactory,
this,
ServerConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationPaths.CachePath,
ApplicationVersion);
}
/// <summary>
@ -393,8 +392,26 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type)
{
if (_creatingInstances == null)
{
_creatingInstances = new List<Type>();
}
if (_creatingInstances.IndexOf(type) != -1)
{
Logger.LogError("DI Loop detected.");
Logger.LogError("Attempted creation of {Type}", type.FullName);
foreach (var entry in _creatingInstances)
{
Logger.LogError("Called from: {stack}", entry.FullName);
}
throw new ExternalException("DI Loop detected.");
}
try
{
_creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type);
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
}
@ -403,6 +420,10 @@ namespace Emby.Server.Implementations
Logger.LogError(ex, "Error creating {Type}", type);
return null;
}
finally
{
_creatingInstances.Remove(type);
}
}
/// <summary>
@ -412,11 +433,7 @@ namespace Emby.Server.Implementations
/// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>();
/// <summary>
/// Gets the export types.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>IEnumerable{Type}.</returns>
/// <inheritdoc/>
public IEnumerable<Type> GetExportTypes<T>()
{
var currentType = typeof(T);
@ -445,6 +462,27 @@ namespace Emby.Server.Implementations
return parts;
}
/// <inheritdoc />
public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
{
// Convert to list so this isn't executed for each iteration
var parts = GetExportTypes<T>()
.Select(i => defaultFunc(i))
.Where(i => i != null)
.Cast<T>()
.ToList();
if (manageLifetime)
{
lock (_disposableParts)
{
_disposableParts.AddRange(parts.OfType<IDisposable>());
}
}
return parts;
}
/// <summary>
/// Runs the startup tasks.
/// </summary>
@ -509,7 +547,7 @@ namespace Emby.Server.Implementations
RegisterServices();
RegisterPluginServices();
_pluginManager.RegisterServices(ServiceCollection);
}
/// <summary>
@ -523,7 +561,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
@ -768,34 +806,7 @@ namespace Emby.Server.Implementations
}
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
_plugins = GetExports<IPlugin>()
.Where(i => i != null)
.ToArray();
if (Plugins != null)
{
foreach (var plugin in Plugins)
{
if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin)
{
// Ensure the version number matches the Plugin Manifest information.
foreach (var item in _pluginsManifests)
{
if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase))
{
// Update version number to that of the manifest.
assemblyPlugin.SetAttributes(
plugin.AssemblyFilePath,
Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)),
item.Version);
break;
}
}
}
Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
}
}
_pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray();
@ -834,22 +845,6 @@ namespace Emby.Server.Implementations
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
}
private void RegisterPluginServices()
{
foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
{
try
{
var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
instance.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
}
}
}
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{
foreach (var ass in assemblies)
@ -862,11 +857,13 @@ namespace Emby.Server.Implementations
catch (FileNotFoundException ex)
{
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
_pluginManager.FailPlugin(ass);
continue;
}
catch (TypeLoadException ex)
{
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
_pluginManager.FailPlugin(ass);
continue;
}
@ -1005,129 +1002,15 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
/// <inheritdoc/>
public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
{
var minimumVersion = new Version(0, 0, 0, 1);
var versions = new List<LocalPlugin>();
if (!Directory.Exists(path))
{
// Plugin path doesn't exist, don't try to enumerate subfolders.
return Enumerable.Empty<LocalPlugin>();
}
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
try
{
var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
targetAbi = minimumVersion;
}
if (!Version.TryParse(manifest.Version, out var version))
{
version = minimumVersion;
}
if (ApplicationVersion >= targetAbi)
{
// Only load Plugins if the plugin is built for this version or below.
versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
}
}
else
{
// No metafile, so lets see if the folder is versioned.
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion))
{
// Versioned folder.
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
}
else
{
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
}
}
}
catch
{
continue;
}
}
string lastName = string.Empty;
versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list.
// The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--)
{
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
{
versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
lastName = versions[x].Name;
continue;
}
if (!string.IsNullOrEmpty(lastName) && cleanup)
{
// Attempt a cleanup of old folders.
try
{
Logger.LogDebug("Deleting {Path}", versions[x].Path);
Directory.Delete(versions[x].Path, true);
}
catch (Exception e)
{
Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
}
versions.RemoveAt(x);
}
}
return versions;
}
/// <summary>
/// Gets the composable part assemblies.
/// </summary>
/// <returns>IEnumerable{Assembly}.</returns>
protected IEnumerable<Assembly> GetComposablePartAssemblies()
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
foreach (var p in _pluginManager.LoadAssemblies())
{
_pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList();
foreach (var plugin in _pluginsManifests)
{
foreach (var file in plugin.DllFiles)
{
Assembly plugAss;
try
{
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss;
}
}
yield return p;
}
// Include composable parts in the Model assembly
@ -1369,17 +1252,6 @@ namespace Emby.Server.Implementations
}
}
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
public void RemovePlugin(IPlugin plugin)
{
var list = _plugins.ToList();
list.Remove(plugin);
_plugins = list.ToArray();
}
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes

View File

@ -73,6 +73,12 @@
<EmbeddedResource Include="Localization\countries.json" />
<EmbeddedResource Include="Localization\Core\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.csv" />
<EmbeddedResource Include="Plugins\blank.png" />
<EmbeddedResource Include="Plugins\Superceded.png" />
<EmbeddedResource Include="Plugins\Disabled.png" />
<EmbeddedResource Include="Plugins\NotSupported.png" />
<EmbeddedResource Include="Plugins\Malfunction.png" />
<EmbeddedResource Include="Plugins\RestartRequired.png" />
<EmbeddedResource Include="Plugins\Active.png" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,674 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations
{
/// <summary>
/// Defines the <see cref="PluginManager" />.
/// </summary>
public class PluginManager : IPluginManager
{
private const int OffsetFromTopRightCorner = 38;
private readonly string _pluginsPath;
private readonly Version _appVersion;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger<PluginManager> _logger;
private readonly IApplicationHost _appHost;
private readonly string _imagesPath;
private readonly ServerConfiguration _config;
private readonly IList<LocalPlugin> _plugins;
private readonly Version _nextVersion;
private readonly Version _minimumVersion;
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
/// <param name="loggerfactory">The <see cref="ILoggerFactory"/>.</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="imagesPath">The image cache path.</param>
/// <param name="appVersion">The application version.</param>
public PluginManager(
ILoggerFactory loggerfactory,
IApplicationHost appHost,
ServerConfiguration config,
string pluginsPath,
string imagesPath,
Version appVersion)
{
_logger = loggerfactory.CreateLogger<PluginManager>();
_pluginsPath = pluginsPath;
_appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
_jsonOptions = JsonDefaults.GetOptions();
_jsonOptions.PropertyNameCaseInsensitive = true;
_jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
_config = config;
_appHost = appHost;
_imagesPath = imagesPath;
_nextVersion = new Version(_appVersion.Major, _appVersion.Minor + 2, _appVersion.Build, _appVersion.Revision);
_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()
{
foreach (var plugin in _plugins)
{
foreach (var file in plugin.DllFiles)
{
try
{
plugin.Assembly = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
_logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
ChangePluginState(plugin, PluginStatus.Malfunction);
continue;
}
_logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugin.Assembly.FullName, file);
yield return plugin.Assembly;
}
}
}
/// <summary>
/// Creates all the plugin instances.
/// </summary>
public void CreatePlugins()
{
var createdPlugins = _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 = GetPluginByType(pluginServiceRegistrator.Assembly.GetType());
if (plugin == null)
{
throw new NullReferenceException();
}
CheckIfStillSuperceded(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.Malfunction))
{
_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 (plugin == null || _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));
}
plugin.Instance?.OnUninstalling();
if (DeletePlugin(plugin))
{
return true;
}
// Unable to delete, so disable.
return ChangePluginState(plugin, PluginStatus.Disabled);
}
/// <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>
/// <param name="plugin">A <see cref="LocalPlugin"/> if found, otherwise null.</param>
/// <returns>Boolean value signifying the success of the search.</returns>
public bool TryGetPlugin(Guid id, Version? version, out LocalPlugin? plugin)
{
if (version == null)
{
// If no version is given, return the largest version number. (This is for backwards compatibility).
plugin = _plugins.Where(p => p.Id.Equals(id)).OrderByDescending(p => p.Version).FirstOrDefault();
}
else
{
plugin = _plugins.FirstOrDefault(p => p.Id.Equals(id) && p.Version.Equals(version));
}
return plugin != null;
}
/// <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))
{
UpdateSuccessors(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))
{
UpdateSuccessors(plugin);
}
}
/// <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 UpdateSuccessors(LocalPlugin plugin)
{
// This value is memory only - so that the web will show restart required.
plugin.Manifest.Status = PluginStatus.RestartRequired;
// Detect whether there is another version of this plugin that needs disabling.
var predecessor = _plugins.OrderByDescending(p => p.Version)
.FirstOrDefault(
p => p.Id.Equals(plugin.Id)
&& p.Name.Equals(plugin.Name, StringComparison.OrdinalIgnoreCase)
&& p.IsEnabledAndSupported
&& p.Version != plugin.Version);
if (predecessor == null)
{
return;
}
if (!ChangePluginState(predecessor, PluginStatus.Superceded))
{
_logger.LogError("Unable to disable version {Version} of {Name}", predecessor.Version, predecessor.Name);
}
}
/// <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.Where(p => assembly.Equals(p.Assembly)).FirstOrDefault();
if (plugin == null)
{
// A plugin's assembly didn't cause this issue, so ignore it.
return;
}
ChangePluginState(plugin, PluginStatus.Malfunction);
}
/// <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;
SaveManifest(plugin.Manifest, plugin.Path);
try
{
var data = JsonSerializer.Serialize(plugin.Manifest, _jsonOptions);
File.WriteAllText(Path.Combine(plugin.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 disable plugin {Path}", plugin.Path);
return false;
}
}
/// <summary>
/// Finds the plugin record using the type.
/// </summary>
/// <param name="type">The <see cref="Type"/> being sought.</param>
/// <returns>The matching record, or null if not found.</returns>
private LocalPlugin? GetPluginByType(Type type)
{
// Find which plugin it is by the path.
return _plugins.FirstOrDefault(p => string.Equals(p.Path, Path.GetDirectoryName(type.Assembly.Location), StringComparison.Ordinal));
}
/// <summary>
/// Creates the instance safe.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>System.Object.</returns>
private object? CreatePluginInstance(Type type)
{
// Find the record for this plugin.
var plugin = GetPluginByType(type);
if (plugin != null)
{
CheckIfStillSuperceded(plugin);
if (plugin.IsEnabledAndSupported == true)
{
_logger.LogInformation("Skipping disabled plugin {Version} of {Name} ", plugin.Version, plugin.Name);
return null;
}
}
try
{
_logger.LogDebug("Creating instance of {Type}", type);
var instance = ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
if (plugin == null)
{
// Create a dummy record for the providers.
var pInstance = (IPlugin)instance;
plugin = new LocalPlugin(
pInstance.AssemblyFilePath,
true,
new PluginManifest
{
Guid = pInstance.Id,
Status = PluginStatus.Active,
Name = pInstance.Name,
Version = pInstance.Version.ToString(),
MaxAbi = _nextVersion.ToString()
})
{
Instance = pInstance
};
_plugins.Add(plugin);
plugin.Manifest.Status = PluginStatus.Active;
}
else
{
plugin.Instance = (IPlugin)instance;
var manifest = plugin.Manifest;
var pluginStr = plugin.Instance.Version.ToString();
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;
}
manifest.Status = PluginStatus.Active;
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.Malfunction))
{
_logger.LogInformation("Plugin {Path} has been disabled.", plugin.Path);
return null;
}
}
_logger.LogDebug("Unable to auto-disable.");
return null;
}
}
private void CheckIfStillSuperceded(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
{
_logger.LogDebug("Deleting {Path}", plugin.Path);
Directory.Delete(plugin.Path, 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}", plugin.Path);
return false;
}
return _plugins.Remove(plugin);
}
private LocalPlugin? LoadManifest(string dir)
{
try
{
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.MaxAbi, out var maxAbi))
{
maxAbi = _appVersion;
}
if (!Version.TryParse(manifest.Version, out version))
{
manifest.Version = _minimumVersion.ToString();
}
return new LocalPlugin(dir, _appVersion >= targetAbi && _appVersion <= maxAbi, 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.
// NOTE: This Plugin is marked as valid for two upgrades, at which point, it can be assumed the
// code base will have changed sufficiently to make it invalid.
manifest = new PluginManifest
{
Status = PluginStatus.RestartRequired,
Name = metafile,
AutoUpdate = false,
Guid = metafile.GetMD5(),
TargetAbi = _appVersion.ToString(),
MaxAbi = _nextVersion.ToString(),
Version = version.ToString()
};
return new LocalPlugin(dir, true, manifest);
}
#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, "Something went wrong!");
return null;
}
}
/// <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);
LocalPlugin? entry;
foreach (var dir in directories)
{
entry = LoadManifest(dir);
if (entry != null)
{
versions.Add(entry);
}
}
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--)
{
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);
}
versions.RemoveAt(x);
}
if (!cleaned)
{
if (manifest == null)
{
_logger.LogWarning("Unable to disable plugin {Path}", entry.Path);
continue;
}
// Update the manifest so its not loaded next time.
manifest.Status = PluginStatus.Disabled;
SaveManifest(manifest, entry.Path);
}
}
// Only want plugin folders which have files.
return versions.Where(p => p.DllFiles.Count != 0);
}
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -8,10 +8,10 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks
{

View File

@ -1,4 +1,4 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.Collections.Concurrent;
@ -12,7 +12,6 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
@ -41,17 +40,15 @@ namespace Emby.Server.Implementations.Updates
private readonly IEventManager _eventManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly IPluginManager _pluginManager;
/// <summary>
/// Gets the application host.
/// </summary>
/// <value>The application host.</value>
private readonly IServerApplicationHost _applicationHost;
private readonly IZipClient _zipClient;
private readonly object _currentInstallationsLock = new object();
/// <summary>
@ -64,6 +61,17 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
/// <summary>
/// Initializes a new instance of the <see cref="InstallationManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{InstallationManager}"/>.</param>
/// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
/// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
/// <param name="eventManager">The <see cref="IEventManager"/>.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
/// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
/// <param name="zipClient">The <see cref="IZipClient"/>.</param>
/// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
public InstallationManager(
ILogger<InstallationManager> logger,
IServerApplicationHost appHost,
@ -71,8 +79,8 @@ namespace Emby.Server.Implementations.Updates
IEventManager eventManager,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
IFileSystem fileSystem,
IZipClient zipClient)
IZipClient zipClient,
IPluginManager pluginManager)
{
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
@ -83,16 +91,17 @@ namespace Emby.Server.Implementations.Updates
_eventManager = eventManager;
_httpClientFactory = httpClientFactory;
_config = config;
_fileSystem = fileSystem;
_zipClient = zipClient;
_jsonSerializerOptions = JsonDefaults.GetOptions();
_jsonSerializerOptions.PropertyNameCaseInsensitive = true;
_pluginManager = pluginManager;
}
/// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default)
public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
{
try
{
@ -103,13 +112,39 @@ namespace Emby.Server.Implementations.Updates
return Array.Empty<PackageInfo>();
}
var minimumVersion = new Version(0, 0, 0, 1);
// Store the repository and repository url with each version, as they may be spread apart.
foreach (var entry in packages)
{
foreach (var ver in entry.versions)
for (int a = entry.Versions.Count - 1; a >= 0; a--)
{
ver.repositoryName = manifestName;
ver.repositoryUrl = manifest;
var ver = entry.Versions[a];
ver.RepositoryName = manifestName;
ver.RepositoryUrl = manifest;
if (!filterIncompatible)
{
continue;
}
if (!Version.TryParse(ver.TargetAbi, out var targetAbi))
{
targetAbi = minimumVersion;
}
if (!Version.TryParse(ver.MaxAbi, out var maxAbi))
{
maxAbi = _applicationHost.ApplicationVersion;
}
// Only show plugins that fall between targetAbi and maxAbi
if (_applicationHost.ApplicationVersion >= targetAbi && _applicationHost.ApplicationVersion <= maxAbi)
{
continue;
}
// Not compatible with this version so remove it.
entry.Versions.Remove(ver);
}
}
@ -132,69 +167,61 @@ namespace Emby.Server.Implementations.Updates
}
}
private static void MergeSort(IList<VersionInfo> source, IList<VersionInfo> dest)
{
int sLength = source.Count - 1;
int dLength = dest.Count;
int s = 0, d = 0;
var sourceVersion = source[0].VersionNumber;
var destVersion = dest[0].VersionNumber;
while (d < dLength)
{
if (sourceVersion.CompareTo(destVersion) >= 0)
{
if (s < sLength)
{
sourceVersion = source[++s].VersionNumber;
}
else
{
// Append all of destination to the end of source.
while (d < dLength)
{
source.Add(dest[d++]);
}
break;
}
}
else
{
source.Insert(s++, dest[d++]);
if (d >= dLength)
{
break;
}
sLength++;
destVersion = dest[d].VersionNumber;
}
}
}
/// <inheritdoc />
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
{
var result = new List<PackageInfo>();
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
{
if (repository.Enabled)
if (repository.Enabled && repository.Url != null)
{
// Where repositories have the same content, the details of the first is taken.
foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true))
// Where repositories have the same content, the details from the first is taken.
foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
{
if (!Guid.TryParse(package.guid, out var packageGuid))
if (!Guid.TryParse(package.Guid, out var packageGuid))
{
// Package doesn't have a valid GUID, skip.
continue;
}
var existing = FilterPackages(result, package.name, packageGuid).FirstOrDefault();
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];
// Update the manifests, if anything changes.
if (_pluginManager.TryGetPlugin(packageGuid, version.VersionNumber, out LocalPlugin? plugin))
{
bool noChange = string.Equals(plugin!.Manifest.MaxAbi, version.MaxAbi, StringComparison.Ordinal)
|| string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal);
if (!noChange)
{
plugin.Manifest.MaxAbi = version.MaxAbi ?? string.Empty;
plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
}
}
// Remove versions with a target abi that is greater then the current application version.
if ((Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
|| (Version.TryParse(version.MaxAbi, out var maxAbi) && _applicationHost.ApplicationVersion > maxAbi))
{
package.Versions.RemoveAt(i);
}
}
// Don't add a package that doesn't have any compatible versions.
if (package.Versions.Count == 0)
{
continue;
}
if (existing != null)
{
// Assumption is both lists are ordered, so slot these into the correct place.
MergeSort(existing.versions, package.versions);
MergeSortedList(existing.Versions, package.Versions);
}
else
{
@ -210,23 +237,23 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc />
public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
Version specificVersion = null)
string? name = null,
Guid? guid = default,
Version? specificVersion = null)
{
if (name != null)
{
availablePackages = availablePackages.Where(x => x.name.Equals(name, StringComparison.OrdinalIgnoreCase));
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
if (guid != Guid.Empty)
{
availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid);
availablePackages = availablePackages.Where(x => Guid.Parse(x.Guid) == guid);
}
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;
@ -235,10 +262,10 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc />
public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
Version minVersion = null,
Version specificVersion = null)
string? name = null,
Guid? guid = default,
Version? minVersion = null,
Version? specificVersion = null)
{
var package = FilterPackages(availablePackages, name, guid, specificVersion).FirstOrDefault();
@ -249,8 +276,9 @@ namespace Emby.Server.Implementations.Updates
}
var appVer = _applicationHost.ApplicationVersion;
var availableVersions = package.versions
.Where(x => Version.Parse(x.targetAbi) <= appVer);
var availableVersions = package.Versions
.Where(x => (string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer)
&& (string.IsNullOrEmpty(x.MaxAbi) || Version.Parse(x.MaxAbi) >= appVer));
if (specificVersion != null)
{
@ -265,12 +293,12 @@ namespace Emby.Server.Implementations.Updates
{
yield return new InstallationInfo
{
Changelog = v.changelog,
Guid = new Guid(package.guid),
Name = package.name,
Changelog = v.Changelog,
Guid = new Guid(package.Guid),
Name = package.Name,
Version = v.VersionNumber,
SourceUrl = v.sourceUrl,
Checksum = v.checksum
SourceUrl = v.SourceUrl,
Checksum = v.Checksum
};
}
}
@ -282,20 +310,6 @@ namespace Emby.Server.Implementations.Updates
return GetAvailablePluginUpdates(catalog);
}
private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
{
var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
foreach (var plugin in plugins)
{
var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
{
yield return version;
}
}
}
/// <inheritdoc />
public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
{
@ -373,24 +387,140 @@ namespace Emby.Server.Implementations.Updates
}
/// <summary>
/// Installs the package internal.
/// Uninstalls a plugin.
/// </summary>
/// <param name="package">The package.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
/// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
public void UninstallPlugin(LocalPlugin plugin)
{
// Set last update time if we were installed before
IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase));
if (plugin == null)
{
return;
}
// Do the install
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
if (plugin.Instance?.CanUninstall == false)
{
_logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
return;
}
// Do plugin-specific processing
_logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
plugin.Instance?.OnUninstalling();
return plugin != null;
// Remove it the quick way for now
_pluginManager.RemovePlugin(plugin);
_eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo()));
_applicationHost.NotifyPendingRestart();
}
/// <inheritdoc/>
public bool CancelInstallation(Guid id)
{
lock (_currentInstallationsLock)
{
var install = _currentInstallations.Find(x => x.info.Guid == id);
if (install == default((InstallationInfo, CancellationTokenSource)))
{
return false;
}
install.token.Cancel();
_currentInstallations.Remove(install);
return true;
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources or <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
if (dispose)
{
lock (_currentInstallationsLock)
{
foreach (var (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.Guid != version.Guid))
{
yield return version;
}
}
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
@ -434,7 +564,9 @@ namespace Emby.Server.Implementations.Updates
{
Directory.Delete(targetDir, true);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
// Ignore any exceptions.
}
@ -442,119 +574,27 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
#pragma warning restore CA5351
_pluginManager.ImportPluginFrom(targetDir);
}
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
public void UninstallPlugin(IPlugin plugin)
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
if (!plugin.CanUninstall)
// Set last update time if we were installed before
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Guid) && p.Version.Equals(package.Version))
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
if (plugin != null)
{
_logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
return;
plugin.Manifest.Timestamp = DateTime.UtcNow;
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
}
plugin.OnUninstalling();
// Do the install
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
// Remove it the quick way for now
_applicationHost.RemovePlugin(plugin);
// Do plugin-specific processing
_logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
var path = plugin.AssemblyFilePath;
bool isDirectory = false;
// Check if we have a plugin directory we should remove too
if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath)
{
path = Path.GetDirectoryName(plugin.AssemblyFilePath);
isDirectory = true;
}
// Make this case-insensitive to account for possible incorrect assembly naming
var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
.FirstOrDefault(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(file))
{
path = file;
}
try
{
if (isDirectory)
{
_logger.LogInformation("Deleting plugin directory {0}", path);
Directory.Delete(path, true);
}
else
{
_logger.LogInformation("Deleting plugin file {0}", path);
_fileSystem.DeleteFile(path);
}
}
catch
{
// Ignore file errors.
}
var list = _config.Configuration.UninstalledPlugins.ToList();
var filename = Path.GetFileName(path);
if (!list.Contains(filename, StringComparer.OrdinalIgnoreCase))
{
list.Add(filename);
_config.Configuration.UninstalledPlugins = list.ToArray();
_config.SaveConfiguration();
}
_eventManager.Publish(new PluginUninstalledEventArgs(plugin));
_applicationHost.NotifyPendingRestart();
}
/// <inheritdoc/>
public bool CancelInstallation(Guid id)
{
lock (_currentInstallationsLock)
{
var install = _currentInstallations.Find(x => x.info.Guid == id);
if (install == default((InstallationInfo, CancellationTokenSource)))
{
return false;
}
install.token.Cancel();
_currentInstallations.Remove(install);
return true;
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources or <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
if (dispose)
{
lock (_currentInstallationsLock)
{
foreach (var tuple in _currentInstallations)
{
tuple.token.Dispose();
}
_currentInstallations.Clear();
}
}
return plugin != null;
}
}
}

View File

@ -29,18 +29,22 @@ namespace Jellyfin.Api.Controllers
{
private readonly ILogger<DashboardController> _logger;
private readonly IServerApplicationHost _appHost;
private readonly IPluginManager _pluginManager;
/// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> class.
/// </summary>
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
public DashboardController(
ILogger<DashboardController> logger,
IServerApplicationHost appHost)
IServerApplicationHost appHost,
IPluginManager pluginManager)
{
_logger = logger;
_appHost = appHost;
_pluginManager = pluginManager;
}
/// <summary>
@ -83,7 +87,7 @@ namespace Jellyfin.Api.Controllers
.Where(i => i != null)
.ToList();
configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
configPages.AddRange(_pluginManager.Plugins.SelectMany(GetConfigPages));
if (pageType.HasValue)
{
@ -155,24 +159,24 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
{
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
}
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin? plugin)
{
if (!(plugin is IHasWebPages hasWebPages))
if (plugin?.Instance is not IHasWebPages hasWebPages)
{
return new List<Tuple<PluginPageInfo, IPlugin>>();
}
return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
}
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
{
return _appHost.Plugins.SelectMany(GetPluginPages);
return _pluginManager.Plugins.SelectMany(GetPluginPages);
}
}
}

View File

@ -2,8 +2,11 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Updates;
@ -43,6 +46,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
[HttpGet("Packages/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(JsonDefaults.CamelCaseMediaType)]
public async Task<ActionResult<PackageInfo>> GetPackageInfo(
[FromRoute, Required] string name,
[FromQuery] Guid? assemblyGuid)
@ -69,6 +73,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
[HttpGet("Packages")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(JsonDefaults.CamelCaseMediaType)]
public async Task<IEnumerable<PackageInfo>> GetPackages()
{
IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
@ -99,7 +104,7 @@ namespace Jellyfin.Api.Controllers
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
if (!string.IsNullOrEmpty(repositoryUrl))
{
packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any())
packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)))
.ToList();
}
@ -143,6 +148,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns>
[HttpGet("Repositories")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(JsonDefaults.CamelCaseMediaType)]
public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories()
{
return _serverConfigurationManager.Configuration.PluginRepositories;

View File

@ -1,15 +1,22 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.PluginDtos;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -23,112 +30,26 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
public class PluginsController : BaseJellyfinApiController
{
private readonly IApplicationHost _appHost;
private readonly IInstallationManager _installationManager;
private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
private readonly IPluginManager _pluginManager;
private readonly IConfigurationManager _config;
private readonly JsonSerializerOptions _serializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="PluginsController"/> class.
/// </summary>
/// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param>
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
/// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
public PluginsController(
IApplicationHost appHost,
IInstallationManager installationManager)
IInstallationManager installationManager,
IPluginManager pluginManager,
IConfigurationManager config)
{
_appHost = appHost;
_installationManager = installationManager;
}
/// <summary>
/// Gets a list of currently installed plugins.
/// </summary>
/// <response code="200">Installed plugins returned.</response>
/// <returns>List of currently installed plugins.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
{
return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
}
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
[HttpDelete("{pluginId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
{
var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
if (plugin == null)
{
return NotFound();
}
_installationManager.UninstallPlugin(plugin);
return NoContent();
}
/// <summary>
/// Gets plugin configuration.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="200">Plugin configuration returned.</response>
/// <response code="404">Plugin not found or plugin configuration not found.</response>
/// <returns>Plugin configuration.</returns>
[HttpGet("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
return NotFound();
}
return plugin.Configuration;
}
/// <summary>
/// Updates plugin configuration.
/// </summary>
/// <remarks>
/// Accepts plugin configuration as JSON body.
/// </remarks>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin configuration updated.</response>
/// <response code="404">Plugin not found or plugin does not have configuration.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration.
/// The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/>
/// when plugin not found or plugin doesn't have configuration.
/// </returns>
[HttpPost("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
return NotFound();
}
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
.ConfigureAwait(false);
if (configuration != null)
{
plugin.UpdateConfiguration(configuration);
}
return NoContent();
_pluginManager = pluginManager;
_serializerOptions = JsonDefaults.GetOptions();
_config = config;
}
/// <summary>
@ -139,7 +60,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("This endpoint should not be used.")]
[HttpGet("SecurityInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
{
return new PluginSecurityInfo
{
@ -148,21 +69,6 @@ namespace Jellyfin.Api.Controllers
};
}
/// <summary>
/// Updates plugin security info.
/// </summary>
/// <param name="pluginSecurityInfo">Plugin security info.</param>
/// <response code="204">Plugin security info updated.</response>
/// <returns>An <see cref="NoContentResult"/>.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpPost("SecurityInfo")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
{
return NoContent();
}
/// <summary>
/// Gets registration status for a feature.
/// </summary>
@ -172,7 +78,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("This endpoint should not be used.")]
[HttpPost("RegistrationRecords/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
{
return new MBRegistrationRecord
{
@ -194,11 +100,257 @@ namespace Jellyfin.Api.Controllers
[Obsolete("Paid plugins are not supported")]
[HttpGet("Registrations/{name}")]
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
public ActionResult GetRegistration([FromRoute, Required] string name)
public static ActionResult GetRegistration([FromRoute, Required] string name)
{
// TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
// delete all these registration endpoints. They are only kept for compatibility.
throw new NotImplementedException();
}
/// <summary>
/// Gets a list of currently installed plugins.
/// </summary>
/// <response code="200">Installed plugins returned.</response>
/// <returns>List of currently installed plugins.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile(MediaTypeNames.Application.Json)]
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
{
return Ok(_pluginManager.Plugins
.OrderBy(p => p.Name)
.Select(p => p.GetPluginInfo()));
}
/// <summary>
/// Enables a disabled plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="204">Plugin enabled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
[HttpPost("{pluginId}/Enable")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute] Version? version)
{
if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin))
{
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 file could not be found.</returns>
[HttpPost("{pluginId}/Disable")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute] Version? version)
{
if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin))
{
return NotFound();
}
_pluginManager.DisablePlugin(plugin!);
return NoContent();
}
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="204">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
[HttpDelete("{pluginId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId, Version version)
{
if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin))
{
return NotFound();
}
_installationManager.UninstallPlugin(plugin!);
return NoContent();
}
/// <summary>
/// Gets plugin configuration.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="200">Plugin configuration returned.</response>
/// <response code="404">Plugin not found or plugin configuration not found.</response>
/// <returns>Plugin configuration.</returns>
[HttpGet("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesFile(MediaTypeNames.Application.Json)]
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId, [FromRoute] Version? version)
{
if (_pluginManager.TryGetPlugin(pluginId, version, out var plugin)
&& plugin!.Instance is IHasPluginConfiguration configPlugin)
{
return configPlugin.Configuration;
}
return NotFound();
}
/// <summary>
/// Updates plugin configuration.
/// </summary>
/// <remarks>
/// Accepts plugin configuration as JSON body.
/// </remarks>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="204">Plugin configuration updated.</response>
/// <response code="404">Plugin not found or plugin does not have configuration.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration.
/// The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/>
/// when plugin not found or plugin doesn't have configuration.
/// </returns>
[HttpPost("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId, [FromRoute] Version? version)
{
if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin)
|| plugin?.Instance is not IHasPluginConfiguration configPlugin)
{
return NotFound();
}
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
.ConfigureAwait(false);
if (configuration != null)
{
configPlugin.UpdateConfiguration(configuration);
}
return NoContent();
}
/// <summary>
/// Gets a plugin's image.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="200">Plugin image returned.</response>
/// <returns>Plugin's image.</returns>
[HttpGet("{pluginId}/Image")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
[AllowAnonymous]
public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute] Version? version)
{
if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin))
{
return NotFound();
}
var imgPath = Path.Combine(plugin!.Path, plugin!.Manifest.ImageUrl ?? string.Empty);
if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages
|| plugin!.Manifest.ImageUrl == null
|| !System.IO.File.Exists(imgPath))
{
// Use a blank image.
var type = GetType();
var stream = type.Assembly.GetManifestResourceStream(type.Namespace + ".Plugins.blank.png");
return File(stream, "image/png");
}
imgPath = Path.Combine(plugin.Path, plugin.Manifest.ImageUrl);
return PhysicalFile(imgPath, MimeTypes.GetMimeType(imgPath));
}
/// <summary>
/// Gets a plugin's status image.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="200">Plugin image returned.</response>
/// <returns>Plugin's image.</returns>
[HttpGet("{pluginId}/StatusImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
[AllowAnonymous]
public ActionResult GetPluginStatusImage([FromRoute, Required] Guid pluginId, [FromRoute] Version? version)
{
if (!_pluginManager.TryGetPlugin(pluginId, version, out var plugin))
{
return NotFound();
}
// Icons from http://www.fatcow.com/free-icons
var status = plugin!.Manifest.Status;
var type = _pluginManager.GetType();
var stream = type.Assembly.GetManifestResourceStream($"{type.Namespace}.Plugins.{status}.png");
return File(stream, "image/png");
}
/// <summary>
/// Gets a plugin's manifest.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="204">Plugin manifest returned.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to get the plugin's manifest.
/// The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/>
/// when plugin not found.
/// </returns>
[HttpPost("{pluginId}/Manifest")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesFile(MediaTypeNames.Application.Json)]
public ActionResult<PluginManifest> GetPluginManifest([FromRoute, Required] Guid pluginId, [FromRoute] Version? version)
{
if (_pluginManager.TryGetPlugin(pluginId, version, out var plugin))
{
return Ok(plugin!.Manifest);
}
return NotFound();
}
/// <summary>
/// Updates plugin security info.
/// </summary>
/// <param name="pluginSecurityInfo">Plugin security info.</param>
/// <response code="204">Plugin security info updated.</response>
/// <returns>An <see cref="NoContentResult"/>.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpPost("SecurityInfo")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
{
return NoContent();
}
}
}

View File

@ -1,4 +1,4 @@
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins;
@ -32,16 +32,16 @@ namespace Jellyfin.Api.Models
/// </summary>
/// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param>
/// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param>
public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page)
public ConfigurationPageInfo(IPlugin? plugin, PluginPageInfo page)
{
Name = page.Name;
EnableInMainMenu = page.EnableInMainMenu;
MenuSection = page.MenuSection;
MenuIcon = page.MenuIcon;
DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin.Name : page.DisplayName;
DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin?.Name ?? page.DisplayName : page.DisplayName;
// Don't use "N" because it needs to match Plugin.Id
PluginId = plugin.Id.ToString();
PluginId = plugin?.Id.ToString();
}
/// <summary>

View File

@ -2,11 +2,16 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using MediaBrowser.Common.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common
{
/// <summary>
/// Delegate used with GetExports{T}.
/// </summary>
/// <param name="type">Type to create.</param>
/// <returns>New instance of type <param>type</param>.</returns>
public delegate object CreationDelegate(Type type);
/// <summary>
/// An interface to be implemented by the applications hosting a kernel.
/// </summary>
@ -53,6 +58,11 @@ namespace MediaBrowser.Common
/// <value>The application version.</value>
Version ApplicationVersion { get; }
/// <summary>
/// Gets or sets the service provider.
/// </summary>
IServiceProvider ServiceProvider { get; set; }
/// <summary>
/// Gets the application version.
/// </summary>
@ -71,12 +81,6 @@ namespace MediaBrowser.Common
/// </summary>
string ApplicationUserAgentAddress { get; }
/// <summary>
/// Gets the plugins.
/// </summary>
/// <value>The plugins.</value>
IReadOnlyList<IPlugin> Plugins { get; }
/// <summary>
/// Gets all plugin assemblies which implement a custom rest api.
/// </summary>
@ -101,6 +105,22 @@ namespace MediaBrowser.Common
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true);
/// <summary>
/// Gets the exports.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="defaultFunc">Delegate function that gets called to create the object.</param>
/// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param>
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true);
/// <summary>
/// Gets the export types.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>IEnumerable{Type}.</returns>
IEnumerable<Type> GetExportTypes<T>();
/// <summary>
/// Resolves this instance.
/// </summary>
@ -114,12 +134,6 @@ namespace MediaBrowser.Common
/// <returns>A task.</returns>
Task Shutdown();
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
void RemovePlugin(IPlugin plugin);
/// <summary>
/// Initializes this instance.
/// </summary>

View File

@ -7,7 +7,6 @@ using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins
{
@ -64,14 +63,12 @@ namespace MediaBrowser.Common.Plugins
/// <returns>PluginInfo.</returns>
public virtual PluginInfo GetPluginInfo()
{
var info = new PluginInfo
{
Name = Name,
Version = Version.ToString(),
Description = Description,
Id = Id.ToString(),
CanUninstall = CanUninstall
};
var info = new PluginInfo(
Name,
Version,
Description,
Id,
CanUninstall);
return info;
}

View File

@ -0,0 +1,33 @@
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);
/// <summary>
/// Sets the startup directory creation function.
/// </summary>
/// <param name="directoryCreateFn">The directory function used to create the configuration folder.</param>
void SetStartupInfo(Action<string> directoryCreateFn);
}
}

View File

@ -1,44 +1,36 @@
#pragma warning disable CS1591
using System;
using MediaBrowser.Model.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Interface IPlugin.
/// Defines the <see cref="IPlugin" />.
/// </summary>
public interface IPlugin
{
/// <summary>
/// Gets the name of the plugin.
/// </summary>
/// <value>The name.</value>
string Name { get; }
/// <summary>
/// Gets the description.
/// Gets the Description.
/// </summary>
/// <value>The description.</value>
string Description { get; }
/// <summary>
/// Gets the unique id.
/// </summary>
/// <value>The unique id.</value>
Guid Id { get; }
/// <summary>
/// Gets the plugin version.
/// </summary>
/// <value>The version.</value>
Version Version { get; }
/// <summary>
/// Gets the path to the assembly file.
/// </summary>
/// <value>The assembly file path.</value>
string AssemblyFilePath { get; }
/// <summary>
@ -49,11 +41,10 @@ namespace MediaBrowser.Common.Plugins
/// <summary>
/// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.
/// </summary>
/// <value>The data folder path.</value>
string DataFolderPath { get; }
/// <summary>
/// Gets the plugin info.
/// Gets the <see cref="PluginInfo"/>.
/// </summary>
/// <returns>PluginInfo.</returns>
PluginInfo GetPluginInfo();
@ -63,29 +54,4 @@ namespace MediaBrowser.Common.Plugins
/// </summary>
void OnUninstalling();
}
public interface IHasPluginConfiguration
{
/// <summary>
/// Gets the type of configuration this plugin uses.
/// </summary>
/// <value>The type of the configuration.</value>
Type ConfigurationType { get; }
/// <summary>
/// Gets the plugin's configuration.
/// </summary>
/// <value>The configuration.</value>
BasePluginConfiguration Configuration { get; }
/// <summary>
/// Completely overwrites the current configuration with a new copy
/// Returns true or false indicating success or failure.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <exception cref="ArgumentNullException"><c>configuration</c> is <c>null</c>.</exception>
void UpdateConfiguration(BasePluginConfiguration configuration);
void SetStartupInfo(Action<string> directoryCreateFn);
}
}

View File

@ -0,0 +1,86 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Reflection;
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>
/// <param name="plugin">A <see cref="LocalPlugin"/> if found, otherwise null.</param>
/// <returns>Boolean value signifying the success of the search.</returns>
bool TryGetPlugin(Guid id, Version? version, out LocalPlugin? plugin);
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
/// <returns>Outcome of the operation.</returns>
bool RemovePlugin(LocalPlugin plugin);
}
}

View File

@ -1,6 +1,9 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
@ -9,36 +12,48 @@ namespace MediaBrowser.Common.Plugins
/// </summary>
public class LocalPlugin : IEquatable<LocalPlugin>
{
private readonly bool _supported;
private Version? _version;
/// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
/// </summary>
/// <param name="id">The plugin id.</param>
/// <param name="name">The plugin name.</param>
/// <param name="version">The plugin version.</param>
/// <param name="path">The plugin path.</param>
public LocalPlugin(Guid id, string name, Version version, string path)
/// <param name="isSupported"><b>True</b> if Jellyfin supports this version of the plugin.</param>
/// <param name="manifest">The manifest record for this plugin, or null if one does not exist.</param>
public LocalPlugin(string path, bool isSupported, PluginManifest manifest)
{
Id = id;
Name = name;
Version = version;
Path = path;
DllFiles = new List<string>();
_supported = isSupported;
Manifest = manifest;
}
/// <summary>
/// Gets the plugin id.
/// </summary>
public Guid Id { get; }
public Guid Id => Manifest.Guid;
/// <summary>
/// Gets the plugin name.
/// </summary>
public string Name { get; }
public string Name => Manifest.Name;
/// <summary>
/// Gets the plugin version.
/// </summary>
public Version Version { get; }
public Version Version
{
get
{
if (_version == null)
{
_version = Version.Parse(Manifest.Version);
}
return _version;
}
}
/// <summary>
/// Gets the plugin path.
@ -51,26 +66,24 @@ namespace MediaBrowser.Common.Plugins
public List<string> DllFiles { get; }
/// <summary>
/// == operator.
/// Gets or sets the instance of this plugin.
/// </summary>
/// <param name="left">Left item.</param>
/// <param name="right">Right item.</param>
/// <returns>Comparison result.</returns>
public static bool operator ==(LocalPlugin left, LocalPlugin right)
{
return left.Equals(right);
}
public IPlugin? Instance { get; set; }
/// <summary>
/// != operator.
/// Gets a value indicating whether Jellyfin supports this version of the plugin, and it's enabled.
/// </summary>
/// <param name="left">Left item.</param>
/// <param name="right">Right item.</param>
/// <returns>Comparison result.</returns>
public static bool operator !=(LocalPlugin left, LocalPlugin right)
{
return !left.Equals(right);
}
public bool IsEnabledAndSupported => _supported && Manifest.Status >= PluginStatus.Active;
/// <summary>
/// Gets a value indicating whether the plugin has a manifest.
/// </summary>
public PluginManifest Manifest { get; }
/// <summary>
/// Gets or sets a value indicating the assembly of the plugin.
/// </summary>
public Assembly? Assembly { get; set; }
/// <summary>
/// Compare two <see cref="LocalPlugin"/>.
@ -80,10 +93,15 @@ namespace MediaBrowser.Common.Plugins
/// <returns>Comparison result.</returns>
public static int Compare(LocalPlugin a, LocalPlugin b)
{
if (a == null || b == null)
{
throw new ArgumentNullException(a == null ? nameof(a) : nameof(b));
}
var compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
// Id is not equal but name is.
if (a.Id != b.Id && compare == 0)
if (!a.Id.Equals(b.Id) && compare == 0)
{
compare = a.Id.CompareTo(b.Id);
}
@ -91,8 +109,20 @@ namespace MediaBrowser.Common.Plugins
return compare == 0 ? a.Version.CompareTo(b.Version) : compare;
}
/// <summary>
/// Returns the plugin information.
/// </summary>
/// <returns>A <see cref="PluginInfo"/> instance containing the information.</returns>
public PluginInfo GetPluginInfo()
{
var inst = Instance?.GetPluginInfo() ?? new PluginInfo(Manifest.Name, Version, Manifest.Description, Manifest.Guid, true);
inst.Status = Manifest.Status;
inst.HasImage = !string.IsNullOrEmpty(Manifest.ImageUrl);
return inst;
}
/// <inheritdoc />
public override bool Equals(object obj)
public override bool Equals(object? obj)
{
return obj is LocalPlugin other && this.Equals(other);
}
@ -104,16 +134,14 @@ namespace MediaBrowser.Common.Plugins
}
/// <inheritdoc />
public bool Equals(LocalPlugin other)
public bool Equals(LocalPlugin? other)
{
// Do not use == or != for comparison as this class overrides the operators.
if (object.ReferenceEquals(other, null))
if (other == null)
{
return false;
}
return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase)
&& Id.Equals(other.Id);
return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && Id.Equals(other.Id) && Version.Equals(other.Version);
}
}
}

View File

@ -0,0 +1,85 @@
#nullable enable
using System;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.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; } = string.Empty;
/// <summary>
/// Gets or sets the changelog information.
/// </summary>
public string Changelog { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the description of the plugin.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the Global Unique Identifier for the plugin.
/// </summary>
#pragma warning disable CA1720 // Identifier contains type name
public Guid Guid { get; set; }
#pragma warning restore CA1720 // Identifier contains type name
/// <summary>
/// Gets or sets the Name of the plugin.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets an overview of the plugin.
/// </summary>
public string Overview { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the owner of the plugin.
/// </summary>
public string Owner { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the compatibility version for the plugin.
/// </summary>
public string TargetAbi { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the upper compatibility version for the plugin.
/// </summary>
public string MaxAbi { get; set; } = string.Empty;
/// <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; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether this plugin should be ignored.
/// </summary>
public PluginStatus Status { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this plugin should automatically update.
/// </summary>
public bool AutoUpdate { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether this plugin has an image.
/// Image must be located in the local plugin folder.
/// </summary>
public string? ImageUrl { get; set; }
}
}

View File

@ -1,4 +1,4 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.Collections.Generic;
@ -9,6 +9,9 @@ using MediaBrowser.Model.Updates;
namespace MediaBrowser.Common.Updates
{
/// <summary>
/// Defines the <see cref="IInstallationManager" />.
/// </summary>
public interface IInstallationManager : IDisposable
{
/// <summary>
@ -21,12 +24,13 @@ namespace MediaBrowser.Common.Updates
/// </summary>
/// <param name="manifestName">Name of the repository.</param>
/// <param name="manifest">The URL to query.</param>
/// <param name="filterIncompatible">Filter out incompatible plugins.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default);
Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all available packages.
/// Gets all available packages that are supported by this version.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
@ -42,9 +46,11 @@ namespace MediaBrowser.Common.Updates
/// <returns>All plugins matching the requirements.</returns>
IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
Version specificVersion = null);
string? name = null,
#pragma warning disable CA1720 // Identifier contains type name
Guid? guid = default,
#pragma warning restore CA1720 // Identifier contains type name
Version? specificVersion = null);
/// <summary>
/// Returns all compatible versions ordered from newest to oldest.
@ -57,13 +63,15 @@ namespace MediaBrowser.Common.Updates
/// <returns>All compatible versions ordered from newest to oldest.</returns>
IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
Version minVersion = null,
Version specificVersion = null);
string? name = null,
#pragma warning disable CA1720 // Identifier contains type name
Guid? guid = default,
#pragma warning restore CA1720 // Identifier contains type name
Version? minVersion = null,
Version? specificVersion = null);
/// <summary>
/// Returns the available plugin updates.
/// Returns the available compatible plugin updates.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The available plugin updates.</returns>
@ -81,7 +89,7 @@ namespace MediaBrowser.Common.Updates
/// Uninstalls a plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
void UninstallPlugin(IPlugin plugin);
void UninstallPlugin(LocalPlugin plugin);
/// <summary>
/// Cancels the installation.

View File

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

View File

@ -1,18 +1,19 @@
using Jellyfin.Data.Events;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Controller.Events.Updates
{
/// <summary>
/// An event that occurs when a plugin is uninstalled.
/// </summary>
public class PluginUninstalledEventArgs : GenericEventArgs<IPlugin>
public class PluginUninstalledEventArgs : GenericEventArgs<PluginInfo>
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginUninstalledEventArgs"/> class.
/// </summary>
/// <param name="arg">The plugin.</param>
public PluginUninstalledEventArgs(IPlugin arg) : base(arg)
public PluginUninstalledEventArgs(PluginInfo arg) : base(arg)
{
}
}

View File

@ -19,8 +19,6 @@ namespace MediaBrowser.Controller
{
event EventHandler HasUpdateAvailableChanged;
IServiceProvider ServiceProvider { get; }
bool CoreStartupHasCompleted { get; }
bool CanLaunchWebBrowser { get; }
@ -122,13 +120,5 @@ namespace MediaBrowser.Controller
string ExpandVirtualPath(string path);
string ReverseVirtualPath(string path);
/// <summary>
/// Gets the list of local plugins.
/// </summary>
/// <param name="path">Plugin base directory.</param>
/// <param name="cleanup">Cleanup old plugins.</param>
/// <returns>Enumerable of local plugins.</returns>
IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true);
}
}

View File

@ -449,5 +449,15 @@ namespace MediaBrowser.Model.Configuration
/// Gets or sets the how many metadata refreshes can run concurrently.
/// </summary>
public int LibraryMetadataRefreshConcurrency { get; set; }
/// <summary>
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
/// </summary>
public bool RemoveOldPlugins { get; set; }
/// <summary>
/// Gets or sets a value indicating whether plugin image should be disabled.
/// </summary>
public bool DisablePluginImages { get; set; }
}
}

View File

@ -1,4 +1,7 @@
#nullable disable
#nullable enable
using System;
namespace MediaBrowser.Model.Plugins
{
/// <summary>
@ -6,34 +9,46 @@ namespace MediaBrowser.Model.Plugins
/// </summary>
public class PluginInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginInfo"/> class.
/// </summary>
/// <param name="name">The plugin name.</param>
/// <param name="version">The plugin <see cref="Version"/>.</param>
/// <param name="description">The plugin description.</param>
/// <param name="id">The <see cref="Guid"/>.</param>
/// <param name="canUninstall">True if this plugin can be uninstalled.</param>
public PluginInfo(string name, Version version, string description, Guid id, bool canUninstall)
{
Name = name;
Version = version?.ToString() ?? throw new ArgumentNullException(nameof(version));
Description = description;
Id = id.ToString();
CanUninstall = canUninstall;
}
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the version.
/// </summary>
/// <value>The version.</value>
public string Version { get; set; }
/// <summary>
/// Gets or sets the name of the configuration file.
/// </summary>
/// <value>The name of the configuration file.</value>
public string ConfigurationFileName { get; set; }
public string? ConfigurationFileName { get; set; }
/// <summary>
/// Gets or sets the description.
/// </summary>
/// <value>The description.</value>
public string Description { get; set; }
/// <summary>
/// Gets or sets the unique id.
/// </summary>
/// <value>The unique id.</value>
public string Id { get; set; }
/// <summary>
@ -42,9 +57,13 @@ namespace MediaBrowser.Model.Plugins
public bool CanUninstall { get; set; }
/// <summary>
/// Gets or sets the image URL.
/// Gets or sets a value indicating whether this plugin has a valid image.
/// </summary>
/// <value>The image URL.</value>
public string ImageUrl { get; set; }
public bool HasImage { get; set; }
/// <summary>
/// Gets or sets a value indicating the status of the plugin.
/// </summary>
public PluginStatus Status { get; set; }
}
}

View File

@ -0,0 +1,17 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable SA1602 // Enumeration items should be documented
namespace MediaBrowser.Model.Plugins
{
/// <summary>
/// Plugin load status.
/// </summary>
public enum PluginStatus
{
RestartRequired = 1,
Active = 0,
Disabled = -1,
NotSupported = -2,
Malfunction = -3,
Superceded = -4
}
}

View File

@ -1,4 +1,4 @@
#nullable disable
#nullable enable
using System;
using System.Collections.Generic;
@ -9,55 +9,70 @@ namespace MediaBrowser.Model.Updates
/// </summary>
public class PackageInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="PackageInfo"/> class.
/// </summary>
public PackageInfo()
{
Versions = Array.Empty<VersionInfo>();
Guid = string.Empty;
Category = string.Empty;
Name = string.Empty;
Overview = string.Empty;
Owner = string.Empty;
Description = string.Empty;
}
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string name { get; set; }
public string Name { get; set; }
/// <summary>
/// Gets or sets a long description of the plugin containing features or helpful explanations.
/// </summary>
/// <value>The description.</value>
public string description { get; set; }
public string Description { get; set; }
/// <summary>
/// Gets or sets a short overview of what the plugin does.
/// </summary>
/// <value>The overview.</value>
public string overview { get; set; }
public string Overview { get; set; }
/// <summary>
/// Gets or sets the owner.
/// </summary>
/// <value>The owner.</value>
public string owner { get; set; }
public string Owner { get; set; }
/// <summary>
/// Gets or sets the category.
/// </summary>
/// <value>The category.</value>
public string category { get; set; }
public string Category { get; set; }
/// <summary>
/// The guid of the assembly associated with this plugin.
/// Gets or sets the guid of the assembly associated with this plugin.
/// This is used to identify the proper item for automatic updates.
/// </summary>
/// <value>The name.</value>
public string guid { get; set; }
#pragma warning disable CA1720 // Identifier contains type name
public string Guid { get; set; }
#pragma warning restore CA1720 // Identifier contains type name
/// <summary>
/// Gets or sets the versions.
/// </summary>
/// <value>The versions.</value>
public IList<VersionInfo> versions { get; set; }
#pragma warning disable CA2227 // Collection properties should be read only
public IList<VersionInfo> Versions { get; set; }
#pragma warning restore CA2227 // Collection properties should be read only
/// <summary>
/// Initializes a new instance of the <see cref="PackageInfo"/> class.
/// Gets or sets the image url for the package.
/// </summary>
public PackageInfo()
{
versions = Array.Empty<VersionInfo>();
}
public string? ImageUrl { get; set; }
}
}

View File

@ -1,6 +1,6 @@
#nullable disable
#nullable enable
using System;
using SysVersion = System.Version;
namespace MediaBrowser.Model.Updates
{
@ -9,68 +9,68 @@ namespace MediaBrowser.Model.Updates
/// </summary>
public class VersionInfo
{
private Version _version;
private SysVersion? _version;
/// <summary>
/// Gets or sets the version.
/// </summary>
/// <value>The version.</value>
public string version
public string Version
{
get
{
return _version == null ? string.Empty : _version.ToString();
}
get => _version == null ? string.Empty : _version.ToString();
set
{
_version = Version.Parse(value);
}
set => _version = SysVersion.Parse(value);
}
/// <summary>
/// Gets the version as a <see cref="Version"/>.
/// Gets the version as a <see cref="SysVersion"/>.
/// </summary>
public Version VersionNumber => _version;
public SysVersion VersionNumber => _version ?? new SysVersion(0, 0, 0);
/// <summary>
/// Gets or sets the changelog for this version.
/// </summary>
/// <value>The changelog.</value>
public string changelog { get; set; }
public string? Changelog { get; set; }
/// <summary>
/// Gets or sets the ABI that this version was built against.
/// </summary>
/// <value>The target ABI version.</value>
public string targetAbi { get; set; }
public string? TargetAbi { get; set; }
/// <summary>
/// Gets or sets the maximum ABI that this version will work with.
/// </summary>
/// <value>The target ABI version.</value>
public string? MaxAbi { get; set; }
/// <summary>
/// Gets or sets the source URL.
/// </summary>
/// <value>The source URL.</value>
public string sourceUrl { get; set; }
public string? SourceUrl { get; set; }
/// <summary>
/// Gets or sets a checksum for the binary.
/// </summary>
/// <value>The checksum.</value>
public string checksum { get; set; }
public string? Checksum { get; set; }
/// <summary>
/// Gets or sets a timestamp of when the binary was built.
/// </summary>
/// <value>The timestamp.</value>
public string timestamp { get; set; }
public string? Timestamp { get; set; }
/// <summary>
/// Gets or sets the repository name.
/// </summary>
public string repositoryName { get; set; }
public string RepositoryName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the repository url.
/// </summary>
public string repositoryUrl { get; set; }
public string RepositoryUrl { get; set; } = string.Empty;
}
}

View File

@ -1,4 +1,5 @@
Microsoft Visual Studio Solution File, Format Version 12.00

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30503.244
MinimumVisualStudioVersion = 10.0.40219.1
@ -70,7 +71,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jell
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution