mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-03 19:17:24 -05:00 
			
		
		
		
	Merge pull request #4709 from BaronGreenback/PluginDowngrade
This commit is contained in:
		
						commit
						406ae3e43a
					
				@ -120,7 +120,9 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
        private readonly IFileSystem _fileSystemManager;
 | 
					        private readonly IFileSystem _fileSystemManager;
 | 
				
			||||||
        private readonly IXmlSerializer _xmlSerializer;
 | 
					        private readonly IXmlSerializer _xmlSerializer;
 | 
				
			||||||
        private readonly IStartupOptions _startupOptions;
 | 
					        private readonly IStartupOptions _startupOptions;
 | 
				
			||||||
 | 
					        private readonly IPluginManager _pluginManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private List<Type> _creatingInstances;
 | 
				
			||||||
        private IMediaEncoder _mediaEncoder;
 | 
					        private IMediaEncoder _mediaEncoder;
 | 
				
			||||||
        private ISessionManager _sessionManager;
 | 
					        private ISessionManager _sessionManager;
 | 
				
			||||||
        private string[] _urlPrefixes;
 | 
					        private string[] _urlPrefixes;
 | 
				
			||||||
@ -183,16 +185,6 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected IServiceCollection ServiceCollection { get; }
 | 
					        protected IServiceCollection ServiceCollection { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private IPlugin[] _plugins;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private IReadOnlyList<LocalPlugin> _pluginsManifests;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the plugins.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The plugins.</value>
 | 
					 | 
				
			||||||
        public IReadOnlyList<IPlugin> Plugins => _plugins;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the logger factory.
 | 
					        /// Gets the logger factory.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
@ -288,6 +280,13 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
            ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
 | 
					            ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
 | 
				
			||||||
            ApplicationVersionString = ApplicationVersion.ToString(3);
 | 
					            ApplicationVersionString = ApplicationVersion.ToString(3);
 | 
				
			||||||
            ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
 | 
					            ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _pluginManager = new PluginManager(
 | 
				
			||||||
 | 
					                LoggerFactory.CreateLogger<PluginManager>(),
 | 
				
			||||||
 | 
					                this,
 | 
				
			||||||
 | 
					                ServerConfigurationManager.Configuration,
 | 
				
			||||||
 | 
					                ApplicationPaths.PluginsPath,
 | 
				
			||||||
 | 
					                ApplicationVersion);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -387,16 +386,41 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
        /// <returns>System.Object.</returns>
 | 
					        /// <returns>System.Object.</returns>
 | 
				
			||||||
        protected object CreateInstanceSafe(Type type)
 | 
					        protected object CreateInstanceSafe(Type type)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            if (_creatingInstances == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _creatingInstances = new List<Type>();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (_creatingInstances.IndexOf(type) != -1)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
 | 
				
			||||||
 | 
					                foreach (var entry in _creatingInstances)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Logger.LogError("Called from: {TypeName}", entry.FullName);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                _pluginManager.FailPlugin(type.Assembly);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                throw new ExternalException("DI Loop detected.");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 | 
					                _creatingInstances.Add(type);
 | 
				
			||||||
                Logger.LogDebug("Creating instance of {Type}", type);
 | 
					                Logger.LogDebug("Creating instance of {Type}", type);
 | 
				
			||||||
                return ActivatorUtilities.CreateInstance(ServiceProvider, type);
 | 
					                return ActivatorUtilities.CreateInstance(ServiceProvider, type);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            catch (Exception ex)
 | 
					            catch (Exception ex)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Logger.LogError(ex, "Error creating {Type}", type);
 | 
					                Logger.LogError(ex, "Error creating {Type}", type);
 | 
				
			||||||
 | 
					                // If this is a plugin fail it.
 | 
				
			||||||
 | 
					                _pluginManager.FailPlugin(type.Assembly);
 | 
				
			||||||
                return null;
 | 
					                return null;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            finally
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _creatingInstances.Remove(type);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -406,11 +430,7 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
        /// <returns>``0.</returns>
 | 
					        /// <returns>``0.</returns>
 | 
				
			||||||
        public T Resolve<T>() => ServiceProvider.GetService<T>();
 | 
					        public T Resolve<T>() => ServiceProvider.GetService<T>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <inheritdoc/>
 | 
				
			||||||
        /// Gets the export types.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <typeparam name="T">The type.</typeparam>
 | 
					 | 
				
			||||||
        /// <returns>IEnumerable{Type}.</returns>
 | 
					 | 
				
			||||||
        public IEnumerable<Type> GetExportTypes<T>()
 | 
					        public IEnumerable<Type> GetExportTypes<T>()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var currentType = typeof(T);
 | 
					            var currentType = typeof(T);
 | 
				
			||||||
@ -439,6 +459,27 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
            return parts;
 | 
					            return parts;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <inheritdoc />
 | 
				
			||||||
 | 
					        public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Convert to list so this isn't executed for each iteration
 | 
				
			||||||
 | 
					            var parts = GetExportTypes<T>()
 | 
				
			||||||
 | 
					                .Select(i => defaultFunc(i))
 | 
				
			||||||
 | 
					                .Where(i => i != null)
 | 
				
			||||||
 | 
					                .Cast<T>()
 | 
				
			||||||
 | 
					                .ToList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (manageLifetime)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                lock (_disposableParts)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    _disposableParts.AddRange(parts.OfType<IDisposable>());
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return parts;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Runs the startup tasks.
 | 
					        /// Runs the startup tasks.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
@ -511,7 +552,7 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            RegisterServices();
 | 
					            RegisterServices();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            RegisterPluginServices();
 | 
					            _pluginManager.RegisterServices(ServiceCollection);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -525,7 +566,7 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            ServiceCollection.AddSingleton(ConfigurationManager);
 | 
					            ServiceCollection.AddSingleton(ConfigurationManager);
 | 
				
			||||||
            ServiceCollection.AddSingleton<IApplicationHost>(this);
 | 
					            ServiceCollection.AddSingleton<IApplicationHost>(this);
 | 
				
			||||||
 | 
					            ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
 | 
				
			||||||
            ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 | 
					            ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ServiceCollection.AddSingleton(_fileSystemManager);
 | 
					            ServiceCollection.AddSingleton(_fileSystemManager);
 | 
				
			||||||
@ -767,34 +808,7 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
 | 
					            ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
 | 
				
			||||||
            _plugins = GetExports<IPlugin>()
 | 
					            _pluginManager.CreatePlugins();
 | 
				
			||||||
                        .Where(i => i != null)
 | 
					 | 
				
			||||||
                        .ToArray();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (Plugins != null)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                foreach (var plugin in Plugins)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        // Ensure the version number matches the Plugin Manifest information.
 | 
					 | 
				
			||||||
                        foreach (var item in _pluginsManifests)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase))
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                // Update version number to that of the manifest.
 | 
					 | 
				
			||||||
                                assemblyPlugin.SetAttributes(
 | 
					 | 
				
			||||||
                                    plugin.AssemblyFilePath,
 | 
					 | 
				
			||||||
                                    Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)),
 | 
					 | 
				
			||||||
                                    item.Version);
 | 
					 | 
				
			||||||
                                break;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            _urlPrefixes = GetUrlPrefixes().ToArray();
 | 
					            _urlPrefixes = GetUrlPrefixes().ToArray();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -833,22 +847,6 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
            _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
 | 
					            _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void RegisterPluginServices()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                try
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
 | 
					 | 
				
			||||||
                    instance.RegisterServices(ServiceCollection);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                catch (Exception ex)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
 | 
					        private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            foreach (var ass in assemblies)
 | 
					            foreach (var ass in assemblies)
 | 
				
			||||||
@ -861,11 +859,13 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
                catch (FileNotFoundException ex)
 | 
					                catch (FileNotFoundException ex)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
 | 
					                    Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
 | 
				
			||||||
 | 
					                    _pluginManager.FailPlugin(ass);
 | 
				
			||||||
                    continue;
 | 
					                    continue;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                catch (TypeLoadException ex)
 | 
					                catch (TypeLoadException ex)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
 | 
					                    Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
 | 
				
			||||||
 | 
					                    _pluginManager.FailPlugin(ass);
 | 
				
			||||||
                    continue;
 | 
					                    continue;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1028,130 +1028,15 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected abstract void RestartInternal();
 | 
					        protected abstract void RestartInternal();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc/>
 | 
					 | 
				
			||||||
        public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            var minimumVersion = new Version(0, 0, 0, 1);
 | 
					 | 
				
			||||||
            var versions = new List<LocalPlugin>();
 | 
					 | 
				
			||||||
            if (!Directory.Exists(path))
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                // Plugin path doesn't exist, don't try to enumerate subfolders.
 | 
					 | 
				
			||||||
                return Enumerable.Empty<LocalPlugin>();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            foreach (var dir in directories)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                try
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    var metafile = Path.Combine(dir, "meta.json");
 | 
					 | 
				
			||||||
                    if (File.Exists(metafile))
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        var jsonString = File.ReadAllText(metafile, Encoding.UTF8);
 | 
					 | 
				
			||||||
                        var manifest = JsonSerializer.Deserialize<PluginManifest>(jsonString, _jsonOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            targetAbi = minimumVersion;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if (!Version.TryParse(manifest.Version, out var version))
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            version = minimumVersion;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if (ApplicationVersion >= targetAbi)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            // Only load Plugins if the plugin is built for this version or below.
 | 
					 | 
				
			||||||
                            versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    else
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        // No metafile, so lets see if the folder is versioned.
 | 
					 | 
				
			||||||
                        metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        int versionIndex = dir.LastIndexOf('_');
 | 
					 | 
				
			||||||
                        if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion))
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            // Versioned folder.
 | 
					 | 
				
			||||||
                            versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        else
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            // Un-versioned folder - Add it under the path name and version 0.0.0.1.
 | 
					 | 
				
			||||||
                            versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                catch
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            string lastName = string.Empty;
 | 
					 | 
				
			||||||
            versions.Sort(LocalPlugin.Compare);
 | 
					 | 
				
			||||||
            // Traverse backwards through the list.
 | 
					 | 
				
			||||||
            // The first item will be the latest version.
 | 
					 | 
				
			||||||
            for (int x = versions.Count - 1; x >= 0; x--)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
 | 
					 | 
				
			||||||
                    lastName = versions[x].Name;
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (!string.IsNullOrEmpty(lastName) && cleanup)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    // Attempt a cleanup of old folders.
 | 
					 | 
				
			||||||
                    try
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Logger.LogDebug("Deleting {Path}", versions[x].Path);
 | 
					 | 
				
			||||||
                        Directory.Delete(versions[x].Path, true);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    catch (Exception e)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    versions.RemoveAt(x);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return versions;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the composable part assemblies.
 | 
					        /// Gets the composable part assemblies.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <returns>IEnumerable{Assembly}.</returns>
 | 
					        /// <returns>IEnumerable{Assembly}.</returns>
 | 
				
			||||||
        protected IEnumerable<Assembly> GetComposablePartAssemblies()
 | 
					        protected IEnumerable<Assembly> GetComposablePartAssemblies()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (Directory.Exists(ApplicationPaths.PluginsPath))
 | 
					            foreach (var p in _pluginManager.LoadAssemblies())
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                _pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList();
 | 
					                yield return p;
 | 
				
			||||||
                foreach (var plugin in _pluginsManifests)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    foreach (var file in plugin.DllFiles)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Assembly plugAss;
 | 
					 | 
				
			||||||
                        try
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            plugAss = Assembly.LoadFrom(file);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        catch (FileLoadException ex)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            Logger.LogError(ex, "Failed to load assembly {Path}", file);
 | 
					 | 
				
			||||||
                            continue;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
 | 
					 | 
				
			||||||
                        yield return plugAss;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Include composable parts in the Model assembly
 | 
					            // Include composable parts in the Model assembly
 | 
				
			||||||
@ -1393,17 +1278,6 @@ namespace Emby.Server.Implementations
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Removes the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <param name="plugin">The plugin.</param>
 | 
					 | 
				
			||||||
        public void RemovePlugin(IPlugin plugin)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            var list = _plugins.ToList();
 | 
					 | 
				
			||||||
            list.Remove(plugin);
 | 
					 | 
				
			||||||
            _plugins = list.ToArray();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        public IEnumerable<Assembly> GetApiPluginAssemblies()
 | 
					        public IEnumerable<Assembly> GetApiPluginAssemblies()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var assemblies = _allConcreteTypes
 | 
					            var assemblies = _allConcreteTypes
 | 
				
			||||||
 | 
				
			|||||||
@ -65,5 +65,4 @@
 | 
				
			|||||||
    <EmbeddedResource Include="Localization\Core\*.json" />
 | 
					    <EmbeddedResource Include="Localization\Core\*.json" />
 | 
				
			||||||
    <EmbeddedResource Include="Localization\Ratings\*.csv" />
 | 
					    <EmbeddedResource Include="Localization\Ratings\*.csv" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					 | 
				
			||||||
</Project>
 | 
					</Project>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										688
									
								
								Emby.Server.Implementations/Plugins/PluginManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										688
									
								
								Emby.Server.Implementations/Plugins/PluginManager.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,688 @@
 | 
				
			|||||||
 | 
					#nullable enable
 | 
				
			||||||
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Collections.Generic;
 | 
				
			||||||
 | 
					using System.IO;
 | 
				
			||||||
 | 
					using System.Linq;
 | 
				
			||||||
 | 
					using System.Reflection;
 | 
				
			||||||
 | 
					using System.Text;
 | 
				
			||||||
 | 
					using System.Text.Json;
 | 
				
			||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using MediaBrowser.Common;
 | 
				
			||||||
 | 
					using MediaBrowser.Common.Extensions;
 | 
				
			||||||
 | 
					using MediaBrowser.Common.Json;
 | 
				
			||||||
 | 
					using MediaBrowser.Common.Json.Converters;
 | 
				
			||||||
 | 
					using MediaBrowser.Common.Plugins;
 | 
				
			||||||
 | 
					using MediaBrowser.Model.Configuration;
 | 
				
			||||||
 | 
					using MediaBrowser.Model.Plugins;
 | 
				
			||||||
 | 
					using Microsoft.Extensions.DependencyInjection;
 | 
				
			||||||
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Emby.Server.Implementations.Plugins
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Defines the <see cref="PluginManager" />.
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    public class PluginManager : IPluginManager
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        private readonly string _pluginsPath;
 | 
				
			||||||
 | 
					        private readonly Version _appVersion;
 | 
				
			||||||
 | 
					        private readonly JsonSerializerOptions _jsonOptions;
 | 
				
			||||||
 | 
					        private readonly ILogger<PluginManager> _logger;
 | 
				
			||||||
 | 
					        private readonly IApplicationHost _appHost;
 | 
				
			||||||
 | 
					        private readonly ServerConfiguration _config;
 | 
				
			||||||
 | 
					        private readonly IList<LocalPlugin> _plugins;
 | 
				
			||||||
 | 
					        private readonly Version _minimumVersion;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Initializes a new instance of the <see cref="PluginManager"/> class.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="logger">The <see cref="ILogger"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="appHost">The <see cref="IApplicationHost"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="config">The <see cref="ServerConfiguration"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="pluginsPath">The plugin path.</param>
 | 
				
			||||||
 | 
					        /// <param name="appVersion">The application version.</param>
 | 
				
			||||||
 | 
					        public PluginManager(
 | 
				
			||||||
 | 
					            ILogger<PluginManager> logger,
 | 
				
			||||||
 | 
					            IApplicationHost appHost,
 | 
				
			||||||
 | 
					            ServerConfiguration config,
 | 
				
			||||||
 | 
					            string pluginsPath,
 | 
				
			||||||
 | 
					            Version appVersion)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 | 
				
			||||||
 | 
					            _pluginsPath = pluginsPath;
 | 
				
			||||||
 | 
					            _appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
 | 
				
			||||||
 | 
					            _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions())
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                WriteIndented = true
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // We need to use the default GUID converter, so we need to remove any custom ones.
 | 
				
			||||||
 | 
					            for (int a = _jsonOptions.Converters.Count - 1; a >= 0; a--)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                if (_jsonOptions.Converters[a] is JsonGuidConverter convertor)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    _jsonOptions.Converters.Remove(convertor);
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _config = config;
 | 
				
			||||||
 | 
					            _appHost = appHost;
 | 
				
			||||||
 | 
					            _minimumVersion = new Version(0, 0, 0, 1);
 | 
				
			||||||
 | 
					            _plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the Plugins.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public IList<LocalPlugin> Plugins => _plugins;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Returns all the assemblies.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <returns>An IEnumerable{Assembly}.</returns>
 | 
				
			||||||
 | 
					        public IEnumerable<Assembly> LoadAssemblies()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Attempt to remove any deleted plugins and change any successors to be active.
 | 
				
			||||||
 | 
					            for (int i = _plugins.Count - 1; i >= 0; i--)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var plugin = _plugins[i];
 | 
				
			||||||
 | 
					                if (plugin.Manifest.Status == PluginStatus.Deleted && DeletePlugin(plugin))
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    // See if there is another version, and if so make that active.
 | 
				
			||||||
 | 
					                    ProcessAlternative(plugin);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Now load the assemblies..
 | 
				
			||||||
 | 
					            foreach (var plugin in _plugins)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                UpdatePluginSuperceedStatus(plugin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (plugin.IsEnabledAndSupported == false)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    _logger.LogInformation("Skipping disabled plugin {Version} of {Name} ", plugin.Version, plugin.Name);
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                foreach (var file in plugin.DllFiles)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Assembly assembly;
 | 
				
			||||||
 | 
					                    try
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        assembly = Assembly.LoadFrom(file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // This force loads all reference dll's that the plugin uses in the try..catch block.
 | 
				
			||||||
 | 
					                        // Removing this will cause JF to bomb out if referenced dll's cause issues.
 | 
				
			||||||
 | 
					                        assembly.GetExportedTypes();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    catch (FileLoadException ex)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
 | 
				
			||||||
 | 
					                        ChangePluginState(plugin, PluginStatus.Malfunctioned);
 | 
				
			||||||
 | 
					                        continue;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
 | 
				
			||||||
 | 
					                    yield return assembly;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Creates all the plugin instances.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public void CreatePlugins()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            _ = _appHost.GetExports<IPlugin>(CreatePluginInstance)
 | 
				
			||||||
 | 
					                .Where(i => i != null)
 | 
				
			||||||
 | 
					                .ToArray();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Registers the plugin's services with the DI.
 | 
				
			||||||
 | 
					        /// Note: DI is not yet instantiated yet.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param>
 | 
				
			||||||
 | 
					        public void RegisterServices(IServiceCollection serviceCollection)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            foreach (var pluginServiceRegistrator in _appHost.GetExportTypes<IPluginServiceRegistrator>())
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var plugin = GetPluginByAssembly(pluginServiceRegistrator.Assembly);
 | 
				
			||||||
 | 
					                if (plugin == null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    _logger.LogError("Unable to find plugin in assembly {Assembly}", pluginServiceRegistrator.Assembly.FullName);
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                UpdatePluginSuperceedStatus(plugin);
 | 
				
			||||||
 | 
					                if (!plugin.IsEnabledAndSupported)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                try
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    var instance = (IPluginServiceRegistrator?)Activator.CreateInstance(pluginServiceRegistrator);
 | 
				
			||||||
 | 
					                    instance?.RegisterServices(serviceCollection);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					#pragma warning disable CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					                catch (Exception ex)
 | 
				
			||||||
 | 
					#pragma warning restore CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    _logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly.FullName);
 | 
				
			||||||
 | 
					                    if (ChangePluginState(plugin, PluginStatus.Malfunctioned))
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        _logger.LogInformation("Disabling plugin {Path}", plugin.Path);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Imports a plugin manifest from <paramref name="folder"/>.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="folder">Folder of the plugin.</param>
 | 
				
			||||||
 | 
					        public void ImportPluginFrom(string folder)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (string.IsNullOrEmpty(folder))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                throw new ArgumentNullException(nameof(folder));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Load the plugin.
 | 
				
			||||||
 | 
					            var plugin = LoadManifest(folder);
 | 
				
			||||||
 | 
					            // Make sure we haven't already loaded this.
 | 
				
			||||||
 | 
					            if (_plugins.Any(p => p.Manifest.Equals(plugin.Manifest)))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _plugins.Add(plugin);
 | 
				
			||||||
 | 
					            EnablePlugin(plugin);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Removes the plugin reference '<paramref name="plugin"/>.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="plugin">The plugin.</param>
 | 
				
			||||||
 | 
					        /// <returns>Outcome of the operation.</returns>
 | 
				
			||||||
 | 
					        public bool RemovePlugin(LocalPlugin plugin)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (plugin == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                throw new ArgumentNullException(nameof(plugin));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (DeletePlugin(plugin))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ProcessAlternative(plugin);
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _logger.LogWarning("Unable to delete {Path}, so marking as deleteOnStartup.", plugin.Path);
 | 
				
			||||||
 | 
					            // Unable to delete, so disable.
 | 
				
			||||||
 | 
					            if (ChangePluginState(plugin, PluginStatus.Deleted))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ProcessAlternative(plugin);
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Attempts to find the plugin with and id of <paramref name="id"/>.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="id">The <see cref="Guid"/> of plugin.</param>
 | 
				
			||||||
 | 
					        /// <param name="version">Optional <see cref="Version"/> of the plugin to locate.</param>
 | 
				
			||||||
 | 
					        /// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns>
 | 
				
			||||||
 | 
					        public LocalPlugin? GetPlugin(Guid id, Version? version = null)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            LocalPlugin? plugin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (version == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // If no version is given, return the current instance.
 | 
				
			||||||
 | 
					                var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                plugin = plugins.FirstOrDefault(p => p.Instance != null);
 | 
				
			||||||
 | 
					                if (plugin == null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    plugin = plugins.OrderByDescending(p => p.Version).FirstOrDefault();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // Match id and version number.
 | 
				
			||||||
 | 
					                plugin = _plugins.FirstOrDefault(p => p.Id.Equals(id) && p.Version.Equals(version));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return plugin;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Enables the plugin, disabling all other versions.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
 | 
				
			||||||
 | 
					        public void EnablePlugin(LocalPlugin plugin)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (plugin == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                throw new ArgumentNullException(nameof(plugin));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (ChangePluginState(plugin, PluginStatus.Active))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // See if there is another version, and if so, supercede it.
 | 
				
			||||||
 | 
					                ProcessAlternative(plugin);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Disable the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
 | 
				
			||||||
 | 
					        public void DisablePlugin(LocalPlugin plugin)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (plugin == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                throw new ArgumentNullException(nameof(plugin));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Update the manifest on disk
 | 
				
			||||||
 | 
					            if (ChangePluginState(plugin, PluginStatus.Disabled))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // If there is another version, activate it.
 | 
				
			||||||
 | 
					                ProcessAlternative(plugin);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Disable the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param>
 | 
				
			||||||
 | 
					        public void FailPlugin(Assembly assembly)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Only save if disabled.
 | 
				
			||||||
 | 
					            if (assembly == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                throw new ArgumentNullException(nameof(assembly));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var plugin = _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location));
 | 
				
			||||||
 | 
					            if (plugin == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // A plugin's assembly didn't cause this issue, so ignore it.
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ChangePluginState(plugin, PluginStatus.Malfunctioned);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Saves the manifest back to disk.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
 | 
				
			||||||
 | 
					        /// <param name="path">The path where to save the manifest.</param>
 | 
				
			||||||
 | 
					        /// <returns>True if successful.</returns>
 | 
				
			||||||
 | 
					        public bool SaveManifest(PluginManifest manifest, string path)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (manifest == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var data = JsonSerializer.Serialize(manifest, _jsonOptions);
 | 
				
			||||||
 | 
					                File.WriteAllText(Path.Combine(path, "meta.json"), data, Encoding.UTF8);
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					#pragma warning disable CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					            catch (Exception e)
 | 
				
			||||||
 | 
					#pragma warning restore CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Changes a plugin's load status.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="plugin">The <see cref="LocalPlugin"/> instance.</param>
 | 
				
			||||||
 | 
					        /// <param name="state">The <see cref="PluginStatus"/> of the plugin.</param>
 | 
				
			||||||
 | 
					        /// <returns>Success of the task.</returns>
 | 
				
			||||||
 | 
					        private bool ChangePluginState(LocalPlugin plugin, PluginStatus state)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (plugin.Manifest.Status == state || string.IsNullOrEmpty(plugin.Path))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // No need to save as the state hasn't changed.
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            plugin.Manifest.Status = state;
 | 
				
			||||||
 | 
					            return SaveManifest(plugin.Manifest, plugin.Path);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Finds the plugin record using the assembly.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="assembly">The <see cref="Assembly"/> being sought.</param>
 | 
				
			||||||
 | 
					        /// <returns>The matching record, or null if not found.</returns>
 | 
				
			||||||
 | 
					        private LocalPlugin? GetPluginByAssembly(Assembly assembly)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Find which plugin it is by the path.
 | 
				
			||||||
 | 
					            return _plugins.FirstOrDefault(p => string.Equals(p.Path, Path.GetDirectoryName(assembly.Location), StringComparison.Ordinal));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Creates the instance safe.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="type">The type.</param>
 | 
				
			||||||
 | 
					        /// <returns>System.Object.</returns>
 | 
				
			||||||
 | 
					        private IPlugin? CreatePluginInstance(Type type)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Find the record for this plugin.
 | 
				
			||||||
 | 
					            var plugin = GetPluginByAssembly(type.Assembly);
 | 
				
			||||||
 | 
					            if (plugin?.Manifest.Status < PluginStatus.Active)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _logger.LogDebug("Creating instance of {Type}", type);
 | 
				
			||||||
 | 
					                var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
 | 
				
			||||||
 | 
					                if (plugin == null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    // Create a dummy record for the providers.
 | 
				
			||||||
 | 
					                    // TODO: remove this code, if all provided have been released as separate plugins.
 | 
				
			||||||
 | 
					                    plugin = new LocalPlugin(
 | 
				
			||||||
 | 
					                        instance.AssemblyFilePath,
 | 
				
			||||||
 | 
					                        true,
 | 
				
			||||||
 | 
					                        new PluginManifest
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            Id = instance.Id,
 | 
				
			||||||
 | 
					                            Status = PluginStatus.Active,
 | 
				
			||||||
 | 
					                            Name = instance.Name,
 | 
				
			||||||
 | 
					                            Version = instance.Version.ToString()
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        Instance = instance
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    _plugins.Add(plugin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    plugin.Manifest.Status = PluginStatus.Active;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                else
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    plugin.Instance = instance;
 | 
				
			||||||
 | 
					                    var manifest = plugin.Manifest;
 | 
				
			||||||
 | 
					                    var pluginStr = plugin.Instance.Version.ToString();
 | 
				
			||||||
 | 
					                    bool changed = false;
 | 
				
			||||||
 | 
					                    if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal))
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        // If a plugin without a manifest failed to load due to an external issue (eg config),
 | 
				
			||||||
 | 
					                        // this updates the manifest to the actual plugin values.
 | 
				
			||||||
 | 
					                        manifest.Version = pluginStr;
 | 
				
			||||||
 | 
					                        manifest.Name = plugin.Instance.Name;
 | 
				
			||||||
 | 
					                        manifest.Description = plugin.Instance.Description;
 | 
				
			||||||
 | 
					                        changed = true;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    changed = changed || manifest.Status != PluginStatus.Active;
 | 
				
			||||||
 | 
					                    manifest.Status = PluginStatus.Active;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (changed)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        SaveManifest(manifest, plugin.Path);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                _logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return instance;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					#pragma warning disable CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					            catch (Exception ex)
 | 
				
			||||||
 | 
					#pragma warning restore CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _logger.LogError(ex, "Error creating {Type}", type.FullName);
 | 
				
			||||||
 | 
					                if (plugin != null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    if (ChangePluginState(plugin, PluginStatus.Malfunctioned))
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        _logger.LogInformation("Plugin {Path} has been disabled.", plugin.Path);
 | 
				
			||||||
 | 
					                        return null;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                _logger.LogDebug("Unable to auto-disable.");
 | 
				
			||||||
 | 
					                return null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private void UpdatePluginSuperceedStatus(LocalPlugin plugin)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (plugin.Manifest.Status != PluginStatus.Superceded)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var predecessor = _plugins.OrderByDescending(p => p.Version)
 | 
				
			||||||
 | 
					                .FirstOrDefault(p => p.Id.Equals(plugin.Id) && p.IsEnabledAndSupported && p.Version != plugin.Version);
 | 
				
			||||||
 | 
					            if (predecessor != null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            plugin.Manifest.Status = PluginStatus.Active;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Attempts to delete a plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="plugin">A <see cref="LocalPlugin"/> instance to delete.</param>
 | 
				
			||||||
 | 
					        /// <returns>True if successful.</returns>
 | 
				
			||||||
 | 
					        private bool DeletePlugin(LocalPlugin plugin)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Attempt a cleanup of old folders.
 | 
				
			||||||
 | 
					            try
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Directory.Delete(plugin.Path, true);
 | 
				
			||||||
 | 
					                _logger.LogDebug("Deleted {Path}", plugin.Path);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					#pragma warning disable CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					            catch
 | 
				
			||||||
 | 
					#pragma warning restore CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return _plugins.Remove(plugin);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private LocalPlugin LoadManifest(string dir)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Version? version;
 | 
				
			||||||
 | 
					            PluginManifest? manifest = null;
 | 
				
			||||||
 | 
					            var metafile = Path.Combine(dir, "meta.json");
 | 
				
			||||||
 | 
					            if (File.Exists(metafile))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                try
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    var data = File.ReadAllText(metafile, Encoding.UTF8);
 | 
				
			||||||
 | 
					                    manifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					#pragma warning disable CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					                catch (Exception ex)
 | 
				
			||||||
 | 
					#pragma warning restore CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    _logger.LogError(ex, "Error deserializing {Path}.", dir);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (manifest != null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    targetAbi = _minimumVersion;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (!Version.TryParse(manifest.Version, out version))
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    manifest.Version = _minimumVersion.ToString();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return new LocalPlugin(dir, _appVersion >= targetAbi, manifest);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // No metafile, so lets see if the folder is versioned.
 | 
				
			||||||
 | 
					            // TODO: Phase this support out in future versions.
 | 
				
			||||||
 | 
					            metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
 | 
				
			||||||
 | 
					            int versionIndex = dir.LastIndexOf('_');
 | 
				
			||||||
 | 
					            if (versionIndex != -1)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // Get the version number from the filename if possible.
 | 
				
			||||||
 | 
					                metafile = Path.GetFileName(dir[..versionIndex]) ?? dir[..versionIndex];
 | 
				
			||||||
 | 
					                version = Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version? parsedVersion) ? parsedVersion : _appVersion;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // Un-versioned folder - Add it under the path name and version it suitable for this instance.
 | 
				
			||||||
 | 
					                version = _appVersion;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Auto-create a plugin manifest, so we can disable it, if it fails to load.
 | 
				
			||||||
 | 
					            manifest = new PluginManifest
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Status = PluginStatus.Restart,
 | 
				
			||||||
 | 
					                Name = metafile,
 | 
				
			||||||
 | 
					                AutoUpdate = false,
 | 
				
			||||||
 | 
					                Id = metafile.GetMD5(),
 | 
				
			||||||
 | 
					                TargetAbi = _appVersion.ToString(),
 | 
				
			||||||
 | 
					                Version = version.ToString()
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return new LocalPlugin(dir, true, manifest);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the list of local plugins.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <returns>Enumerable of local plugins.</returns>
 | 
				
			||||||
 | 
					        private IEnumerable<LocalPlugin> DiscoverPlugins()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var versions = new List<LocalPlugin>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!Directory.Exists(_pluginsPath))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // Plugin path doesn't exist, don't try to enumerate sub-folders.
 | 
				
			||||||
 | 
					                return Enumerable.Empty<LocalPlugin>();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var directories = Directory.EnumerateDirectories(_pluginsPath, "*.*", SearchOption.TopDirectoryOnly);
 | 
				
			||||||
 | 
					            foreach (var dir in directories)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                versions.Add(LoadManifest(dir));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            string lastName = string.Empty;
 | 
				
			||||||
 | 
					            versions.Sort(LocalPlugin.Compare);
 | 
				
			||||||
 | 
					            // Traverse backwards through the list.
 | 
				
			||||||
 | 
					            // The first item will be the latest version.
 | 
				
			||||||
 | 
					            for (int x = versions.Count - 1; x >= 0; x--)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var entry = versions[x];
 | 
				
			||||||
 | 
					                if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories));
 | 
				
			||||||
 | 
					                    if (entry.IsEnabledAndSupported)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        lastName = entry.Name;
 | 
				
			||||||
 | 
					                        continue;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (string.IsNullOrEmpty(lastName))
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var manifest = entry.Manifest;
 | 
				
			||||||
 | 
					                var cleaned = false;
 | 
				
			||||||
 | 
					                var path = entry.Path;
 | 
				
			||||||
 | 
					                if (_config.RemoveOldPlugins)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    // Attempt a cleanup of old folders.
 | 
				
			||||||
 | 
					                    try
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        _logger.LogDebug("Deleting {Path}", path);
 | 
				
			||||||
 | 
					                        Directory.Delete(path, true);
 | 
				
			||||||
 | 
					                        cleaned = true;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					#pragma warning disable CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					                    catch (Exception e)
 | 
				
			||||||
 | 
					#pragma warning restore CA1031 // Do not catch general exception types
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        _logger.LogWarning(e, "Unable to delete {Path}", path);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (cleaned)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        versions.RemoveAt(x);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    else
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        if (manifest == null)
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            _logger.LogWarning("Unable to disable plugin {Path}", entry.Path);
 | 
				
			||||||
 | 
					                            continue;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        ChangePluginState(entry, PluginStatus.Deleted);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Only want plugin folders which have files.
 | 
				
			||||||
 | 
					            return versions.Where(p => p.DllFiles.Count != 0);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Changes the status of the other versions of the plugin to "Superceded".
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="plugin">The <see cref="LocalPlugin"/> that's master.</param>
 | 
				
			||||||
 | 
					        private void ProcessAlternative(LocalPlugin plugin)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Detect whether there is another version of this plugin that needs disabling.
 | 
				
			||||||
 | 
					            var previousVersion = _plugins.OrderByDescending(p => p.Version)
 | 
				
			||||||
 | 
					                .FirstOrDefault(
 | 
				
			||||||
 | 
					                    p => p.Id.Equals(plugin.Id)
 | 
				
			||||||
 | 
					                    && p.IsEnabledAndSupported
 | 
				
			||||||
 | 
					                    && p.Version != plugin.Version);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (previousVersion == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // This value is memory only - so that the web will show restart required.
 | 
				
			||||||
 | 
					                plugin.Manifest.Status = PluginStatus.Restart;
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Superceded))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _logger.LogError("Unable to enable version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else if (plugin.Manifest.Status == PluginStatus.Superceded && !ChangePluginState(previousVersion, PluginStatus.Active))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _logger.LogError("Unable to supercede version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // This value is memory only - so that the web will show restart required.
 | 
				
			||||||
 | 
					            plugin.Manifest.Status = PluginStatus.Restart;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,60 +0,0 @@
 | 
				
			|||||||
using System;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Emby.Server.Implementations.Plugins
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /// <summary>
 | 
					 | 
				
			||||||
    /// Defines a Plugin manifest file.
 | 
					 | 
				
			||||||
    /// </summary>
 | 
					 | 
				
			||||||
    public class PluginManifest
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the category of the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public string Category { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the changelog information.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public string Changelog { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the description of the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public string Description { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the Global Unique Identifier for the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public Guid Guid { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the Name of the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public string Name { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets an overview of the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public string Overview { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the owner of the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public string Owner { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the compatibility version for the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public string TargetAbi { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the timestamp of the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public DateTime Timestamp { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the Version number of the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public string Version { get; set; }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -8,10 +8,10 @@ using System.Net.Http;
 | 
				
			|||||||
using System.Threading;
 | 
					using System.Threading;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using MediaBrowser.Common.Updates;
 | 
					using MediaBrowser.Common.Updates;
 | 
				
			||||||
 | 
					using MediaBrowser.Model.Globalization;
 | 
				
			||||||
using MediaBrowser.Model.Net;
 | 
					using MediaBrowser.Model.Net;
 | 
				
			||||||
using MediaBrowser.Model.Tasks;
 | 
					using MediaBrowser.Model.Tasks;
 | 
				
			||||||
using Microsoft.Extensions.Logging;
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
using MediaBrowser.Model.Globalization;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Emby.Server.Implementations.ScheduledTasks
 | 
					namespace Emby.Server.Implementations.ScheduledTasks
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
#pragma warning disable CS1591
 | 
					#nullable enable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Concurrent;
 | 
					using System.Collections.Concurrent;
 | 
				
			||||||
@ -40,17 +40,15 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
        private readonly IEventManager _eventManager;
 | 
					        private readonly IEventManager _eventManager;
 | 
				
			||||||
        private readonly IHttpClientFactory _httpClientFactory;
 | 
					        private readonly IHttpClientFactory _httpClientFactory;
 | 
				
			||||||
        private readonly IServerConfigurationManager _config;
 | 
					        private readonly IServerConfigurationManager _config;
 | 
				
			||||||
        private readonly IFileSystem _fileSystem;
 | 
					 | 
				
			||||||
        private readonly JsonSerializerOptions _jsonSerializerOptions;
 | 
					        private readonly JsonSerializerOptions _jsonSerializerOptions;
 | 
				
			||||||
 | 
					        private readonly IPluginManager _pluginManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the application host.
 | 
					        /// Gets the application host.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The application host.</value>
 | 
					        /// <value>The application host.</value>
 | 
				
			||||||
        private readonly IServerApplicationHost _applicationHost;
 | 
					        private readonly IServerApplicationHost _applicationHost;
 | 
				
			||||||
 | 
					 | 
				
			||||||
        private readonly IZipClient _zipClient;
 | 
					        private readonly IZipClient _zipClient;
 | 
				
			||||||
 | 
					 | 
				
			||||||
        private readonly object _currentInstallationsLock = new object();
 | 
					        private readonly object _currentInstallationsLock = new object();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -63,6 +61,17 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
 | 
					        private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Initializes a new instance of the <see cref="InstallationManager"/> class.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="logger">The <see cref="ILogger{InstallationManager}"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="eventManager">The <see cref="IEventManager"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="zipClient">The <see cref="IZipClient"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
 | 
				
			||||||
        public InstallationManager(
 | 
					        public InstallationManager(
 | 
				
			||||||
            ILogger<InstallationManager> logger,
 | 
					            ILogger<InstallationManager> logger,
 | 
				
			||||||
            IServerApplicationHost appHost,
 | 
					            IServerApplicationHost appHost,
 | 
				
			||||||
@ -70,8 +79,8 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
            IEventManager eventManager,
 | 
					            IEventManager eventManager,
 | 
				
			||||||
            IHttpClientFactory httpClientFactory,
 | 
					            IHttpClientFactory httpClientFactory,
 | 
				
			||||||
            IServerConfigurationManager config,
 | 
					            IServerConfigurationManager config,
 | 
				
			||||||
            IFileSystem fileSystem,
 | 
					            IZipClient zipClient,
 | 
				
			||||||
            IZipClient zipClient)
 | 
					            IPluginManager pluginManager)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
 | 
					            _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
 | 
				
			||||||
            _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
 | 
					            _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
 | 
				
			||||||
@ -82,38 +91,65 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
            _eventManager = eventManager;
 | 
					            _eventManager = eventManager;
 | 
				
			||||||
            _httpClientFactory = httpClientFactory;
 | 
					            _httpClientFactory = httpClientFactory;
 | 
				
			||||||
            _config = config;
 | 
					            _config = config;
 | 
				
			||||||
            _fileSystem = fileSystem;
 | 
					 | 
				
			||||||
            _zipClient = zipClient;
 | 
					            _zipClient = zipClient;
 | 
				
			||||||
            _jsonSerializerOptions = JsonDefaults.GetOptions();
 | 
					            _jsonSerializerOptions = JsonDefaults.GetOptions();
 | 
				
			||||||
 | 
					            _pluginManager = pluginManager;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
 | 
					        public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default)
 | 
					        public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var packages = await _httpClientFactory.CreateClient(NamedClient.Default)
 | 
					                List<PackageInfo>? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
 | 
				
			||||||
                        .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
 | 
					                        .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (packages == null)
 | 
					                if (packages == null)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    return Array.Empty<PackageInfo>();
 | 
					                    return Array.Empty<PackageInfo>();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var minimumVersion = new Version(0, 0, 0, 1);
 | 
				
			||||||
                // Store the repository and repository url with each version, as they may be spread apart.
 | 
					                // Store the repository and repository url with each version, as they may be spread apart.
 | 
				
			||||||
                foreach (var entry in packages)
 | 
					                foreach (var entry in packages)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    foreach (var ver in entry.versions)
 | 
					                    for (int a = entry.Versions.Count - 1; a >= 0; a--)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        ver.repositoryName = manifestName;
 | 
					                        var ver = entry.Versions[a];
 | 
				
			||||||
                        ver.repositoryUrl = manifest;
 | 
					                        ver.RepositoryName = manifestName;
 | 
				
			||||||
 | 
					                        ver.RepositoryUrl = manifest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (!filterIncompatible)
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            continue;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (!Version.TryParse(ver.TargetAbi, out var targetAbi))
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            targetAbi = minimumVersion;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // Only show plugins that are greater than or equal to targetAbi.
 | 
				
			||||||
 | 
					                        if (_applicationHost.ApplicationVersion >= targetAbi)
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            continue;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // Not compatible with this version so remove it.
 | 
				
			||||||
 | 
					                        entry.Versions.Remove(ver);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return packages;
 | 
					                return packages;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            catch (IOException ex)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _logger.LogError(ex, "Cannot locate the plugin manifest {Manifest}", manifest);
 | 
				
			||||||
 | 
					                return Array.Empty<PackageInfo>();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            catch (JsonException ex)
 | 
					            catch (JsonException ex)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
 | 
					                _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
 | 
				
			||||||
@ -131,85 +167,58 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private static void MergeSort(IList<VersionInfo> source, IList<VersionInfo> dest)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            int sLength = source.Count - 1;
 | 
					 | 
				
			||||||
            int dLength = dest.Count;
 | 
					 | 
				
			||||||
            int s = 0, d = 0;
 | 
					 | 
				
			||||||
            var sourceVersion = source[0].VersionNumber;
 | 
					 | 
				
			||||||
            var destVersion = dest[0].VersionNumber;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            while (d < dLength)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                if (sourceVersion.CompareTo(destVersion) >= 0)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    if (s < sLength)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        sourceVersion = source[++s].VersionNumber;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    else
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        // Append all of destination to the end of source.
 | 
					 | 
				
			||||||
                        while (d < dLength)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            source.Add(dest[d++]);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                else
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    source.Insert(s++, dest[d++]);
 | 
					 | 
				
			||||||
                    if (d >= dLength)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    sLength++;
 | 
					 | 
				
			||||||
                    destVersion = dest[d].VersionNumber;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
 | 
					        public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var result = new List<PackageInfo>();
 | 
					            var result = new List<PackageInfo>();
 | 
				
			||||||
            foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
 | 
					            foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                if (repository.Enabled)
 | 
					                if (repository.Enabled && repository.Url != null)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    // Where repositories have the same content, the details of the first is taken.
 | 
					                    // Where repositories have the same content, the details from the first is taken.
 | 
				
			||||||
                    foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true))
 | 
					                    foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        if (!Guid.TryParse(package.guid, out var packageGuid))
 | 
					                        if (!Guid.TryParse(package.Id, out var packageGuid))
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            // Package doesn't have a valid GUID, skip.
 | 
					                            // Package doesn't have a valid GUID, skip.
 | 
				
			||||||
                            continue;
 | 
					                            continue;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        for (var i = package.versions.Count - 1; i >= 0; i--)
 | 
					                        var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // Remove invalid versions from the valid package.
 | 
				
			||||||
 | 
					                        for (var i = package.Versions.Count - 1; i >= 0; i--)
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
 | 
					                            var version = package.Versions[i];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
 | 
				
			||||||
 | 
					                            // Update the manifests, if anything changes.
 | 
				
			||||||
 | 
					                            if (plugin != null)
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
 | 
					                                if (!string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal))
 | 
				
			||||||
 | 
					                                {
 | 
				
			||||||
 | 
					                                    plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
 | 
				
			||||||
 | 
					                                    _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            // Remove versions with a target abi that is greater then the current application version.
 | 
					                            // Remove versions with a target abi that is greater then the current application version.
 | 
				
			||||||
                            if (Version.TryParse(package.versions[i].targetAbi, out var targetAbi)
 | 
					                            if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
 | 
				
			||||||
                                && _applicationHost.ApplicationVersion < targetAbi)
 | 
					 | 
				
			||||||
                            {
 | 
					                            {
 | 
				
			||||||
                                package.versions.RemoveAt(i);
 | 
					                                package.Versions.RemoveAt(i);
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        // Don't add a package that doesn't have any compatible versions.
 | 
					                        // Don't add a package that doesn't have any compatible versions.
 | 
				
			||||||
                        if (package.versions.Count == 0)
 | 
					                        if (package.Versions.Count == 0)
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            continue;
 | 
					                            continue;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        var existing = FilterPackages(result, package.name, packageGuid).FirstOrDefault();
 | 
					 | 
				
			||||||
                        if (existing != null)
 | 
					                        if (existing != null)
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            // Assumption is both lists are ordered, so slot these into the correct place.
 | 
					                            // Assumption is both lists are ordered, so slot these into the correct place.
 | 
				
			||||||
                            MergeSort(existing.versions, package.versions);
 | 
					                            MergeSortedList(existing.Versions, package.Versions);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        else
 | 
					                        else
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
@ -225,23 +234,23 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public IEnumerable<PackageInfo> FilterPackages(
 | 
					        public IEnumerable<PackageInfo> FilterPackages(
 | 
				
			||||||
            IEnumerable<PackageInfo> availablePackages,
 | 
					            IEnumerable<PackageInfo> availablePackages,
 | 
				
			||||||
            string name = null,
 | 
					            string? name = null,
 | 
				
			||||||
            Guid guid = default,
 | 
					            Guid? id = default,
 | 
				
			||||||
            Version specificVersion = null)
 | 
					            Version? specificVersion = null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (name != null)
 | 
					            if (name != null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                availablePackages = availablePackages.Where(x => x.name.Equals(name, StringComparison.OrdinalIgnoreCase));
 | 
					                availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (guid != Guid.Empty)
 | 
					            if (id != Guid.Empty)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid);
 | 
					                availablePackages = availablePackages.Where(x => Guid.Parse(x.Id) == id);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (specificVersion != null)
 | 
					            if (specificVersion != null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                availablePackages = availablePackages.Where(x => x.versions.Where(y => y.VersionNumber.Equals(specificVersion)).Any());
 | 
					                availablePackages = availablePackages.Where(x => x.Versions.Any(y => y.VersionNumber.Equals(specificVersion)));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return availablePackages;
 | 
					            return availablePackages;
 | 
				
			||||||
@ -250,12 +259,12 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public IEnumerable<InstallationInfo> GetCompatibleVersions(
 | 
					        public IEnumerable<InstallationInfo> GetCompatibleVersions(
 | 
				
			||||||
            IEnumerable<PackageInfo> availablePackages,
 | 
					            IEnumerable<PackageInfo> availablePackages,
 | 
				
			||||||
            string name = null,
 | 
					            string? name = null,
 | 
				
			||||||
            Guid guid = default,
 | 
					            Guid? id = default,
 | 
				
			||||||
            Version minVersion = null,
 | 
					            Version? minVersion = null,
 | 
				
			||||||
            Version specificVersion = null)
 | 
					            Version? specificVersion = null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var package = FilterPackages(availablePackages, name, guid, specificVersion).FirstOrDefault();
 | 
					            var package = FilterPackages(availablePackages, name, id, specificVersion).FirstOrDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Package not found in repository
 | 
					            // Package not found in repository
 | 
				
			||||||
            if (package == null)
 | 
					            if (package == null)
 | 
				
			||||||
@ -264,8 +273,8 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var appVer = _applicationHost.ApplicationVersion;
 | 
					            var appVer = _applicationHost.ApplicationVersion;
 | 
				
			||||||
            var availableVersions = package.versions
 | 
					            var availableVersions = package.Versions
 | 
				
			||||||
                .Where(x => Version.Parse(x.targetAbi) <= appVer);
 | 
					                .Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (specificVersion != null)
 | 
					            if (specificVersion != null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -280,12 +289,12 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                yield return new InstallationInfo
 | 
					                yield return new InstallationInfo
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    Changelog = v.changelog,
 | 
					                    Changelog = v.Changelog,
 | 
				
			||||||
                    Guid = new Guid(package.guid),
 | 
					                    Id = new Guid(package.Id),
 | 
				
			||||||
                    Name = package.name,
 | 
					                    Name = package.Name,
 | 
				
			||||||
                    Version = v.VersionNumber,
 | 
					                    Version = v.VersionNumber,
 | 
				
			||||||
                    SourceUrl = v.sourceUrl,
 | 
					                    SourceUrl = v.SourceUrl,
 | 
				
			||||||
                    Checksum = v.checksum
 | 
					                    Checksum = v.Checksum
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -297,20 +306,6 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
            return GetAvailablePluginUpdates(catalog);
 | 
					            return GetAvailablePluginUpdates(catalog);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
 | 
					 | 
				
			||||||
            foreach (var plugin in plugins)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
 | 
					 | 
				
			||||||
                var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
 | 
					 | 
				
			||||||
                if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    yield return version;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
 | 
					        public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@ -388,24 +383,140 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Installs the package internal.
 | 
					        /// Uninstalls a plugin.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="package">The package.</param>
 | 
					        /// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
 | 
				
			||||||
        /// <param name="cancellationToken">The cancellation token.</param>
 | 
					        public void UninstallPlugin(LocalPlugin plugin)
 | 
				
			||||||
        /// <returns><see cref="Task" />.</returns>
 | 
					 | 
				
			||||||
        private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // Set last update time if we were installed before
 | 
					            if (plugin == null)
 | 
				
			||||||
            IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
 | 
					            {
 | 
				
			||||||
                           ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase));
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Do the install
 | 
					            if (plugin.Instance?.CanUninstall == false)
 | 
				
			||||||
            await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
 | 
					            {
 | 
				
			||||||
 | 
					                _logger.LogWarning("Attempt to delete non removable plugin {PluginName}, ignoring request", plugin.Name);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Do plugin-specific processing
 | 
					            plugin.Instance?.OnUninstalling();
 | 
				
			||||||
            _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return plugin != null;
 | 
					            // Remove it the quick way for now
 | 
				
			||||||
 | 
					            _pluginManager.RemovePlugin(plugin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _applicationHost.NotifyPendingRestart();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <inheritdoc/>
 | 
				
			||||||
 | 
					        public bool CancelInstallation(Guid id)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            lock (_currentInstallationsLock)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var install = _currentInstallations.Find(x => x.info.Id == id);
 | 
				
			||||||
 | 
					                if (install == default((InstallationInfo, CancellationTokenSource)))
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                install.token.Cancel();
 | 
				
			||||||
 | 
					                _currentInstallations.Remove(install);
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <inheritdoc />
 | 
				
			||||||
 | 
					        public void Dispose()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Dispose(true);
 | 
				
			||||||
 | 
					            GC.SuppressFinalize(this);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Releases unmanaged and optionally managed resources.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources or <c>false</c> to release only unmanaged resources.</param>
 | 
				
			||||||
 | 
					        protected virtual void Dispose(bool dispose)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (dispose)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                lock (_currentInstallationsLock)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    foreach (var (info, token) in _currentInstallations)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        token.Dispose();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    _currentInstallations.Clear();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Merges two sorted lists.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="source">The source <see cref="IList{VersionInfo}"/> instance to merge.</param>
 | 
				
			||||||
 | 
					        /// <param name="dest">The destination <see cref="IList{VersionInfo}"/> instance to merge with.</param>
 | 
				
			||||||
 | 
					        private static void MergeSortedList(IList<VersionInfo> source, IList<VersionInfo> dest)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            int sLength = source.Count - 1;
 | 
				
			||||||
 | 
					            int dLength = dest.Count;
 | 
				
			||||||
 | 
					            int s = 0, d = 0;
 | 
				
			||||||
 | 
					            var sourceVersion = source[0].VersionNumber;
 | 
				
			||||||
 | 
					            var destVersion = dest[0].VersionNumber;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            while (d < dLength)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                if (sourceVersion.CompareTo(destVersion) >= 0)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    if (s < sLength)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        sourceVersion = source[++s].VersionNumber;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    else
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        // Append all of destination to the end of source.
 | 
				
			||||||
 | 
					                        while (d < dLength)
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            source.Add(dest[d++]);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                else
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    source.Insert(s++, dest[d++]);
 | 
				
			||||||
 | 
					                    if (d >= dLength)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    sLength++;
 | 
				
			||||||
 | 
					                    destVersion = dest[d].VersionNumber;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var plugins = _pluginManager.Plugins;
 | 
				
			||||||
 | 
					            foreach (var plugin in plugins)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                if (plugin.Manifest?.AutoUpdate == false)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
 | 
				
			||||||
 | 
					                var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (version != null && CompletedInstallations.All(x => x.Id != version.Id))
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    yield return version;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
 | 
					        private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
 | 
				
			||||||
@ -450,7 +561,9 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
                {
 | 
					                {
 | 
				
			||||||
                    Directory.Delete(targetDir, true);
 | 
					                    Directory.Delete(targetDir, true);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					#pragma warning disable CA1031 // Do not catch general exception types
 | 
				
			||||||
                catch
 | 
					                catch
 | 
				
			||||||
 | 
					#pragma warning restore CA1031 // Do not catch general exception types
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    // Ignore any exceptions.
 | 
					                    // Ignore any exceptions.
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@ -458,119 +571,27 @@ namespace Emby.Server.Implementations.Updates
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            stream.Position = 0;
 | 
					            stream.Position = 0;
 | 
				
			||||||
            _zipClient.ExtractAllFromZip(stream, targetDir, true);
 | 
					            _zipClient.ExtractAllFromZip(stream, targetDir, true);
 | 
				
			||||||
 | 
					            _pluginManager.ImportPluginFrom(targetDir);
 | 
				
			||||||
#pragma warning restore CA5351
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
 | 
				
			||||||
        /// Uninstalls a plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <param name="plugin">The plugin.</param>
 | 
					 | 
				
			||||||
        public void UninstallPlugin(IPlugin plugin)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (!plugin.CanUninstall)
 | 
					            // Set last update time if we were installed before
 | 
				
			||||||
 | 
					            LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
 | 
				
			||||||
 | 
					                  ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
 | 
				
			||||||
 | 
					            if (plugin != null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                _logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
 | 
					                plugin.Manifest.Timestamp = DateTime.UtcNow;
 | 
				
			||||||
                return;
 | 
					                _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            plugin.OnUninstalling();
 | 
					            // Do the install
 | 
				
			||||||
 | 
					            await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Remove it the quick way for now
 | 
					            // Do plugin-specific processing
 | 
				
			||||||
            _applicationHost.RemovePlugin(plugin);
 | 
					            _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var path = plugin.AssemblyFilePath;
 | 
					            return plugin != null;
 | 
				
			||||||
            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();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -29,18 +29,22 @@ namespace Jellyfin.Api.Controllers
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly ILogger<DashboardController> _logger;
 | 
					        private readonly ILogger<DashboardController> _logger;
 | 
				
			||||||
        private readonly IServerApplicationHost _appHost;
 | 
					        private readonly IServerApplicationHost _appHost;
 | 
				
			||||||
 | 
					        private readonly IPluginManager _pluginManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes a new instance of the <see cref="DashboardController"/> class.
 | 
					        /// Initializes a new instance of the <see cref="DashboardController"/> class.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
 | 
					        /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
 | 
				
			||||||
        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
 | 
					        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
 | 
				
			||||||
 | 
					        /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
 | 
				
			||||||
        public DashboardController(
 | 
					        public DashboardController(
 | 
				
			||||||
            ILogger<DashboardController> logger,
 | 
					            ILogger<DashboardController> logger,
 | 
				
			||||||
            IServerApplicationHost appHost)
 | 
					            IServerApplicationHost appHost,
 | 
				
			||||||
 | 
					            IPluginManager pluginManager)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _logger = logger;
 | 
					            _logger = logger;
 | 
				
			||||||
            _appHost = appHost;
 | 
					            _appHost = appHost;
 | 
				
			||||||
 | 
					            _pluginManager = pluginManager;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -83,7 +87,7 @@ namespace Jellyfin.Api.Controllers
 | 
				
			|||||||
                .Where(i => i != null)
 | 
					                .Where(i => i != null)
 | 
				
			||||||
                .ToList();
 | 
					                .ToList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
 | 
					            configPages.AddRange(_pluginManager.Plugins.SelectMany(GetConfigPages));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (pageType.HasValue)
 | 
					            if (pageType.HasValue)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -155,24 +159,24 @@ namespace Jellyfin.Api.Controllers
 | 
				
			|||||||
            return NotFound();
 | 
					            return NotFound();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
 | 
					        private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
 | 
					            return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
 | 
					        private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin? plugin)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (!(plugin is IHasWebPages hasWebPages))
 | 
					            if (plugin?.Instance is not IHasWebPages hasWebPages)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                return new List<Tuple<PluginPageInfo, IPlugin>>();
 | 
					                return new List<Tuple<PluginPageInfo, IPlugin>>();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
 | 
					            return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
 | 
					        private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            return _appHost.Plugins.SelectMany(GetPluginPages);
 | 
					            return _pluginManager.Plugins.SelectMany(GetPluginPages);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
 | 
				
			|||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using Jellyfin.Api.Constants;
 | 
					using Jellyfin.Api.Constants;
 | 
				
			||||||
 | 
					using MediaBrowser.Common.Json;
 | 
				
			||||||
using MediaBrowser.Common.Updates;
 | 
					using MediaBrowser.Common.Updates;
 | 
				
			||||||
using MediaBrowser.Controller.Configuration;
 | 
					using MediaBrowser.Controller.Configuration;
 | 
				
			||||||
using MediaBrowser.Model.Updates;
 | 
					using MediaBrowser.Model.Updates;
 | 
				
			||||||
@ -99,7 +100,7 @@ namespace Jellyfin.Api.Controllers
 | 
				
			|||||||
            var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
 | 
					            var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
 | 
				
			||||||
            if (!string.IsNullOrEmpty(repositoryUrl))
 | 
					            if (!string.IsNullOrEmpty(repositoryUrl))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any())
 | 
					                packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)))
 | 
				
			||||||
                    .ToList();
 | 
					                    .ToList();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,21 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.ComponentModel.DataAnnotations;
 | 
					using System.ComponentModel.DataAnnotations;
 | 
				
			||||||
 | 
					using System.Globalization;
 | 
				
			||||||
 | 
					using System.IO;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
 | 
					using System.Net.Mime;
 | 
				
			||||||
using System.Text.Json;
 | 
					using System.Text.Json;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using Jellyfin.Api.Attributes;
 | 
				
			||||||
using Jellyfin.Api.Constants;
 | 
					using Jellyfin.Api.Constants;
 | 
				
			||||||
using Jellyfin.Api.Models.PluginDtos;
 | 
					using Jellyfin.Api.Models.PluginDtos;
 | 
				
			||||||
using MediaBrowser.Common;
 | 
					using MediaBrowser.Common.Configuration;
 | 
				
			||||||
using MediaBrowser.Common.Json;
 | 
					using MediaBrowser.Common.Json;
 | 
				
			||||||
using MediaBrowser.Common.Plugins;
 | 
					using MediaBrowser.Common.Plugins;
 | 
				
			||||||
using MediaBrowser.Common.Updates;
 | 
					using MediaBrowser.Common.Updates;
 | 
				
			||||||
 | 
					using MediaBrowser.Model.Configuration;
 | 
				
			||||||
 | 
					using MediaBrowser.Model.Net;
 | 
				
			||||||
using MediaBrowser.Model.Plugins;
 | 
					using MediaBrowser.Model.Plugins;
 | 
				
			||||||
using Microsoft.AspNetCore.Authorization;
 | 
					using Microsoft.AspNetCore.Authorization;
 | 
				
			||||||
using Microsoft.AspNetCore.Http;
 | 
					using Microsoft.AspNetCore.Http;
 | 
				
			||||||
@ -23,112 +29,26 @@ namespace Jellyfin.Api.Controllers
 | 
				
			|||||||
    [Authorize(Policy = Policies.DefaultAuthorization)]
 | 
					    [Authorize(Policy = Policies.DefaultAuthorization)]
 | 
				
			||||||
    public class PluginsController : BaseJellyfinApiController
 | 
					    public class PluginsController : BaseJellyfinApiController
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly IApplicationHost _appHost;
 | 
					 | 
				
			||||||
        private readonly IInstallationManager _installationManager;
 | 
					        private readonly IInstallationManager _installationManager;
 | 
				
			||||||
 | 
					        private readonly IPluginManager _pluginManager;
 | 
				
			||||||
        private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
 | 
					        private readonly IConfigurationManager _config;
 | 
				
			||||||
 | 
					        private readonly JsonSerializerOptions _serializerOptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes a new instance of the <see cref="PluginsController"/> class.
 | 
					        /// Initializes a new instance of the <see cref="PluginsController"/> class.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param>
 | 
					 | 
				
			||||||
        /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
 | 
					        /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
 | 
				
			||||||
 | 
					        /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
 | 
				
			||||||
 | 
					        /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
 | 
				
			||||||
        public PluginsController(
 | 
					        public PluginsController(
 | 
				
			||||||
            IApplicationHost appHost,
 | 
					            IInstallationManager installationManager,
 | 
				
			||||||
            IInstallationManager installationManager)
 | 
					            IPluginManager pluginManager,
 | 
				
			||||||
 | 
					            IConfigurationManager config)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _appHost = appHost;
 | 
					 | 
				
			||||||
            _installationManager = installationManager;
 | 
					            _installationManager = installationManager;
 | 
				
			||||||
        }
 | 
					            _pluginManager = pluginManager;
 | 
				
			||||||
 | 
					            _serializerOptions = JsonDefaults.GetOptions();
 | 
				
			||||||
        /// <summary>
 | 
					            _config = config;
 | 
				
			||||||
        /// 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();
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -139,7 +59,7 @@ namespace Jellyfin.Api.Controllers
 | 
				
			|||||||
        [Obsolete("This endpoint should not be used.")]
 | 
					        [Obsolete("This endpoint should not be used.")]
 | 
				
			||||||
        [HttpGet("SecurityInfo")]
 | 
					        [HttpGet("SecurityInfo")]
 | 
				
			||||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
					        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
        public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
 | 
					        public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            return new PluginSecurityInfo
 | 
					            return new PluginSecurityInfo
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -148,21 +68,6 @@ namespace Jellyfin.Api.Controllers
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Updates plugin security info.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <param name="pluginSecurityInfo">Plugin security info.</param>
 | 
					 | 
				
			||||||
        /// <response code="204">Plugin security info updated.</response>
 | 
					 | 
				
			||||||
        /// <returns>An <see cref="NoContentResult"/>.</returns>
 | 
					 | 
				
			||||||
        [Obsolete("This endpoint should not be used.")]
 | 
					 | 
				
			||||||
        [HttpPost("SecurityInfo")]
 | 
					 | 
				
			||||||
        [Authorize(Policy = Policies.RequiresElevation)]
 | 
					 | 
				
			||||||
        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
					 | 
				
			||||||
        public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return NoContent();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets registration status for a feature.
 | 
					        /// Gets registration status for a feature.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
@ -172,7 +77,7 @@ namespace Jellyfin.Api.Controllers
 | 
				
			|||||||
        [Obsolete("This endpoint should not be used.")]
 | 
					        [Obsolete("This endpoint should not be used.")]
 | 
				
			||||||
        [HttpPost("RegistrationRecords/{name}")]
 | 
					        [HttpPost("RegistrationRecords/{name}")]
 | 
				
			||||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
					        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
        public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
 | 
					        public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            return new MBRegistrationRecord
 | 
					            return new MBRegistrationRecord
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -194,11 +99,253 @@ namespace Jellyfin.Api.Controllers
 | 
				
			|||||||
        [Obsolete("Paid plugins are not supported")]
 | 
					        [Obsolete("Paid plugins are not supported")]
 | 
				
			||||||
        [HttpGet("Registrations/{name}")]
 | 
					        [HttpGet("Registrations/{name}")]
 | 
				
			||||||
        [ProducesResponseType(StatusCodes.Status501NotImplemented)]
 | 
					        [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,
 | 
					            // 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.
 | 
					            // delete all these registration endpoints. They are only kept for compatibility.
 | 
				
			||||||
            throw new NotImplementedException();
 | 
					            throw new NotImplementedException();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets a list of currently installed plugins.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <response code="200">Installed plugins returned.</response>
 | 
				
			||||||
 | 
					        /// <returns>List of currently installed plugins.</returns>
 | 
				
			||||||
 | 
					        [HttpGet]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
 | 
					        public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return Ok(_pluginManager.Plugins
 | 
				
			||||||
 | 
					                .OrderBy(p => p.Name)
 | 
				
			||||||
 | 
					                .Select(p => p.GetPluginInfo()));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Enables a disabled plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="pluginId">Plugin id.</param>
 | 
				
			||||||
 | 
					        /// <param name="version">Plugin version.</param>
 | 
				
			||||||
 | 
					        /// <response code="204">Plugin enabled.</response>
 | 
				
			||||||
 | 
					        /// <response code="404">Plugin not found.</response>
 | 
				
			||||||
 | 
					        /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
 | 
				
			||||||
 | 
					        [HttpPost("{pluginId}/{version}/Enable")]
 | 
				
			||||||
 | 
					        [Authorize(Policy = Policies.RequiresElevation)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
 | 
					        public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var plugin = _pluginManager.GetPlugin(pluginId, version);
 | 
				
			||||||
 | 
					            if (plugin == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return NotFound();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _pluginManager.EnablePlugin(plugin);
 | 
				
			||||||
 | 
					            return NoContent();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Disable a plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="pluginId">Plugin id.</param>
 | 
				
			||||||
 | 
					        /// <param name="version">Plugin version.</param>
 | 
				
			||||||
 | 
					        /// <response code="204">Plugin disabled.</response>
 | 
				
			||||||
 | 
					        /// <response code="404">Plugin not found.</response>
 | 
				
			||||||
 | 
					        /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
 | 
				
			||||||
 | 
					        [HttpPost("{pluginId}/{version}/Disable")]
 | 
				
			||||||
 | 
					        [Authorize(Policy = Policies.RequiresElevation)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
 | 
					        public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var plugin = _pluginManager.GetPlugin(pluginId, version);
 | 
				
			||||||
 | 
					            if (plugin == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return NotFound();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _pluginManager.DisablePlugin(plugin);
 | 
				
			||||||
 | 
					            return NoContent();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Uninstalls a plugin by version.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="pluginId">Plugin id.</param>
 | 
				
			||||||
 | 
					        /// <param name="version">Plugin version.</param>
 | 
				
			||||||
 | 
					        /// <response code="204">Plugin uninstalled.</response>
 | 
				
			||||||
 | 
					        /// <response code="404">Plugin not found.</response>
 | 
				
			||||||
 | 
					        /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
 | 
				
			||||||
 | 
					        [HttpDelete("{pluginId}/{version}")]
 | 
				
			||||||
 | 
					        [Authorize(Policy = Policies.RequiresElevation)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
 | 
					        public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var plugin = _pluginManager.GetPlugin(pluginId, version);
 | 
				
			||||||
 | 
					            if (plugin == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return NotFound();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _installationManager.UninstallPlugin(plugin);
 | 
				
			||||||
 | 
					            return NoContent();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Uninstalls a plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="pluginId">Plugin id.</param>
 | 
				
			||||||
 | 
					        /// <response code="204">Plugin uninstalled.</response>
 | 
				
			||||||
 | 
					        /// <response code="404">Plugin not found.</response>
 | 
				
			||||||
 | 
					        /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
 | 
				
			||||||
 | 
					        [HttpDelete("{pluginId}")]
 | 
				
			||||||
 | 
					        [Authorize(Policy = Policies.RequiresElevation)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
 | 
					        [Obsolete("Please use the UninstallPluginByVersion API.")]
 | 
				
			||||||
 | 
					        public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // If no version is given, return the current instance.
 | 
				
			||||||
 | 
					            var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Select the un-instanced one first.
 | 
				
			||||||
 | 
					            var plugin = plugins.FirstOrDefault(p => p.Instance == null);
 | 
				
			||||||
 | 
					            if (plugin == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // Then by the status.
 | 
				
			||||||
 | 
					                plugin = plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (plugin != null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _installationManager.UninstallPlugin(plugin);
 | 
				
			||||||
 | 
					                return NoContent();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return NotFound();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets plugin configuration.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="pluginId">Plugin id.</param>
 | 
				
			||||||
 | 
					        /// <response code="200">Plugin configuration returned.</response>
 | 
				
			||||||
 | 
					        /// <response code="404">Plugin not found or plugin configuration not found.</response>
 | 
				
			||||||
 | 
					        /// <returns>Plugin configuration.</returns>
 | 
				
			||||||
 | 
					        [HttpGet("{pluginId}/Configuration")]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
 | 
					        public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var plugin = _pluginManager.GetPlugin(pluginId);
 | 
				
			||||||
 | 
					            if (plugin?.Instance is IHasPluginConfiguration configPlugin)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return configPlugin.Configuration;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return NotFound();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Updates plugin configuration.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <remarks>
 | 
				
			||||||
 | 
					        /// Accepts plugin configuration as JSON body.
 | 
				
			||||||
 | 
					        /// </remarks>
 | 
				
			||||||
 | 
					        /// <param name="pluginId">Plugin id.</param>
 | 
				
			||||||
 | 
					        /// <response code="204">Plugin configuration updated.</response>
 | 
				
			||||||
 | 
					        /// <response code="404">Plugin not found or plugin does not have configuration.</response>
 | 
				
			||||||
 | 
					        /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
 | 
				
			||||||
 | 
					        [HttpPost("{pluginId}/Configuration")]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
 | 
					        public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var plugin = _pluginManager.GetPlugin(pluginId);
 | 
				
			||||||
 | 
					            if (plugin?.Instance is not IHasPluginConfiguration configPlugin)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return NotFound();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
 | 
				
			||||||
 | 
					                .ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (configuration != null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                configPlugin.UpdateConfiguration(configuration);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return NoContent();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets a plugin's image.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="pluginId">Plugin id.</param>
 | 
				
			||||||
 | 
					        /// <param name="version">Plugin version.</param>
 | 
				
			||||||
 | 
					        /// <response code="200">Plugin image returned.</response>
 | 
				
			||||||
 | 
					        /// <returns>Plugin's image.</returns>
 | 
				
			||||||
 | 
					        [HttpGet("{pluginId}/{version}/Image")]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
 | 
					        [ProducesImageFile]
 | 
				
			||||||
 | 
					        [AllowAnonymous]
 | 
				
			||||||
 | 
					        public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var plugin = _pluginManager.GetPlugin(pluginId, version);
 | 
				
			||||||
 | 
					            if (plugin == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return NotFound();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
 | 
				
			||||||
 | 
					            if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages
 | 
				
			||||||
 | 
					                || plugin.Manifest.ImagePath == null
 | 
				
			||||||
 | 
					                || !System.IO.File.Exists(imagePath))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return NotFound();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
 | 
				
			||||||
 | 
					            return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets a plugin's manifest.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="pluginId">Plugin id.</param>
 | 
				
			||||||
 | 
					        /// <response code="204">Plugin manifest returned.</response>
 | 
				
			||||||
 | 
					        /// <response code="404">Plugin not found.</response>
 | 
				
			||||||
 | 
					        /// <returns>A <see cref="PluginManifest"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
 | 
				
			||||||
 | 
					        [HttpPost("{pluginId}/Manifest")]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
 | 
					        public ActionResult<PluginManifest> GetPluginManifest([FromRoute, Required] Guid pluginId)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var plugin = _pluginManager.GetPlugin(pluginId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (plugin != null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return plugin.Manifest;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return NotFound();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Updates plugin security info.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="pluginSecurityInfo">Plugin security info.</param>
 | 
				
			||||||
 | 
					        /// <response code="204">Plugin security info updated.</response>
 | 
				
			||||||
 | 
					        /// <returns>An <see cref="NoContentResult"/>.</returns>
 | 
				
			||||||
 | 
					        [Obsolete("This endpoint should not be used.")]
 | 
				
			||||||
 | 
					        [HttpPost("SecurityInfo")]
 | 
				
			||||||
 | 
					        [Authorize(Policy = Policies.RequiresElevation)]
 | 
				
			||||||
 | 
					        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
				
			||||||
 | 
					        public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return NoContent();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
using MediaBrowser.Common.Plugins;
 | 
					using System;
 | 
				
			||||||
 | 
					using MediaBrowser.Common.Plugins;
 | 
				
			||||||
using MediaBrowser.Controller.Plugins;
 | 
					using MediaBrowser.Controller.Plugins;
 | 
				
			||||||
using MediaBrowser.Model.Plugins;
 | 
					using MediaBrowser.Model.Plugins;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,8 +23,7 @@ namespace Jellyfin.Api.Models
 | 
				
			|||||||
            if (page.Plugin != null)
 | 
					            if (page.Plugin != null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                DisplayName = page.Plugin.Name;
 | 
					                DisplayName = page.Plugin.Name;
 | 
				
			||||||
                // Don't use "N" because it needs to match Plugin.Id
 | 
					                PluginId = page.Plugin.Id;
 | 
				
			||||||
                PluginId = page.Plugin.Id.ToString();
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -32,16 +32,14 @@ namespace Jellyfin.Api.Models
 | 
				
			|||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param>
 | 
					        /// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param>
 | 
				
			||||||
        /// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param>
 | 
					        /// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param>
 | 
				
			||||||
        public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page)
 | 
					        public ConfigurationPageInfo(IPlugin? plugin, PluginPageInfo page)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Name = page.Name;
 | 
					            Name = page.Name;
 | 
				
			||||||
            EnableInMainMenu = page.EnableInMainMenu;
 | 
					            EnableInMainMenu = page.EnableInMainMenu;
 | 
				
			||||||
            MenuSection = page.MenuSection;
 | 
					            MenuSection = page.MenuSection;
 | 
				
			||||||
            MenuIcon = page.MenuIcon;
 | 
					            MenuIcon = page.MenuIcon;
 | 
				
			||||||
            DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin.Name : page.DisplayName;
 | 
					            DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin?.Name : page.DisplayName;
 | 
				
			||||||
 | 
					            PluginId = plugin?.Id;
 | 
				
			||||||
            // Don't use "N" because it needs to match Plugin.Id
 | 
					 | 
				
			||||||
            PluginId = plugin.Id.ToString();
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -80,6 +78,6 @@ namespace Jellyfin.Api.Models
 | 
				
			|||||||
        /// Gets or sets the plugin id.
 | 
					        /// Gets or sets the plugin id.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The plugin id.</value>
 | 
					        /// <value>The plugin id.</value>
 | 
				
			||||||
        public string? PluginId { get; set; }
 | 
					        public Guid? PluginId { get; set; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -227,6 +227,7 @@ namespace Jellyfin.Server.Extensions
 | 
				
			|||||||
                    options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
 | 
					                    options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
 | 
				
			||||||
                    options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
 | 
					                    options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
 | 
				
			||||||
                    options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
 | 
					                    options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
 | 
				
			||||||
 | 
					                    options.JsonSerializerOptions.PropertyNameCaseInsensitive = jsonOptions.PropertyNameCaseInsensitive;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    options.JsonSerializerOptions.Converters.Clear();
 | 
					                    options.JsonSerializerOptions.Converters.Clear();
 | 
				
			||||||
                    foreach (var converter in jsonOptions.Converters)
 | 
					                    foreach (var converter in jsonOptions.Converters)
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,16 @@ using System;
 | 
				
			|||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Reflection;
 | 
					using System.Reflection;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using MediaBrowser.Common.Plugins;
 | 
					 | 
				
			||||||
using Microsoft.Extensions.DependencyInjection;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Common
 | 
					namespace MediaBrowser.Common
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Delegate used with GetExports{T}.
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    /// <param name="type">Type to create.</param>
 | 
				
			||||||
 | 
					    /// <returns>New instance of type <param>type</param>.</returns>
 | 
				
			||||||
 | 
					    public delegate object CreationDelegate(Type type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// An interface to be implemented by the applications hosting a kernel.
 | 
					    /// An interface to be implemented by the applications hosting a kernel.
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
@ -53,6 +58,11 @@ namespace MediaBrowser.Common
 | 
				
			|||||||
        /// <value>The application version.</value>
 | 
					        /// <value>The application version.</value>
 | 
				
			||||||
        Version ApplicationVersion { get; }
 | 
					        Version ApplicationVersion { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the service provider.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        IServiceProvider ServiceProvider { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the application version.
 | 
					        /// Gets the application version.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
@ -71,12 +81,6 @@ namespace MediaBrowser.Common
 | 
				
			|||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        string ApplicationUserAgentAddress { get; }
 | 
					        string ApplicationUserAgentAddress { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the plugins.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The plugins.</value>
 | 
					 | 
				
			||||||
        IReadOnlyList<IPlugin> Plugins { get; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets all plugin assemblies which implement a custom rest api.
 | 
					        /// Gets all plugin assemblies which implement a custom rest api.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
@ -101,6 +105,22 @@ namespace MediaBrowser.Common
 | 
				
			|||||||
        /// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
 | 
					        /// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
 | 
				
			||||||
        IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true);
 | 
					        IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the exports.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <typeparam name="T">The type.</typeparam>
 | 
				
			||||||
 | 
					        /// <param name="defaultFunc">Delegate function that gets called to create the object.</param>
 | 
				
			||||||
 | 
					        /// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param>
 | 
				
			||||||
 | 
					        /// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
 | 
				
			||||||
 | 
					        IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the export types.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <typeparam name="T">The type.</typeparam>
 | 
				
			||||||
 | 
					        /// <returns>IEnumerable{Type}.</returns>
 | 
				
			||||||
 | 
					        IEnumerable<Type> GetExportTypes<T>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Resolves this instance.
 | 
					        /// Resolves this instance.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
@ -114,12 +134,6 @@ namespace MediaBrowser.Common
 | 
				
			|||||||
        /// <returns>A task.</returns>
 | 
					        /// <returns>A task.</returns>
 | 
				
			||||||
        Task Shutdown();
 | 
					        Task Shutdown();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Removes the plugin.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <param name="plugin">The plugin.</param>
 | 
					 | 
				
			||||||
        void RemovePlugin(IPlugin plugin);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes this instance.
 | 
					        /// Initializes this instance.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
 | 
				
			|||||||
@ -31,6 +31,7 @@ namespace MediaBrowser.Common.Json
 | 
				
			|||||||
            WriteIndented = false,
 | 
					            WriteIndented = false,
 | 
				
			||||||
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 | 
					            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 | 
				
			||||||
            NumberHandling = JsonNumberHandling.AllowReadingFromString,
 | 
					            NumberHandling = JsonNumberHandling.AllowReadingFromString,
 | 
				
			||||||
 | 
					            PropertyNameCaseInsensitive = true,
 | 
				
			||||||
            Converters =
 | 
					            Converters =
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                new JsonGuidConverter(),
 | 
					                new JsonGuidConverter(),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,3 @@
 | 
				
			|||||||
#pragma warning disable SA1402
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.IO;
 | 
					using System.IO;
 | 
				
			||||||
using System.Reflection;
 | 
					using System.Reflection;
 | 
				
			||||||
@ -7,7 +5,6 @@ using System.Runtime.InteropServices;
 | 
				
			|||||||
using MediaBrowser.Common.Configuration;
 | 
					using MediaBrowser.Common.Configuration;
 | 
				
			||||||
using MediaBrowser.Model.Plugins;
 | 
					using MediaBrowser.Model.Plugins;
 | 
				
			||||||
using MediaBrowser.Model.Serialization;
 | 
					using MediaBrowser.Model.Serialization;
 | 
				
			||||||
using Microsoft.Extensions.DependencyInjection;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Common.Plugins
 | 
					namespace MediaBrowser.Common.Plugins
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -64,14 +61,12 @@ namespace MediaBrowser.Common.Plugins
 | 
				
			|||||||
        /// <returns>PluginInfo.</returns>
 | 
					        /// <returns>PluginInfo.</returns>
 | 
				
			||||||
        public virtual PluginInfo GetPluginInfo()
 | 
					        public virtual PluginInfo GetPluginInfo()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var info = new PluginInfo
 | 
					            var info = new PluginInfo(
 | 
				
			||||||
            {
 | 
					                Name,
 | 
				
			||||||
                Name = Name,
 | 
					                Version,
 | 
				
			||||||
                Version = Version.ToString(),
 | 
					                Description,
 | 
				
			||||||
                Description = Description,
 | 
					                Id,
 | 
				
			||||||
                Id = Id.ToString(),
 | 
					                CanUninstall);
 | 
				
			||||||
                CanUninstall = CanUninstall
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return info;
 | 
					            return info;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -97,207 +92,4 @@ namespace MediaBrowser.Common.Plugins
 | 
				
			|||||||
            Id = assemblyId;
 | 
					            Id = assemblyId;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// <summary>
 | 
					 | 
				
			||||||
    /// Provides a common base class for all plugins.
 | 
					 | 
				
			||||||
    /// </summary>
 | 
					 | 
				
			||||||
    /// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam>
 | 
					 | 
				
			||||||
    public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration
 | 
					 | 
				
			||||||
        where TConfigurationType : BasePluginConfiguration
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// The configuration sync lock.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        private readonly object _configurationSyncLock = new object();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// The configuration save lock.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        private readonly object _configurationSaveLock = new object();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private Action<string> _directoryCreateFn;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// The configuration.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        private TConfigurationType _configuration;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <param name="applicationPaths">The application paths.</param>
 | 
					 | 
				
			||||||
        /// <param name="xmlSerializer">The XML serializer.</param>
 | 
					 | 
				
			||||||
        protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            ApplicationPaths = applicationPaths;
 | 
					 | 
				
			||||||
            XmlSerializer = xmlSerializer;
 | 
					 | 
				
			||||||
            if (this is IPluginAssembly assemblyPlugin)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                var assembly = GetType().Assembly;
 | 
					 | 
				
			||||||
                var assemblyName = assembly.GetName();
 | 
					 | 
				
			||||||
                var assemblyFilePath = assembly.Location;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
 | 
					 | 
				
			||||||
                if (idAttributes.Length > 0)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    var attribute = (GuidAttribute)idAttributes[0];
 | 
					 | 
				
			||||||
                    var assemblyId = new Guid(attribute.Value);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    assemblyPlugin.SetId(assemblyId);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (this is IHasPluginConfiguration hasPluginConfiguration)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the application paths.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The application paths.</value>
 | 
					 | 
				
			||||||
        protected IApplicationPaths ApplicationPaths { get; private set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the XML serializer.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The XML serializer.</value>
 | 
					 | 
				
			||||||
        protected IXmlSerializer XmlSerializer { get; private set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the type of configuration this plugin uses.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The type of the configuration.</value>
 | 
					 | 
				
			||||||
        public Type ConfigurationType => typeof(TConfigurationType);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the event handler that is triggered when this configuration changes.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public EventHandler<BasePluginConfiguration> ConfigurationChanged { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the name the assembly file.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The name of the assembly file.</value>
 | 
					 | 
				
			||||||
        protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets or sets the plugin configuration.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The configuration.</value>
 | 
					 | 
				
			||||||
        public TConfigurationType Configuration
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            get
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                // Lazy load
 | 
					 | 
				
			||||||
                if (_configuration == null)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    lock (_configurationSyncLock)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        if (_configuration == null)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            _configuration = LoadConfiguration();
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return _configuration;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            protected set => _configuration = value;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the name of the configuration file. Subclasses should override.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The name of the configuration file.</value>
 | 
					 | 
				
			||||||
        public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the full path to the configuration file.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The configuration file path.</value>
 | 
					 | 
				
			||||||
        public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the plugin configuration.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The configuration.</value>
 | 
					 | 
				
			||||||
        BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <inheritdoc />
 | 
					 | 
				
			||||||
        public void SetStartupInfo(Action<string> directoryCreateFn)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            // hack alert, until the .net core transition is complete
 | 
					 | 
				
			||||||
            _directoryCreateFn = directoryCreateFn;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private TConfigurationType LoadConfiguration()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            var path = ConfigurationFilePath;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            try
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            catch
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
 | 
					 | 
				
			||||||
                SaveConfiguration(config);
 | 
					 | 
				
			||||||
                return config;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Saves the current configuration to the file system.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <param name="config">Configuration to save.</param>
 | 
					 | 
				
			||||||
        public virtual void SaveConfiguration(TConfigurationType config)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            lock (_configurationSaveLock)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                _directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                XmlSerializer.SerializeToFile(config, ConfigurationFilePath);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Saves the current configuration to the file system.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public virtual void SaveConfiguration()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            SaveConfiguration(Configuration);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <inheritdoc />
 | 
					 | 
				
			||||||
        public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if (configuration == null)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                throw new ArgumentNullException(nameof(configuration));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Configuration = (TConfigurationType)configuration;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            SaveConfiguration(Configuration);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            ConfigurationChanged?.Invoke(this, configuration);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <inheritdoc />
 | 
					 | 
				
			||||||
        public override PluginInfo GetPluginInfo()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            var info = base.GetPluginInfo();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            info.ConfigurationFileName = ConfigurationFileName;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return info;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										208
									
								
								MediaBrowser.Common/Plugins/BasePluginOfT.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								MediaBrowser.Common/Plugins/BasePluginOfT.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,208 @@
 | 
				
			|||||||
 | 
					#pragma warning disable SA1649 // File name should match first type name
 | 
				
			||||||
 | 
					using System;
 | 
				
			||||||
 | 
					using System.IO;
 | 
				
			||||||
 | 
					using System.Runtime.InteropServices;
 | 
				
			||||||
 | 
					using MediaBrowser.Common.Configuration;
 | 
				
			||||||
 | 
					using MediaBrowser.Model.Plugins;
 | 
				
			||||||
 | 
					using MediaBrowser.Model.Serialization;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace MediaBrowser.Common.Plugins
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Provides a common base class for all plugins.
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    /// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam>
 | 
				
			||||||
 | 
					    public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration
 | 
				
			||||||
 | 
					        where TConfigurationType : BasePluginConfiguration
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// The configuration sync lock.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        private readonly object _configurationSyncLock = new object();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// The configuration save lock.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        private readonly object _configurationSaveLock = new object();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// The configuration.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        private TConfigurationType _configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="applicationPaths">The application paths.</param>
 | 
				
			||||||
 | 
					        /// <param name="xmlSerializer">The XML serializer.</param>
 | 
				
			||||||
 | 
					        protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            ApplicationPaths = applicationPaths;
 | 
				
			||||||
 | 
					            XmlSerializer = xmlSerializer;
 | 
				
			||||||
 | 
					            if (this is IPluginAssembly assemblyPlugin)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var assembly = GetType().Assembly;
 | 
				
			||||||
 | 
					                var assemblyName = assembly.GetName();
 | 
				
			||||||
 | 
					                var assemblyFilePath = assembly.Location;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
 | 
				
			||||||
 | 
					                if (!Directory.Exists(dataFolderPath) && Version != null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    // Try again with the version number appended to the folder name.
 | 
				
			||||||
 | 
					                    dataFolderPath = dataFolderPath + "_" + Version.ToString();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
 | 
				
			||||||
 | 
					                if (idAttributes.Length > 0)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    var attribute = (GuidAttribute)idAttributes[0];
 | 
				
			||||||
 | 
					                    var assemblyId = new Guid(attribute.Value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    assemblyPlugin.SetId(assemblyId);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the application paths.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <value>The application paths.</value>
 | 
				
			||||||
 | 
					        protected IApplicationPaths ApplicationPaths { get; private set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the XML serializer.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <value>The XML serializer.</value>
 | 
				
			||||||
 | 
					        protected IXmlSerializer XmlSerializer { get; private set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the type of configuration this plugin uses.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <value>The type of the configuration.</value>
 | 
				
			||||||
 | 
					        public Type ConfigurationType => typeof(TConfigurationType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the event handler that is triggered when this configuration changes.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public EventHandler<BasePluginConfiguration> ConfigurationChanged { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the name the assembly file.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <value>The name of the assembly file.</value>
 | 
				
			||||||
 | 
					        protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the plugin configuration.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <value>The configuration.</value>
 | 
				
			||||||
 | 
					        public TConfigurationType Configuration
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            get
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // Lazy load
 | 
				
			||||||
 | 
					                if (_configuration == null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    lock (_configurationSyncLock)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        if (_configuration == null)
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            _configuration = LoadConfiguration();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return _configuration;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            protected set => _configuration = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the name of the configuration file. Subclasses should override.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <value>The name of the configuration file.</value>
 | 
				
			||||||
 | 
					        public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the full path to the configuration file.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <value>The configuration file path.</value>
 | 
				
			||||||
 | 
					        public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the plugin configuration.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <value>The configuration.</value>
 | 
				
			||||||
 | 
					        BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Saves the current configuration to the file system.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="config">Configuration to save.</param>
 | 
				
			||||||
 | 
					        public virtual void SaveConfiguration(TConfigurationType config)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            lock (_configurationSaveLock)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var folder = Path.GetDirectoryName(ConfigurationFilePath);
 | 
				
			||||||
 | 
					                if (!Directory.Exists(folder))
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Directory.CreateDirectory(folder);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                XmlSerializer.SerializeToFile(config, ConfigurationFilePath);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Saves the current configuration to the file system.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public virtual void SaveConfiguration()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            SaveConfiguration(Configuration);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <inheritdoc />
 | 
				
			||||||
 | 
					        public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (configuration == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                throw new ArgumentNullException(nameof(configuration));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Configuration = (TConfigurationType)configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            SaveConfiguration(Configuration);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ConfigurationChanged?.Invoke(this, configuration);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <inheritdoc />
 | 
				
			||||||
 | 
					        public override PluginInfo GetPluginInfo()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var info = base.GetPluginInfo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            info.ConfigurationFileName = ConfigurationFileName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return info;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private TConfigurationType LoadConfiguration()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var path = ConfigurationFilePath;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            catch
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
 | 
				
			||||||
 | 
					                SaveConfiguration(config);
 | 
				
			||||||
 | 
					                return config;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								MediaBrowser.Common/Plugins/IHasPluginConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					using System;
 | 
				
			||||||
 | 
					using MediaBrowser.Model.Plugins;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace MediaBrowser.Common.Plugins
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Defines the <see cref="IHasPluginConfiguration" />.
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    public interface IHasPluginConfiguration
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the type of configuration this plugin uses.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        Type ConfigurationType { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the plugin's configuration.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        BasePluginConfiguration Configuration { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Completely overwrites the current configuration with a new copy.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="configuration">The configuration.</param>
 | 
				
			||||||
 | 
					        void UpdateConfiguration(BasePluginConfiguration configuration);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,44 +1,36 @@
 | 
				
			|||||||
#pragma warning disable CS1591
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using MediaBrowser.Model.Plugins;
 | 
					using MediaBrowser.Model.Plugins;
 | 
				
			||||||
using Microsoft.Extensions.DependencyInjection;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Common.Plugins
 | 
					namespace MediaBrowser.Common.Plugins
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// Interface IPlugin.
 | 
					    /// Defines the <see cref="IPlugin" />.
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public interface IPlugin
 | 
					    public interface IPlugin
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the name of the plugin.
 | 
					        /// Gets the name of the plugin.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The name.</value>
 | 
					 | 
				
			||||||
        string Name { get; }
 | 
					        string Name { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the description.
 | 
					        /// Gets the Description.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The description.</value>
 | 
					 | 
				
			||||||
        string Description { get; }
 | 
					        string Description { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the unique id.
 | 
					        /// Gets the unique id.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The unique id.</value>
 | 
					 | 
				
			||||||
        Guid Id { get; }
 | 
					        Guid Id { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the plugin version.
 | 
					        /// Gets the plugin version.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The version.</value>
 | 
					 | 
				
			||||||
        Version Version { get; }
 | 
					        Version Version { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the path to the assembly file.
 | 
					        /// Gets the path to the assembly file.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The assembly file path.</value>
 | 
					 | 
				
			||||||
        string AssemblyFilePath { get; }
 | 
					        string AssemblyFilePath { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -49,11 +41,10 @@ namespace MediaBrowser.Common.Plugins
 | 
				
			|||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.
 | 
					        /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The data folder path.</value>
 | 
					 | 
				
			||||||
        string DataFolderPath { get; }
 | 
					        string DataFolderPath { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the plugin info.
 | 
					        /// Gets the <see cref="PluginInfo"/>.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <returns>PluginInfo.</returns>
 | 
					        /// <returns>PluginInfo.</returns>
 | 
				
			||||||
        PluginInfo GetPluginInfo();
 | 
					        PluginInfo GetPluginInfo();
 | 
				
			||||||
@ -63,29 +54,4 @@ namespace MediaBrowser.Common.Plugins
 | 
				
			|||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        void OnUninstalling();
 | 
					        void OnUninstalling();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    public interface IHasPluginConfiguration
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the type of configuration this plugin uses.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The type of the configuration.</value>
 | 
					 | 
				
			||||||
        Type ConfigurationType { get; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the plugin's configuration.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <value>The configuration.</value>
 | 
					 | 
				
			||||||
        BasePluginConfiguration Configuration { get; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Completely overwrites the current configuration with a new copy
 | 
					 | 
				
			||||||
        /// Returns true or false indicating success or failure.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <param name="configuration">The configuration.</param>
 | 
					 | 
				
			||||||
        /// <exception cref="ArgumentNullException"><c>configuration</c> is <c>null</c>.</exception>
 | 
					 | 
				
			||||||
        void UpdateConfiguration(BasePluginConfiguration configuration);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        void SetStartupInfo(Action<string> directoryCreateFn);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										86
									
								
								MediaBrowser.Common/Plugins/IPluginManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								MediaBrowser.Common/Plugins/IPluginManager.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,86 @@
 | 
				
			|||||||
 | 
					#nullable enable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Collections.Generic;
 | 
				
			||||||
 | 
					using System.Reflection;
 | 
				
			||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using Microsoft.Extensions.DependencyInjection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace MediaBrowser.Common.Plugins
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Defines the <see cref="IPluginManager" />.
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    public interface IPluginManager
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the Plugins.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        IList<LocalPlugin> Plugins { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Creates the plugins.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        void CreatePlugins();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Returns all the assemblies.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <returns>An IEnumerable{Assembly}.</returns>
 | 
				
			||||||
 | 
					        IEnumerable<Assembly> LoadAssemblies();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Registers the plugin's services with the DI.
 | 
				
			||||||
 | 
					        /// Note: DI is not yet instantiated yet.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param>
 | 
				
			||||||
 | 
					        void RegisterServices(IServiceCollection serviceCollection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Saves the manifest back to disk.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
 | 
				
			||||||
 | 
					        /// <param name="path">The path where to save the manifest.</param>
 | 
				
			||||||
 | 
					        /// <returns>True if successful.</returns>
 | 
				
			||||||
 | 
					        bool SaveManifest(PluginManifest manifest, string path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Imports plugin details from a folder.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="folder">Folder of the plugin.</param>
 | 
				
			||||||
 | 
					        void ImportPluginFrom(string folder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Disable the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param>
 | 
				
			||||||
 | 
					        void FailPlugin(Assembly assembly);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Disable the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
 | 
				
			||||||
 | 
					        void DisablePlugin(LocalPlugin plugin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Enables the plugin, disabling all other versions.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
 | 
				
			||||||
 | 
					        void EnablePlugin(LocalPlugin plugin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Attempts to find the plugin with and id of <paramref name="id"/>.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="id">Id of plugin.</param>
 | 
				
			||||||
 | 
					        /// <param name="version">The version of the plugin to locate.</param>
 | 
				
			||||||
 | 
					        /// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns>
 | 
				
			||||||
 | 
					        LocalPlugin? GetPlugin(Guid id, Version? version = null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Removes the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="plugin">The plugin.</param>
 | 
				
			||||||
 | 
					        /// <returns>Outcome of the operation.</returns>
 | 
				
			||||||
 | 
					        bool RemovePlugin(LocalPlugin plugin);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
 | 
					#nullable enable
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Globalization;
 | 
					using MediaBrowser.Model.Plugins;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Common.Plugins
 | 
					namespace MediaBrowser.Common.Plugins
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -9,36 +10,48 @@ namespace MediaBrowser.Common.Plugins
 | 
				
			|||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public class LocalPlugin : IEquatable<LocalPlugin>
 | 
					    public class LocalPlugin : IEquatable<LocalPlugin>
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        private readonly bool _supported;
 | 
				
			||||||
 | 
					        private Version? _version;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes a new instance of the <see cref="LocalPlugin"/> class.
 | 
					        /// Initializes a new instance of the <see cref="LocalPlugin"/> class.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="id">The plugin id.</param>
 | 
					 | 
				
			||||||
        /// <param name="name">The plugin name.</param>
 | 
					 | 
				
			||||||
        /// <param name="version">The plugin version.</param>
 | 
					 | 
				
			||||||
        /// <param name="path">The plugin path.</param>
 | 
					        /// <param name="path">The plugin path.</param>
 | 
				
			||||||
        public LocalPlugin(Guid id, string name, Version version, string path)
 | 
					        /// <param name="isSupported"><b>True</b> if Jellyfin supports this version of the plugin.</param>
 | 
				
			||||||
 | 
					        /// <param name="manifest">The manifest record for this plugin, or null if one does not exist.</param>
 | 
				
			||||||
 | 
					        public LocalPlugin(string path, bool isSupported, PluginManifest manifest)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Id = id;
 | 
					 | 
				
			||||||
            Name = name;
 | 
					 | 
				
			||||||
            Version = version;
 | 
					 | 
				
			||||||
            Path = path;
 | 
					            Path = path;
 | 
				
			||||||
            DllFiles = new List<string>();
 | 
					            DllFiles = new List<string>();
 | 
				
			||||||
 | 
					            _supported = isSupported;
 | 
				
			||||||
 | 
					            Manifest = manifest;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the plugin id.
 | 
					        /// Gets the plugin id.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public Guid Id { get; }
 | 
					        public Guid Id => Manifest.Id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the plugin name.
 | 
					        /// Gets the plugin name.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public string Name { get; }
 | 
					        public string Name => Manifest.Name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the plugin version.
 | 
					        /// Gets the plugin version.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public Version Version { get; }
 | 
					        public Version Version
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            get
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                if (_version == null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    _version = Version.Parse(Manifest.Version);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return _version;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the plugin path.
 | 
					        /// Gets the plugin path.
 | 
				
			||||||
@ -51,26 +64,19 @@ namespace MediaBrowser.Common.Plugins
 | 
				
			|||||||
        public List<string> DllFiles { get; }
 | 
					        public List<string> DllFiles { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// == operator.
 | 
					        /// Gets or sets the instance of this plugin.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="left">Left item.</param>
 | 
					        public IPlugin? Instance { get; set; }
 | 
				
			||||||
        /// <param name="right">Right item.</param>
 | 
					 | 
				
			||||||
        /// <returns>Comparison result.</returns>
 | 
					 | 
				
			||||||
        public static bool operator ==(LocalPlugin left, LocalPlugin right)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return left.Equals(right);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// != operator.
 | 
					        /// Gets a value indicating whether Jellyfin supports this version of the plugin, and it's enabled.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="left">Left item.</param>
 | 
					        public bool IsEnabledAndSupported => _supported && Manifest.Status >= PluginStatus.Active;
 | 
				
			||||||
        /// <param name="right">Right item.</param>
 | 
					
 | 
				
			||||||
        /// <returns>Comparison result.</returns>
 | 
					        /// <summary>
 | 
				
			||||||
        public static bool operator !=(LocalPlugin left, LocalPlugin right)
 | 
					        /// Gets a value indicating whether the plugin has a manifest.
 | 
				
			||||||
        {
 | 
					        /// </summary>
 | 
				
			||||||
            return !left.Equals(right);
 | 
					        public PluginManifest Manifest { get; }
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Compare two <see cref="LocalPlugin"/>.
 | 
					        /// Compare two <see cref="LocalPlugin"/>.
 | 
				
			||||||
@ -80,10 +86,15 @@ namespace MediaBrowser.Common.Plugins
 | 
				
			|||||||
        /// <returns>Comparison result.</returns>
 | 
					        /// <returns>Comparison result.</returns>
 | 
				
			||||||
        public static int Compare(LocalPlugin a, LocalPlugin b)
 | 
					        public static int Compare(LocalPlugin a, LocalPlugin b)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
 | 
					            if (a == null || b == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                throw new ArgumentNullException(a == null ? nameof(a) : nameof(b));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var compare = string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Id is not equal but name is.
 | 
					            // Id is not equal but name is.
 | 
				
			||||||
            if (a.Id != b.Id && compare == 0)
 | 
					            if (!a.Id.Equals(b.Id) && compare == 0)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                compare = a.Id.CompareTo(b.Id);
 | 
					                compare = a.Id.CompareTo(b.Id);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -91,8 +102,20 @@ namespace MediaBrowser.Common.Plugins
 | 
				
			|||||||
            return compare == 0 ? a.Version.CompareTo(b.Version) : compare;
 | 
					            return compare == 0 ? a.Version.CompareTo(b.Version) : compare;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Returns the plugin information.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <returns>A <see cref="PluginInfo"/> instance containing the information.</returns>
 | 
				
			||||||
 | 
					        public PluginInfo GetPluginInfo()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var inst = Instance?.GetPluginInfo() ?? new PluginInfo(Manifest.Name, Version, Manifest.Description, Manifest.Id, true);
 | 
				
			||||||
 | 
					            inst.Status = Manifest.Status;
 | 
				
			||||||
 | 
					            inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath);
 | 
				
			||||||
 | 
					            return inst;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public override bool Equals(object obj)
 | 
					        public override bool Equals(object? obj)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            return obj is LocalPlugin other && this.Equals(other);
 | 
					            return obj is LocalPlugin other && this.Equals(other);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -104,16 +127,14 @@ namespace MediaBrowser.Common.Plugins
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <inheritdoc />
 | 
					        /// <inheritdoc />
 | 
				
			||||||
        public bool Equals(LocalPlugin other)
 | 
					        public bool Equals(LocalPlugin? other)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // Do not use == or != for comparison as this class overrides the operators.
 | 
					            if (other == null)
 | 
				
			||||||
            if (object.ReferenceEquals(other, null))
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase)
 | 
					            return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && Id.Equals(other.Id) && Version.Equals(other.Version);
 | 
				
			||||||
                   && Id.Equals(other.Id);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										110
									
								
								MediaBrowser.Common/Plugins/PluginManifest.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								MediaBrowser.Common/Plugins/PluginManifest.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,110 @@
 | 
				
			|||||||
 | 
					#nullable enable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Text.Json.Serialization;
 | 
				
			||||||
 | 
					using MediaBrowser.Model.Plugins;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace MediaBrowser.Common.Plugins
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Defines a Plugin manifest file.
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    public class PluginManifest
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Initializes a new instance of the <see cref="PluginManifest"/> class.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public PluginManifest()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Category = string.Empty;
 | 
				
			||||||
 | 
					            Changelog = string.Empty;
 | 
				
			||||||
 | 
					            Description = string.Empty;
 | 
				
			||||||
 | 
					            Id = Guid.Empty;
 | 
				
			||||||
 | 
					            Name = string.Empty;
 | 
				
			||||||
 | 
					            Owner = string.Empty;
 | 
				
			||||||
 | 
					            Overview = string.Empty;
 | 
				
			||||||
 | 
					            TargetAbi = string.Empty;
 | 
				
			||||||
 | 
					            Version = string.Empty;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the category of the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("category")]
 | 
				
			||||||
 | 
					        public string Category { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the changelog information.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("changelog")]
 | 
				
			||||||
 | 
					        public string Changelog { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the description of the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("description")]
 | 
				
			||||||
 | 
					        public string Description { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the Global Unique Identifier for the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("guid")]
 | 
				
			||||||
 | 
					        public Guid Id { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the Name of the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("name")]
 | 
				
			||||||
 | 
					        public string Name { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets an overview of the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("overview")]
 | 
				
			||||||
 | 
					        public string Overview { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the owner of the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("owner")]
 | 
				
			||||||
 | 
					        public string Owner { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the compatibility version for the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("targetAbi")]
 | 
				
			||||||
 | 
					        public string TargetAbi { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the timestamp of the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("timestamp")]
 | 
				
			||||||
 | 
					        public DateTime Timestamp { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the Version number of the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("version")]
 | 
				
			||||||
 | 
					        public string Version { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets a value indicating the operational status of this plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("status")]
 | 
				
			||||||
 | 
					        public PluginStatus Status { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets a value indicating whether this plugin should automatically update.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("autoUpdate")]
 | 
				
			||||||
 | 
					        public bool AutoUpdate { get; set; } = true; // DO NOT MOVE THIS INTO THE CONSTRUCTOR.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the ImagePath
 | 
				
			||||||
 | 
					        /// Gets or sets a value indicating whether this plugin has an image.
 | 
				
			||||||
 | 
					        /// Image must be located in the local plugin folder.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        [JsonPropertyName("imagePath")]
 | 
				
			||||||
 | 
					        public string? ImagePath { get; set; }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
#pragma warning disable CS1591
 | 
					#nullable enable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
@ -9,6 +9,9 @@ using MediaBrowser.Model.Updates;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Common.Updates
 | 
					namespace MediaBrowser.Common.Updates
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Defines the <see cref="IInstallationManager" />.
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
    public interface IInstallationManager : IDisposable
 | 
					    public interface IInstallationManager : IDisposable
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -21,12 +24,13 @@ namespace MediaBrowser.Common.Updates
 | 
				
			|||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="manifestName">Name of the repository.</param>
 | 
					        /// <param name="manifestName">Name of the repository.</param>
 | 
				
			||||||
        /// <param name="manifest">The URL to query.</param>
 | 
					        /// <param name="manifest">The URL to query.</param>
 | 
				
			||||||
 | 
					        /// <param name="filterIncompatible">Filter out incompatible plugins.</param>
 | 
				
			||||||
        /// <param name="cancellationToken">The cancellation token.</param>
 | 
					        /// <param name="cancellationToken">The cancellation token.</param>
 | 
				
			||||||
        /// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
 | 
					        /// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
 | 
				
			||||||
        Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default);
 | 
					        Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets all available packages.
 | 
					        /// Gets all available packages that are supported by this version.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="cancellationToken">The cancellation token.</param>
 | 
					        /// <param name="cancellationToken">The cancellation token.</param>
 | 
				
			||||||
        /// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
 | 
					        /// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
 | 
				
			||||||
@ -37,33 +41,33 @@ namespace MediaBrowser.Common.Updates
 | 
				
			|||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="availablePackages">The available packages.</param>
 | 
					        /// <param name="availablePackages">The available packages.</param>
 | 
				
			||||||
        /// <param name="name">The name of the plugin.</param>
 | 
					        /// <param name="name">The name of the plugin.</param>
 | 
				
			||||||
        /// <param name="guid">The id of the plugin.</param>
 | 
					        /// <param name="id">The id of the plugin.</param>
 | 
				
			||||||
        /// <param name="specificVersion">The version of the plugin.</param>
 | 
					        /// <param name="specificVersion">The version of the plugin.</param>
 | 
				
			||||||
        /// <returns>All plugins matching the requirements.</returns>
 | 
					        /// <returns>All plugins matching the requirements.</returns>
 | 
				
			||||||
        IEnumerable<PackageInfo> FilterPackages(
 | 
					        IEnumerable<PackageInfo> FilterPackages(
 | 
				
			||||||
            IEnumerable<PackageInfo> availablePackages,
 | 
					            IEnumerable<PackageInfo> availablePackages,
 | 
				
			||||||
            string name = null,
 | 
					            string? name = null,
 | 
				
			||||||
            Guid guid = default,
 | 
					            Guid? id = default,
 | 
				
			||||||
            Version specificVersion = null);
 | 
					            Version? specificVersion = null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Returns all compatible versions ordered from newest to oldest.
 | 
					        /// Returns all compatible versions ordered from newest to oldest.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="availablePackages">The available packages.</param>
 | 
					        /// <param name="availablePackages">The available packages.</param>
 | 
				
			||||||
        /// <param name="name">The name.</param>
 | 
					        /// <param name="name">The name.</param>
 | 
				
			||||||
        /// <param name="guid">The guid of the plugin.</param>
 | 
					        /// <param name="id">The id of the plugin.</param>
 | 
				
			||||||
        /// <param name="minVersion">The minimum required version of the plugin.</param>
 | 
					        /// <param name="minVersion">The minimum required version of the plugin.</param>
 | 
				
			||||||
        /// <param name="specificVersion">The specific version of the plugin to install.</param>
 | 
					        /// <param name="specificVersion">The specific version of the plugin to install.</param>
 | 
				
			||||||
        /// <returns>All compatible versions ordered from newest to oldest.</returns>
 | 
					        /// <returns>All compatible versions ordered from newest to oldest.</returns>
 | 
				
			||||||
        IEnumerable<InstallationInfo> GetCompatibleVersions(
 | 
					        IEnumerable<InstallationInfo> GetCompatibleVersions(
 | 
				
			||||||
            IEnumerable<PackageInfo> availablePackages,
 | 
					            IEnumerable<PackageInfo> availablePackages,
 | 
				
			||||||
            string name = null,
 | 
					            string? name = null,
 | 
				
			||||||
            Guid guid = default,
 | 
					            Guid? id = default,
 | 
				
			||||||
            Version minVersion = null,
 | 
					            Version? minVersion = null,
 | 
				
			||||||
            Version specificVersion = null);
 | 
					            Version? specificVersion = null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Returns the available plugin updates.
 | 
					        /// Returns the available compatible plugin updates.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="cancellationToken">The cancellation token.</param>
 | 
					        /// <param name="cancellationToken">The cancellation token.</param>
 | 
				
			||||||
        /// <returns>The available plugin updates.</returns>
 | 
					        /// <returns>The available plugin updates.</returns>
 | 
				
			||||||
@ -81,7 +85,7 @@ namespace MediaBrowser.Common.Updates
 | 
				
			|||||||
        /// Uninstalls a plugin.
 | 
					        /// Uninstalls a plugin.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="plugin">The plugin.</param>
 | 
					        /// <param name="plugin">The plugin.</param>
 | 
				
			||||||
        void UninstallPlugin(IPlugin plugin);
 | 
					        void UninstallPlugin(LocalPlugin plugin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Cancels the installation.
 | 
					        /// Cancels the installation.
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,21 @@
 | 
				
			|||||||
#pragma warning disable CS1591
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using MediaBrowser.Model.Updates;
 | 
					using MediaBrowser.Model.Updates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Common.Updates
 | 
					namespace MediaBrowser.Common.Updates
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Defines the <see cref="InstallationEventArgs" />.
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
    public class InstallationEventArgs : EventArgs
 | 
					    public class InstallationEventArgs : EventArgs
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the <see cref="InstallationInfo"/>.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
        public InstallationInfo InstallationInfo { get; set; }
 | 
					        public InstallationInfo InstallationInfo { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the <see cref="VersionInfo"/>.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
        public VersionInfo VersionInfo { get; set; }
 | 
					        public VersionInfo VersionInfo { get; set; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,19 @@
 | 
				
			|||||||
using Jellyfin.Data.Events;
 | 
					using Jellyfin.Data.Events;
 | 
				
			||||||
using MediaBrowser.Common.Plugins;
 | 
					using MediaBrowser.Common.Plugins;
 | 
				
			||||||
 | 
					using MediaBrowser.Model.Plugins;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Controller.Events.Updates
 | 
					namespace MediaBrowser.Controller.Events.Updates
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// An event that occurs when a plugin is uninstalled.
 | 
					    /// An event that occurs when a plugin is uninstalled.
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public class PluginUninstalledEventArgs : GenericEventArgs<IPlugin>
 | 
					    public class PluginUninstalledEventArgs : GenericEventArgs<PluginInfo>
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes a new instance of the <see cref="PluginUninstalledEventArgs"/> class.
 | 
					        /// Initializes a new instance of the <see cref="PluginUninstalledEventArgs"/> class.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="arg">The plugin.</param>
 | 
					        /// <param name="arg">The plugin.</param>
 | 
				
			||||||
        public PluginUninstalledEventArgs(IPlugin arg) : base(arg)
 | 
					        public PluginUninstalledEventArgs(PluginInfo arg) : base(arg)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -19,8 +19,6 @@ namespace MediaBrowser.Controller
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        event EventHandler HasUpdateAvailableChanged;
 | 
					        event EventHandler HasUpdateAvailableChanged;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        IServiceProvider ServiceProvider { get; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        bool CoreStartupHasCompleted { get; }
 | 
					        bool CoreStartupHasCompleted { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        bool CanLaunchWebBrowser { get; }
 | 
					        bool CanLaunchWebBrowser { get; }
 | 
				
			||||||
@ -122,13 +120,5 @@ namespace MediaBrowser.Controller
 | 
				
			|||||||
        string ExpandVirtualPath(string path);
 | 
					        string ExpandVirtualPath(string path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        string ReverseVirtualPath(string path);
 | 
					        string ReverseVirtualPath(string path);
 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Gets the list of local plugins.
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        /// <param name="path">Plugin base directory.</param>
 | 
					 | 
				
			||||||
        /// <param name="cleanup">Cleanup old plugins.</param>
 | 
					 | 
				
			||||||
        /// <returns>Enumerable of local plugins.</returns>
 | 
					 | 
				
			||||||
        IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -456,5 +456,15 @@ namespace MediaBrowser.Model.Configuration
 | 
				
			|||||||
        /// Gets or sets the how many metadata refreshes can run concurrently.
 | 
					        /// Gets or sets the how many metadata refreshes can run concurrently.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public int LibraryMetadataRefreshConcurrency { get; set; }
 | 
					        public int LibraryMetadataRefreshConcurrency { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public bool RemoveOldPlugins { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets a value indicating whether plugin image should be disabled.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public bool DisablePluginImages { get; set; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,7 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable enable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using System;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Model.Plugins
 | 
					namespace MediaBrowser.Model.Plugins
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
@ -6,35 +9,47 @@ namespace MediaBrowser.Model.Plugins
 | 
				
			|||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public class PluginInfo
 | 
					    public class PluginInfo
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Initializes a new instance of the <see cref="PluginInfo"/> class.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="name">The plugin name.</param>
 | 
				
			||||||
 | 
					        /// <param name="version">The plugin <see cref="Version"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="description">The plugin description.</param>
 | 
				
			||||||
 | 
					        /// <param name="id">The <see cref="Guid"/>.</param>
 | 
				
			||||||
 | 
					        /// <param name="canUninstall">True if this plugin can be uninstalled.</param>
 | 
				
			||||||
 | 
					        public PluginInfo(string name, Version version, string description, Guid id, bool canUninstall)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Name = name;
 | 
				
			||||||
 | 
					            Version = version;
 | 
				
			||||||
 | 
					            Description = description;
 | 
				
			||||||
 | 
					            Id = id;
 | 
				
			||||||
 | 
					            CanUninstall = canUninstall;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the name.
 | 
					        /// Gets or sets the name.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The name.</value>
 | 
					 | 
				
			||||||
        public string Name { get; set; }
 | 
					        public string Name { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the version.
 | 
					        /// Gets or sets the version.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The version.</value>
 | 
					        public Version Version { get; set; }
 | 
				
			||||||
        public string Version { get; set; }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the name of the configuration file.
 | 
					        /// Gets or sets the name of the configuration file.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The name of the configuration file.</value>
 | 
					        public string? ConfigurationFileName { get; set; }
 | 
				
			||||||
        public string ConfigurationFileName { get; set; }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the description.
 | 
					        /// Gets or sets the description.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The description.</value>
 | 
					 | 
				
			||||||
        public string Description { get; set; }
 | 
					        public string Description { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the unique id.
 | 
					        /// Gets or sets the unique id.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The unique id.</value>
 | 
					        public Guid Id { get; set; }
 | 
				
			||||||
        public string Id { get; set; }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets a value indicating whether the plugin can be uninstalled.
 | 
					        /// Gets or sets a value indicating whether the plugin can be uninstalled.
 | 
				
			||||||
@ -42,9 +57,13 @@ namespace MediaBrowser.Model.Plugins
 | 
				
			|||||||
        public bool CanUninstall { get; set; }
 | 
					        public bool CanUninstall { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the image URL.
 | 
					        /// Gets or sets a value indicating whether this plugin has a valid image.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The image URL.</value>
 | 
					        public bool HasImage { get; set; }
 | 
				
			||||||
        public string ImageUrl { get; set; }
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets a value indicating the status of the plugin.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public PluginStatus Status { get; set; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +1,40 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable enable
 | 
				
			||||||
#pragma warning disable CS1591
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Model.Plugins
 | 
					namespace MediaBrowser.Model.Plugins
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Defines the <see cref="PluginPageInfo" />.
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
    public class PluginPageInfo
 | 
					    public class PluginPageInfo
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        public string Name { get; set; }
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the name.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public string Name { get; set; } = string.Empty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string DisplayName { get; set; }
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the display name.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public string? DisplayName { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string EmbeddedResourcePath { get; set; }
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the resource path.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public string EmbeddedResourcePath { get; set; } = string.Empty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets a value indicating whether this plugin should appear in the main menu.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
        public bool EnableInMainMenu { get; set; }
 | 
					        public bool EnableInMainMenu { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string MenuSection { get; set; }
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the menu section.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public string? MenuSection { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string MenuIcon { get; set; }
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the menu icon.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public string? MenuIcon { get; set; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										47
									
								
								MediaBrowser.Model/Plugins/PluginStatus.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								MediaBrowser.Model/Plugins/PluginStatus.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					namespace MediaBrowser.Model.Plugins
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Plugin load status.
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    public enum PluginStatus
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// This plugin requires a restart in order for it to load. This is a memory only status.
 | 
				
			||||||
 | 
					        /// The actual status of the plugin after reload is present in the manifest.
 | 
				
			||||||
 | 
					        /// eg. A disabled plugin will still be active until the next restart, and so will have a memory status of Restart,
 | 
				
			||||||
 | 
					        /// but a disk manifest status of Disabled.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        Restart = 1,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// This plugin is currently running.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        Active = 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// This plugin has been marked as disabled.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        Disabled = -1,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// This plugin does not meet the TargetAbi requirements.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        NotSupported = -2,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// This plugin caused an error when instantiated. (Either DI loop, or exception)
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        Malfunctioned = -3,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// This plugin has been superceded by another version.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        Superceded = -4,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// An attempt to remove this plugin from disk will happen at every restart.
 | 
				
			||||||
 | 
					        /// It will not be loaded, if unable to do so.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        Deleted = -5
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Text.Json.Serialization;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Model.Updates
 | 
					namespace MediaBrowser.Model.Updates
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -9,10 +10,11 @@ namespace MediaBrowser.Model.Updates
 | 
				
			|||||||
    public class InstallationInfo
 | 
					    public class InstallationInfo
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the guid.
 | 
					        /// Gets or sets the Id.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The guid.</value>
 | 
					        /// <value>The Id.</value>
 | 
				
			||||||
        public Guid Guid { get; set; }
 | 
					        [JsonPropertyName("Guid")]
 | 
				
			||||||
 | 
					        public Guid Id { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the name.
 | 
					        /// Gets or sets the name.
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable enable
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
 | 
					using System.Text.Json.Serialization;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Model.Updates
 | 
					namespace MediaBrowser.Model.Updates
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -9,55 +10,76 @@ namespace MediaBrowser.Model.Updates
 | 
				
			|||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public class PackageInfo
 | 
					    public class PackageInfo
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Initializes a new instance of the <see cref="PackageInfo"/> class.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public PackageInfo()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Versions = Array.Empty<VersionInfo>();
 | 
				
			||||||
 | 
					            Id = string.Empty;
 | 
				
			||||||
 | 
					            Category = string.Empty;
 | 
				
			||||||
 | 
					            Name = string.Empty;
 | 
				
			||||||
 | 
					            Overview = string.Empty;
 | 
				
			||||||
 | 
					            Owner = string.Empty;
 | 
				
			||||||
 | 
					            Description = string.Empty;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the name.
 | 
					        /// Gets or sets the name.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The name.</value>
 | 
					        /// <value>The name.</value>
 | 
				
			||||||
        public string name { get; set; }
 | 
					        [JsonPropertyName("name")]
 | 
				
			||||||
 | 
					        public string Name { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets a long description of the plugin containing features or helpful explanations.
 | 
					        /// Gets or sets a long description of the plugin containing features or helpful explanations.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The description.</value>
 | 
					        /// <value>The description.</value>
 | 
				
			||||||
        public string description { get; set; }
 | 
					        [JsonPropertyName("description")]
 | 
				
			||||||
 | 
					        public string Description { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets a short overview of what the plugin does.
 | 
					        /// Gets or sets a short overview of what the plugin does.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The overview.</value>
 | 
					        /// <value>The overview.</value>
 | 
				
			||||||
        public string overview { get; set; }
 | 
					        [JsonPropertyName("overview")]
 | 
				
			||||||
 | 
					        public string Overview { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the owner.
 | 
					        /// Gets or sets the owner.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The owner.</value>
 | 
					        /// <value>The owner.</value>
 | 
				
			||||||
        public string owner { get; set; }
 | 
					        [JsonPropertyName("owner")]
 | 
				
			||||||
 | 
					        public string Owner { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the category.
 | 
					        /// Gets or sets the category.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The category.</value>
 | 
					        /// <value>The category.</value>
 | 
				
			||||||
        public string category { get; set; }
 | 
					        [JsonPropertyName("category")]
 | 
				
			||||||
 | 
					        public string Category { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// The guid of the assembly associated with this plugin.
 | 
					        /// Gets or sets the guid of the assembly associated with this plugin.
 | 
				
			||||||
        /// This is used to identify the proper item for automatic updates.
 | 
					        /// This is used to identify the proper item for automatic updates.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The name.</value>
 | 
					        /// <value>The name.</value>
 | 
				
			||||||
        public string guid { get; set; }
 | 
					        [JsonPropertyName("guid")]
 | 
				
			||||||
 | 
					        public string Id { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the versions.
 | 
					        /// Gets or sets the versions.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The versions.</value>
 | 
					        /// <value>The versions.</value>
 | 
				
			||||||
        public IList<VersionInfo> versions { get; set; }
 | 
					        [JsonPropertyName("versions")]
 | 
				
			||||||
 | 
					#pragma warning disable CA2227 // Collection properties should be read only
 | 
				
			||||||
 | 
					        public IList<VersionInfo> Versions { get; set; }
 | 
				
			||||||
 | 
					#pragma warning restore CA2227 // Collection properties should be read only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes a new instance of the <see cref="PackageInfo"/> class.
 | 
					        /// Gets or sets the image url for the package.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public PackageInfo()
 | 
					        [JsonPropertyName("imageUrl")]
 | 
				
			||||||
        {
 | 
					        public string? ImageUrl { get; set; }
 | 
				
			||||||
            versions = Array.Empty<VersionInfo>();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,76 +1,79 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable enable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System.Text.Json.Serialization;
 | 
				
			||||||
 | 
					using SysVersion = System.Version;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Model.Updates
 | 
					namespace MediaBrowser.Model.Updates
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// Class PackageVersionInfo.
 | 
					    /// Defines the <see cref="VersionInfo"/> class.
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public class VersionInfo
 | 
					    public class VersionInfo
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private Version _version;
 | 
					        private SysVersion? _version;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the version.
 | 
					        /// Gets or sets the version.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The version.</value>
 | 
					        /// <value>The version.</value>
 | 
				
			||||||
        public string version
 | 
					        [JsonPropertyName("version")]
 | 
				
			||||||
 | 
					        public string Version
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            get
 | 
					            get => _version == null ? string.Empty : _version.ToString();
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                return _version == null ? string.Empty : _version.ToString();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            set
 | 
					            set => _version = SysVersion.Parse(value);
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                _version = Version.Parse(value);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the version as a <see cref="Version"/>.
 | 
					        /// Gets the version as a <see cref="SysVersion"/>.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public Version VersionNumber => _version;
 | 
					        public SysVersion VersionNumber => _version ?? new SysVersion(0, 0, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the changelog for this version.
 | 
					        /// Gets or sets the changelog for this version.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The changelog.</value>
 | 
					        /// <value>The changelog.</value>
 | 
				
			||||||
        public string changelog { get; set; }
 | 
					        [JsonPropertyName("changelog")]
 | 
				
			||||||
 | 
					        public string? Changelog { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the ABI that this version was built against.
 | 
					        /// Gets or sets the ABI that this version was built against.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The target ABI version.</value>
 | 
					        /// <value>The target ABI version.</value>
 | 
				
			||||||
        public string targetAbi { get; set; }
 | 
					        [JsonPropertyName("targetAbi")]
 | 
				
			||||||
 | 
					        public string? TargetAbi { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the source URL.
 | 
					        /// Gets or sets the source URL.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The source URL.</value>
 | 
					        /// <value>The source URL.</value>
 | 
				
			||||||
        public string sourceUrl { get; set; }
 | 
					        [JsonPropertyName("sourceUrl")]
 | 
				
			||||||
 | 
					        public string? SourceUrl { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets a checksum for the binary.
 | 
					        /// Gets or sets a checksum for the binary.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The checksum.</value>
 | 
					        /// <value>The checksum.</value>
 | 
				
			||||||
        public string checksum { get; set; }
 | 
					        [JsonPropertyName("checksum")]
 | 
				
			||||||
 | 
					        public string? Checksum { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets a timestamp of when the binary was built.
 | 
					        /// Gets or sets a timestamp of when the binary was built.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <value>The timestamp.</value>
 | 
					        /// <value>The timestamp.</value>
 | 
				
			||||||
        public string timestamp { get; set; }
 | 
					        [JsonPropertyName("timestamp")]
 | 
				
			||||||
 | 
					        public string? Timestamp { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the repository name.
 | 
					        /// Gets or sets the repository name.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public string repositoryName { get; set; }
 | 
					        [JsonPropertyName("repositoryName")]
 | 
				
			||||||
 | 
					        public string RepositoryName { get; set; } = string.Empty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the repository url.
 | 
					        /// Gets or sets the repository url.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public string repositoryUrl { get; set; }
 | 
					        [JsonPropertyName("repositoryUrl")]
 | 
				
			||||||
 | 
					        public string RepositoryUrl { get; set; } = string.Empty;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
#pragma warning disable CS1591
 | 
					#pragma warning disable CS1591
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
#pragma warning disable CS1591
 | 
					#pragma warning disable CS1591
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
#pragma warning disable CS1591
 | 
					#pragma warning disable CS1591
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
 | 
					Microsoft Visual Studio Solution File, Format Version 12.00
 | 
				
			||||||
# Visual Studio Version 16
 | 
					# Visual Studio Version 16
 | 
				
			||||||
VisualStudioVersion = 16.0.30503.244
 | 
					VisualStudioVersion = 16.0.30503.244
 | 
				
			||||||
MinimumVisualStudioVersion = 10.0.40219.1
 | 
					MinimumVisualStudioVersion = 10.0.40219.1
 | 
				
			||||||
@ -70,7 +70,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jell
 | 
				
			|||||||
EndProject
 | 
					EndProject
 | 
				
			||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
 | 
					Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
 | 
				
			||||||
EndProject
 | 
					EndProject
 | 
				
			||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
 | 
					Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
 | 
				
			||||||
EndProject
 | 
					EndProject
 | 
				
			||||||
Global
 | 
					Global
 | 
				
			||||||
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 | 
						GlobalSection(SolutionConfigurationPlatforms) = preSolution
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user