mirror of
https://github.com/Lidarr/Lidarr
synced 2025-12-06 00:16:41 +01:00
New: Improve Plugin Installation and Removal Process
Fixes restart loops reduces github bans improves UX with messaging for restart improves version notes
This commit is contained in:
parent
08a84b6b70
commit
c79acf328a
9 changed files with 292 additions and 65 deletions
|
|
@ -7,11 +7,13 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
|||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import PluginRow from './PluginRow';
|
||||
|
||||
const columns = [
|
||||
|
|
@ -78,7 +80,15 @@ class Plugins extends Component {
|
|||
isInstallingPlugin,
|
||||
onInstallPluginPress,
|
||||
isUninstallingPlugin,
|
||||
onUninstallPluginPress
|
||||
onUninstallPluginPress,
|
||||
isRestartRequiredModalOpen,
|
||||
onCloseRestartRequiredModal,
|
||||
pluginOwner,
|
||||
pluginName,
|
||||
pluginVersion,
|
||||
pluginAction,
|
||||
pluginDetailsUrl,
|
||||
pluginBranch
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
|
|
@ -87,6 +97,23 @@ class Plugins extends Component {
|
|||
|
||||
const noPlugins = isPopulated && !error && !items.length;
|
||||
|
||||
// Build modal title and message
|
||||
let modalTitle = translate('RestartRequired');
|
||||
let modalMessage = translate('LidarrRequiresRestartToApplyPluginChanges');
|
||||
|
||||
if (pluginOwner && pluginName && pluginAction) {
|
||||
const versionText = pluginVersion ? ` v${pluginVersion}` : '';
|
||||
const branchText = pluginBranch ? ` (${pluginBranch})` : '';
|
||||
const actionText = pluginAction === 'install' ? 'installed' : 'uninstalled';
|
||||
modalTitle = `Plugin ${actionText} - ${translate('RestartRequired')}`;
|
||||
|
||||
if (pluginDetailsUrl) {
|
||||
modalMessage = `Plugin: [${pluginOwner}/${pluginName}]${versionText}${branchText}\n\nInstalled from:\n${pluginDetailsUrl}\n\nPlease restart Lidarr to apply changes.`;
|
||||
} else {
|
||||
modalMessage = `Plugin: [${pluginOwner}/${pluginName}]${versionText}${branchText}\n\nPlugin ${actionText} successfully.\n\nPlease restart Lidarr to apply changes.`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContent title="Plugins">
|
||||
<PageContentBody>
|
||||
|
|
@ -148,6 +175,17 @@ class Plugins extends Component {
|
|||
</Table>
|
||||
}
|
||||
</FieldSet>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isRestartRequiredModalOpen}
|
||||
kind={kinds.INFO}
|
||||
title={modalTitle}
|
||||
message={modalMessage}
|
||||
confirmLabel={translate('Ok')}
|
||||
hideCancelButton={true}
|
||||
onConfirm={onCloseRestartRequiredModal}
|
||||
onCancel={onCloseRestartRequiredModal}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
|
@ -162,7 +200,15 @@ Plugins.propTypes = {
|
|||
isInstallingPlugin: PropTypes.bool.isRequired,
|
||||
onInstallPluginPress: PropTypes.func.isRequired,
|
||||
isUninstallingPlugin: PropTypes.bool.isRequired,
|
||||
onUninstallPluginPress: PropTypes.func.isRequired
|
||||
onUninstallPluginPress: PropTypes.func.isRequired,
|
||||
isRestartRequiredModalOpen: PropTypes.bool,
|
||||
onCloseRestartRequiredModal: PropTypes.func,
|
||||
pluginOwner: PropTypes.string,
|
||||
pluginName: PropTypes.string,
|
||||
pluginVersion: PropTypes.string,
|
||||
pluginAction: PropTypes.string,
|
||||
pluginDetailsUrl: PropTypes.string,
|
||||
pluginBranch: PropTypes.string
|
||||
};
|
||||
|
||||
export default Plugins;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,20 @@ class PluginsConnector extends Component {
|
|||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRestartRequiredModalOpen: false,
|
||||
pluginOwner: '',
|
||||
pluginName: '',
|
||||
pluginVersion: '',
|
||||
pluginAction: '',
|
||||
pluginDetailsUrl: '',
|
||||
pluginBranch: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
|
|
@ -59,27 +73,108 @@ class PluginsConnector extends Component {
|
|||
// Listeners
|
||||
|
||||
onInstallPluginPress = (url) => {
|
||||
this.currentPluginOperation = { action: 'install', url };
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.INSTALL_PLUGIN,
|
||||
githubUrl: url
|
||||
githubUrl: url,
|
||||
commandFinished: this.onPluginCommandFinished
|
||||
});
|
||||
};
|
||||
|
||||
onUninstallPluginPress = (url) => {
|
||||
this.currentPluginOperation = { action: 'uninstall', url };
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.UNINSTALL_PLUGIN,
|
||||
githubUrl: url
|
||||
githubUrl: url,
|
||||
commandFinished: this.onPluginCommandFinished
|
||||
});
|
||||
};
|
||||
|
||||
onPluginCommandFinished = (command) => {
|
||||
let pluginOwner = '';
|
||||
let pluginName = '';
|
||||
let pluginVersion = '';
|
||||
let pluginAction = '';
|
||||
let pluginDetailsUrl = '';
|
||||
let pluginBranch = '';
|
||||
|
||||
if (this.currentPluginOperation && command) {
|
||||
const url = this.currentPluginOperation.url;
|
||||
|
||||
const match = url.match(/github\.com\/([^/]+)\/([^/]+)/);
|
||||
if (match) {
|
||||
[, pluginOwner, pluginName] = match;
|
||||
pluginAction = this.currentPluginOperation.action;
|
||||
|
||||
// Extract branch from GitHub URL
|
||||
if (url.includes('/tree/')) {
|
||||
const branchMatch = url.match(/\/tree\/([^/]+)/);
|
||||
if (branchMatch) {
|
||||
pluginBranch = branchMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentPluginOperation.action === 'install') {
|
||||
if (command && command.message) {
|
||||
const pluginMatch = command.message.match(/Plugin \[([^/]+)\/([^\]]+)\] v([0-9.]+) installed/);
|
||||
if (pluginMatch) {
|
||||
pluginVersion = pluginMatch[3];
|
||||
}
|
||||
console.log('Plugin match result:', pluginMatch);
|
||||
}
|
||||
pluginDetailsUrl = url;
|
||||
} else {
|
||||
if (command && command.message) {
|
||||
const pluginMatch = command.message.match(/Plugin \[([^/]+)\/([^\]]+)\] v([0-9.]+) uninstalled/);
|
||||
if (pluginMatch) {
|
||||
pluginVersion = pluginMatch[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isRestartRequiredModalOpen: true,
|
||||
pluginOwner,
|
||||
pluginName,
|
||||
pluginVersion,
|
||||
pluginAction,
|
||||
pluginDetailsUrl,
|
||||
pluginBranch
|
||||
});
|
||||
this.repopulate();
|
||||
};
|
||||
|
||||
onCloseRestartRequiredModal = () => {
|
||||
this.setState({
|
||||
isRestartRequiredModalOpen: false,
|
||||
pluginOwner: '',
|
||||
pluginName: '',
|
||||
pluginVersion: '',
|
||||
pluginAction: '',
|
||||
pluginDetailsUrl: '',
|
||||
pluginBranch: ''
|
||||
});
|
||||
this.currentPluginOperation = null;
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Plugins
|
||||
isRestartRequiredModalOpen={this.state.isRestartRequiredModalOpen}
|
||||
pluginOwner={this.state.pluginOwner}
|
||||
pluginName={this.state.pluginName}
|
||||
pluginVersion={this.state.pluginVersion}
|
||||
pluginAction={this.state.pluginAction}
|
||||
pluginDetailsUrl={this.state.pluginDetailsUrl}
|
||||
pluginBranch={this.state.pluginBranch}
|
||||
onInstallPluginPress={this.onInstallPluginPress}
|
||||
onUninstallPluginPress={this.onUninstallPluginPress}
|
||||
onCloseRestartRequiredModal={this.onCloseRestartRequiredModal}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -89,7 +184,8 @@ class PluginsConnector extends Component {
|
|||
|
||||
PluginsConnector.propTypes = {
|
||||
dispatchFetchInstalledPlugins: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||
items: PropTypes.array
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(PluginsConnector);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Lidarr.Http.REST;
|
||||
|
|
@ -17,6 +18,21 @@ public class PluginResource : RestResource
|
|||
|
||||
public static class PluginResourceMapper
|
||||
{
|
||||
private static string FormatVersion(Version version)
|
||||
{
|
||||
if (version == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Always show 4-part version for UI consistency, handling undefined components
|
||||
var major = version.Major;
|
||||
var minor = version.Minor;
|
||||
var build = version.Build == -1 ? 0 : version.Build;
|
||||
var revision = version.Revision == -1 ? 0 : version.Revision;
|
||||
return $"{major}.{minor}.{build}.{revision}";
|
||||
}
|
||||
|
||||
public static PluginResource ToResource(this IPlugin plugin)
|
||||
{
|
||||
return new PluginResource
|
||||
|
|
@ -24,8 +40,8 @@ public static PluginResource ToResource(this IPlugin plugin)
|
|||
Name = plugin.Name,
|
||||
Owner = plugin.Owner,
|
||||
GithubUrl = plugin.GithubUrl,
|
||||
InstalledVersion = plugin.InstalledVersion.ToString(),
|
||||
AvailableVersion = plugin.AvailableVersion.ToString(),
|
||||
InstalledVersion = FormatVersion(plugin.InstalledVersion),
|
||||
AvailableVersion = FormatVersion(plugin.AvailableVersion),
|
||||
UpdateAvailable = plugin.AvailableVersion > plugin.InstalledVersion
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -678,6 +678,7 @@
|
|||
"LastSearched": "Last Searched",
|
||||
"LastUsed": "Last Used",
|
||||
"LastWriteTime": "Last Write Time",
|
||||
"LidarrRequiresRestartToApplyPluginChanges": "Lidarr requires a restart to apply plugin changes",
|
||||
"LatestAlbum": "Latest Album",
|
||||
"LatestAlbumData": "Monitor the latest albums and future albums",
|
||||
"LaunchBrowserHelpText": " Open a web browser and navigate to {appName} homepage on app start.",
|
||||
|
|
@ -914,6 +915,7 @@
|
|||
"Period": "Period",
|
||||
"Permissions": "Permissions",
|
||||
"Playlist": "Playlist",
|
||||
"Plugins": "Plugins",
|
||||
"Port": "Port",
|
||||
"PortNumber": "Port Number",
|
||||
"PostImportCategory": "Post-Import Category",
|
||||
|
|
@ -1082,6 +1084,7 @@
|
|||
"Restart": "Restart",
|
||||
"RestartLidarr": "Restart {appName}",
|
||||
"RestartNow": "Restart Now",
|
||||
"RestartRequired": "Restart Required",
|
||||
"RestartRequiredHelpTextWarning": "Requires restart to take effect",
|
||||
"Restore": "Restore",
|
||||
"RestoreBackup": "Restore Backup",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,5 @@ public class InstallPluginCommand : Command
|
|||
|
||||
public override bool SendUpdatesToClient => true;
|
||||
public override bool IsExclusive => true;
|
||||
public override string CompletionMessage => null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,5 @@ public class UninstallPluginCommand : Command
|
|||
|
||||
public override bool SendUpdatesToClient => true;
|
||||
public override bool IsExclusive => true;
|
||||
public override string CompletionMessage => null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Disk;
|
||||
|
|
@ -7,7 +8,6 @@
|
|||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Plugins.Commands;
|
||||
|
||||
|
|
@ -20,7 +20,6 @@ public class InstallPluginService : IExecute<InstallPluginCommand>, IExecute<Uni
|
|||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IArchiveService _archiveService;
|
||||
private readonly ILifecycleService _lifecycleService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public InstallPluginService(IPluginService pluginService,
|
||||
|
|
@ -28,7 +27,6 @@ public InstallPluginService(IPluginService pluginService,
|
|||
IAppFolderInfo appFolderInfo,
|
||||
IHttpClient httpClient,
|
||||
IArchiveService archiveService,
|
||||
ILifecycleService lifecycleService,
|
||||
Logger logger)
|
||||
{
|
||||
_pluginService = pluginService;
|
||||
|
|
@ -36,14 +34,19 @@ public InstallPluginService(IPluginService pluginService,
|
|||
_appFolderInfo = appFolderInfo;
|
||||
_httpClient = httpClient;
|
||||
_archiveService = archiveService;
|
||||
_lifecycleService = lifecycleService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Execute(UninstallPluginCommand message)
|
||||
{
|
||||
var (owner, name) = _pluginService.ParseUrl(message.GithubUrl);
|
||||
UninstallPlugin(owner, name);
|
||||
|
||||
// Get installed version before uninstalling
|
||||
var installedPlugins = _pluginService.GetInstalledPlugins();
|
||||
var installedPlugin = installedPlugins.FirstOrDefault(p => p.Owner == owner && p.Name == name);
|
||||
var version = installedPlugin?.InstalledVersion;
|
||||
|
||||
UninstallPlugin(owner, name, version);
|
||||
}
|
||||
|
||||
public void Execute(InstallPluginCommand message)
|
||||
|
|
@ -67,24 +70,30 @@ private void InstallPlugin(RemotePlugin package)
|
|||
}
|
||||
|
||||
var packageDestination = Path.Combine(tempFolder, $"{package.Name}.zip");
|
||||
|
||||
_logger.ProgressInfo($"Downloading plugin {package.Name}");
|
||||
var packageTitle = $"{package.Owner}/{package.Name} v{package.Version}";
|
||||
_logger.ProgressInfo($"Downloading plugin [{packageTitle}]");
|
||||
_httpClient.DownloadFile(package.PackageUrl, packageDestination);
|
||||
|
||||
_logger.ProgressInfo("Extracting Plugin package");
|
||||
_logger.ProgressInfo($"Extracting plugin [{packageTitle}]");
|
||||
_archiveService.Extract(packageDestination, Path.Combine(PluginFolder(), package.Owner, package.Name));
|
||||
_logger.ProgressInfo($"Installed {package.Name}, restarting");
|
||||
|
||||
Task.Factory.StartNew(() => _lifecycleService.Restart());
|
||||
_logger.ProgressInfo($"Plugin [{package.Owner}/{package.Name}] v{package.Version} installed. Please restart Lidarr.");
|
||||
}
|
||||
|
||||
private void UninstallPlugin(string owner, string name)
|
||||
private void UninstallPlugin(string owner, string name, Version version)
|
||||
{
|
||||
_logger.ProgressInfo($"Uninstalling Plugin {owner}/{name}");
|
||||
_diskProvider.DeleteFolder(Path.Combine(PluginFolder(), owner, name), true);
|
||||
_logger.ProgressInfo($"Uninstalled Plugin {owner}/{name}, restarting");
|
||||
_logger.ProgressInfo($"Uninstalling plugin [{owner}/{name}]");
|
||||
var pluginFolder = Path.Combine(PluginFolder(), owner, name);
|
||||
_logger.Debug("Deleting folder: {0}", pluginFolder);
|
||||
_diskProvider.DeleteFolder(pluginFolder, true);
|
||||
|
||||
Task.Factory.StartNew(() => _lifecycleService.Restart());
|
||||
if (version != null)
|
||||
{
|
||||
_logger.ProgressInfo($"Plugin [{owner}/{name}] v{version} uninstalled. Please restart Lidarr.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.ProgressInfo($"Plugin [{owner}/{name}] uninstalled. Please restart Lidarr.");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsurePluginFolder()
|
||||
|
|
|
|||
|
|
@ -52,51 +52,69 @@ public PluginService(IHttpClient httpClient,
|
|||
|
||||
public RemotePlugin GetRemotePlugin(string repoUrl)
|
||||
{
|
||||
var (owner, name) = ParseUrl(repoUrl);
|
||||
var releaseUrl = $"https://api.github.com/repos/{owner}/{name}/releases";
|
||||
|
||||
var releases = _httpClient.Get<List<Release>>(new HttpRequest(releaseUrl)).Resource;
|
||||
|
||||
if (!releases?.Any() ?? true)
|
||||
try
|
||||
{
|
||||
_logger.Warn($"No releases found for {name}");
|
||||
var (owner, name) = ParseUrl(repoUrl);
|
||||
var releaseUrl = $"https://api.github.com/repos/{owner}/{name}/releases";
|
||||
|
||||
var releases = _httpClient.Get<List<Release>>(new HttpRequest(releaseUrl)).Resource;
|
||||
|
||||
if (!releases?.Any() ?? true)
|
||||
{
|
||||
_logger.Warn($"No releases found for {name}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var latest = releases.OrderByDescending(x => x.PublishedAt).FirstOrDefault(x => IsSupported(x));
|
||||
|
||||
if (latest == null)
|
||||
{
|
||||
_logger.Warn($"Plugin {name} requires newer version of Lidarr");
|
||||
return null;
|
||||
}
|
||||
|
||||
var version = Version.Parse(latest.TagName.TrimStart('v'));
|
||||
var framework = "net6.0";
|
||||
var asset = latest.Assets.FirstOrDefault(x => x.Name.EndsWith($"{framework}.zip"));
|
||||
|
||||
if (asset == null)
|
||||
{
|
||||
_logger.Warn($"No plugin package found for {framework} for {name}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RemotePlugin
|
||||
{
|
||||
GithubUrl = repoUrl,
|
||||
Name = name,
|
||||
Owner = owner,
|
||||
Version = version,
|
||||
PackageUrl = asset.BrowserDownloadUrl
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to get remote plugin information for {0}", repoUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
var latest = releases.OrderByDescending(x => x.PublishedAt).FirstOrDefault(x => IsSupported(x));
|
||||
|
||||
if (latest == null)
|
||||
{
|
||||
_logger.Warn($"Plugin {name} requires newer version of Lidarr");
|
||||
return null;
|
||||
}
|
||||
|
||||
var version = Version.Parse(latest.TagName.TrimStart('v'));
|
||||
var framework = "net6.0";
|
||||
var asset = latest.Assets.FirstOrDefault(x => x.Name.EndsWith($"{framework}.zip"));
|
||||
|
||||
if (asset == null)
|
||||
{
|
||||
_logger.Warn($"No plugin package found for {framework} for {name}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RemotePlugin
|
||||
{
|
||||
GithubUrl = repoUrl,
|
||||
Name = name,
|
||||
Owner = owner,
|
||||
Version = version,
|
||||
PackageUrl = asset.BrowserDownloadUrl
|
||||
};
|
||||
}
|
||||
|
||||
public List<IPlugin> GetInstalledPlugins()
|
||||
{
|
||||
foreach (var plugin in _installedPlugins)
|
||||
{
|
||||
var remote = GetRemotePlugin(plugin.GithubUrl);
|
||||
plugin.AvailableVersion = remote.Version;
|
||||
try
|
||||
{
|
||||
var remote = GetRemotePlugin(plugin.GithubUrl);
|
||||
if (remote != null)
|
||||
{
|
||||
plugin.AvailableVersion = remote.Version;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to check for updates for plugin {0}/{1}", plugin.Owner, plugin.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return _installedPlugins;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Lifecycle.Commands;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Plugins.Commands;
|
||||
using NzbDrone.Integration.Test.Client;
|
||||
|
|
@ -16,11 +17,13 @@ public class PluginFixture : IntegrationTest
|
|||
[Order(0)]
|
||||
public void should_install_plugin()
|
||||
{
|
||||
PostAndWaitForRestart(new InstallPluginCommand
|
||||
PostAndWaitForCompletion(new InstallPluginCommand
|
||||
{
|
||||
GithubUrl = "https://github.com/ta264/Lidarr.Plugin.Deemix"
|
||||
});
|
||||
|
||||
PostAndWaitForRestart(new RestartCommand());
|
||||
|
||||
WaitForRestart();
|
||||
|
||||
var plugins = Plugins.All();
|
||||
|
|
@ -36,18 +39,19 @@ public void should_uninstall_plugin()
|
|||
plugins.Should().HaveCount(1);
|
||||
plugins[0].Name.Should().Be("Deemix");
|
||||
|
||||
PostAndWaitForRestart(new UninstallPluginCommand
|
||||
PostAndWaitForCompletion(new UninstallPluginCommand
|
||||
{
|
||||
GithubUrl = "https://github.com/ta264/Lidarr.Plugin.Deemix"
|
||||
});
|
||||
|
||||
PostAndWaitForRestart(new RestartCommand());
|
||||
|
||||
WaitForRestart();
|
||||
|
||||
plugins = Plugins.All();
|
||||
plugins.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// Installing / uninstalling triggers a restart, so manually shutdown the restarted app
|
||||
[OneTimeTearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
|
|
@ -57,6 +61,43 @@ public void TearDown()
|
|||
RestClient.Execute(request);
|
||||
}
|
||||
|
||||
private SimpleCommandResource PostAndWaitForCompletion<T>(T command)
|
||||
where T : Command, new()
|
||||
{
|
||||
var request = new RestRequest("command");
|
||||
request.Method = Method.POST;
|
||||
request.AddHeader("Authorization", ApiKey);
|
||||
request.AddJsonBody(command);
|
||||
|
||||
var result = RestClient.Execute(request);
|
||||
var resource = Json.Deserialize<SimpleCommandResource>(result.Content);
|
||||
|
||||
var id = resource.Id;
|
||||
|
||||
id.Should().NotBe(0);
|
||||
|
||||
for (var i = 0; i < 50; i++)
|
||||
{
|
||||
if (resource?.Status == CommandStatus.Completed)
|
||||
{
|
||||
return resource;
|
||||
}
|
||||
|
||||
var get = new RestRequest($"command/{id}");
|
||||
get.AddHeader("Authorization", ApiKey);
|
||||
|
||||
result = RestClient.Execute(get);
|
||||
|
||||
TestContext.Progress.WriteLine("Waiting for command to finish : {0} [{1}] {2}\n{3}", result.ResponseStatus, result.StatusDescription, result.ErrorException?.Message, result.Content);
|
||||
|
||||
resource = Json.Deserialize<SimpleCommandResource>(result.Content);
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
|
||||
Assert.Fail("Command failed");
|
||||
return resource;
|
||||
}
|
||||
|
||||
private SimpleCommandResource PostAndWaitForRestart<T>(T command)
|
||||
where T : Command, new()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue