mirror of
https://github.com/Lidarr/Lidarr
synced 2025-12-06 08:25:54 +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 FormLabel from 'Components/Form/FormLabel';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import PluginRow from './PluginRow';
|
import PluginRow from './PluginRow';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
|
@ -78,7 +80,15 @@ class Plugins extends Component {
|
||||||
isInstallingPlugin,
|
isInstallingPlugin,
|
||||||
onInstallPluginPress,
|
onInstallPluginPress,
|
||||||
isUninstallingPlugin,
|
isUninstallingPlugin,
|
||||||
onUninstallPluginPress
|
onUninstallPluginPress,
|
||||||
|
isRestartRequiredModalOpen,
|
||||||
|
onCloseRestartRequiredModal,
|
||||||
|
pluginOwner,
|
||||||
|
pluginName,
|
||||||
|
pluginVersion,
|
||||||
|
pluginAction,
|
||||||
|
pluginDetailsUrl,
|
||||||
|
pluginBranch
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -87,6 +97,23 @@ class Plugins extends Component {
|
||||||
|
|
||||||
const noPlugins = isPopulated && !error && !items.length;
|
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 (
|
return (
|
||||||
<PageContent title="Plugins">
|
<PageContent title="Plugins">
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
|
|
@ -148,6 +175,17 @@ class Plugins extends Component {
|
||||||
</Table>
|
</Table>
|
||||||
}
|
}
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isRestartRequiredModalOpen}
|
||||||
|
kind={kinds.INFO}
|
||||||
|
title={modalTitle}
|
||||||
|
message={modalMessage}
|
||||||
|
confirmLabel={translate('Ok')}
|
||||||
|
hideCancelButton={true}
|
||||||
|
onConfirm={onCloseRestartRequiredModal}
|
||||||
|
onCancel={onCloseRestartRequiredModal}
|
||||||
|
/>
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|
@ -162,7 +200,15 @@ Plugins.propTypes = {
|
||||||
isInstallingPlugin: PropTypes.bool.isRequired,
|
isInstallingPlugin: PropTypes.bool.isRequired,
|
||||||
onInstallPluginPress: PropTypes.func.isRequired,
|
onInstallPluginPress: PropTypes.func.isRequired,
|
||||||
isUninstallingPlugin: PropTypes.bool.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;
|
export default Plugins;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,20 @@ class PluginsConnector extends Component {
|
||||||
//
|
//
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isRestartRequiredModalOpen: false,
|
||||||
|
pluginOwner: '',
|
||||||
|
pluginName: '',
|
||||||
|
pluginVersion: '',
|
||||||
|
pluginAction: '',
|
||||||
|
pluginDetailsUrl: '',
|
||||||
|
pluginBranch: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
registerPagePopulator(this.repopulate);
|
registerPagePopulator(this.repopulate);
|
||||||
|
|
||||||
|
|
@ -59,27 +73,108 @@ class PluginsConnector extends Component {
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onInstallPluginPress = (url) => {
|
onInstallPluginPress = (url) => {
|
||||||
|
this.currentPluginOperation = { action: 'install', url };
|
||||||
this.props.dispatchExecuteCommand({
|
this.props.dispatchExecuteCommand({
|
||||||
name: commandNames.INSTALL_PLUGIN,
|
name: commandNames.INSTALL_PLUGIN,
|
||||||
githubUrl: url
|
githubUrl: url,
|
||||||
|
commandFinished: this.onPluginCommandFinished
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onUninstallPluginPress = (url) => {
|
onUninstallPluginPress = (url) => {
|
||||||
|
this.currentPluginOperation = { action: 'uninstall', url };
|
||||||
this.props.dispatchExecuteCommand({
|
this.props.dispatchExecuteCommand({
|
||||||
name: commandNames.UNINSTALL_PLUGIN,
|
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
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Plugins
|
<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}
|
onInstallPluginPress={this.onInstallPluginPress}
|
||||||
onUninstallPluginPress={this.onUninstallPluginPress}
|
onUninstallPluginPress={this.onUninstallPluginPress}
|
||||||
|
onCloseRestartRequiredModal={this.onCloseRestartRequiredModal}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -89,7 +184,8 @@ class PluginsConnector extends Component {
|
||||||
|
|
||||||
PluginsConnector.propTypes = {
|
PluginsConnector.propTypes = {
|
||||||
dispatchFetchInstalledPlugins: PropTypes.func.isRequired,
|
dispatchFetchInstalledPlugins: PropTypes.func.isRequired,
|
||||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||||
|
items: PropTypes.array
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(PluginsConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(PluginsConnector);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Lidarr.Http.REST;
|
using Lidarr.Http.REST;
|
||||||
|
|
@ -17,6 +18,21 @@ public class PluginResource : RestResource
|
||||||
|
|
||||||
public static class PluginResourceMapper
|
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)
|
public static PluginResource ToResource(this IPlugin plugin)
|
||||||
{
|
{
|
||||||
return new PluginResource
|
return new PluginResource
|
||||||
|
|
@ -24,8 +40,8 @@ public static PluginResource ToResource(this IPlugin plugin)
|
||||||
Name = plugin.Name,
|
Name = plugin.Name,
|
||||||
Owner = plugin.Owner,
|
Owner = plugin.Owner,
|
||||||
GithubUrl = plugin.GithubUrl,
|
GithubUrl = plugin.GithubUrl,
|
||||||
InstalledVersion = plugin.InstalledVersion.ToString(),
|
InstalledVersion = FormatVersion(plugin.InstalledVersion),
|
||||||
AvailableVersion = plugin.AvailableVersion.ToString(),
|
AvailableVersion = FormatVersion(plugin.AvailableVersion),
|
||||||
UpdateAvailable = plugin.AvailableVersion > plugin.InstalledVersion
|
UpdateAvailable = plugin.AvailableVersion > plugin.InstalledVersion
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -678,6 +678,7 @@
|
||||||
"LastSearched": "Last Searched",
|
"LastSearched": "Last Searched",
|
||||||
"LastUsed": "Last Used",
|
"LastUsed": "Last Used",
|
||||||
"LastWriteTime": "Last Write Time",
|
"LastWriteTime": "Last Write Time",
|
||||||
|
"LidarrRequiresRestartToApplyPluginChanges": "Lidarr requires a restart to apply plugin changes",
|
||||||
"LatestAlbum": "Latest Album",
|
"LatestAlbum": "Latest Album",
|
||||||
"LatestAlbumData": "Monitor the latest albums and future albums",
|
"LatestAlbumData": "Monitor the latest albums and future albums",
|
||||||
"LaunchBrowserHelpText": " Open a web browser and navigate to {appName} homepage on app start.",
|
"LaunchBrowserHelpText": " Open a web browser and navigate to {appName} homepage on app start.",
|
||||||
|
|
@ -914,6 +915,7 @@
|
||||||
"Period": "Period",
|
"Period": "Period",
|
||||||
"Permissions": "Permissions",
|
"Permissions": "Permissions",
|
||||||
"Playlist": "Playlist",
|
"Playlist": "Playlist",
|
||||||
|
"Plugins": "Plugins",
|
||||||
"Port": "Port",
|
"Port": "Port",
|
||||||
"PortNumber": "Port Number",
|
"PortNumber": "Port Number",
|
||||||
"PostImportCategory": "Post-Import Category",
|
"PostImportCategory": "Post-Import Category",
|
||||||
|
|
@ -1082,6 +1084,7 @@
|
||||||
"Restart": "Restart",
|
"Restart": "Restart",
|
||||||
"RestartLidarr": "Restart {appName}",
|
"RestartLidarr": "Restart {appName}",
|
||||||
"RestartNow": "Restart Now",
|
"RestartNow": "Restart Now",
|
||||||
|
"RestartRequired": "Restart Required",
|
||||||
"RestartRequiredHelpTextWarning": "Requires restart to take effect",
|
"RestartRequiredHelpTextWarning": "Requires restart to take effect",
|
||||||
"Restore": "Restore",
|
"Restore": "Restore",
|
||||||
"RestoreBackup": "Restore Backup",
|
"RestoreBackup": "Restore Backup",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,5 @@ public class InstallPluginCommand : Command
|
||||||
|
|
||||||
public override bool SendUpdatesToClient => true;
|
public override bool SendUpdatesToClient => true;
|
||||||
public override bool IsExclusive => 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 SendUpdatesToClient => true;
|
||||||
public override bool IsExclusive => true;
|
public override bool IsExclusive => true;
|
||||||
public override string CompletionMessage => null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common;
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
|
@ -7,7 +8,6 @@
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
using NzbDrone.Core.Lifecycle;
|
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Plugins.Commands;
|
using NzbDrone.Core.Plugins.Commands;
|
||||||
|
|
||||||
|
|
@ -20,7 +20,6 @@ public class InstallPluginService : IExecute<InstallPluginCommand>, IExecute<Uni
|
||||||
private readonly IAppFolderInfo _appFolderInfo;
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly IArchiveService _archiveService;
|
private readonly IArchiveService _archiveService;
|
||||||
private readonly ILifecycleService _lifecycleService;
|
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public InstallPluginService(IPluginService pluginService,
|
public InstallPluginService(IPluginService pluginService,
|
||||||
|
|
@ -28,7 +27,6 @@ public InstallPluginService(IPluginService pluginService,
|
||||||
IAppFolderInfo appFolderInfo,
|
IAppFolderInfo appFolderInfo,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IArchiveService archiveService,
|
IArchiveService archiveService,
|
||||||
ILifecycleService lifecycleService,
|
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_pluginService = pluginService;
|
_pluginService = pluginService;
|
||||||
|
|
@ -36,14 +34,19 @@ public InstallPluginService(IPluginService pluginService,
|
||||||
_appFolderInfo = appFolderInfo;
|
_appFolderInfo = appFolderInfo;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_archiveService = archiveService;
|
_archiveService = archiveService;
|
||||||
_lifecycleService = lifecycleService;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute(UninstallPluginCommand message)
|
public void Execute(UninstallPluginCommand message)
|
||||||
{
|
{
|
||||||
var (owner, name) = _pluginService.ParseUrl(message.GithubUrl);
|
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)
|
public void Execute(InstallPluginCommand message)
|
||||||
|
|
@ -67,24 +70,30 @@ private void InstallPlugin(RemotePlugin package)
|
||||||
}
|
}
|
||||||
|
|
||||||
var packageDestination = Path.Combine(tempFolder, $"{package.Name}.zip");
|
var packageDestination = Path.Combine(tempFolder, $"{package.Name}.zip");
|
||||||
|
var packageTitle = $"{package.Owner}/{package.Name} v{package.Version}";
|
||||||
_logger.ProgressInfo($"Downloading plugin {package.Name}");
|
_logger.ProgressInfo($"Downloading plugin [{packageTitle}]");
|
||||||
_httpClient.DownloadFile(package.PackageUrl, packageDestination);
|
_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));
|
_archiveService.Extract(packageDestination, Path.Combine(PluginFolder(), package.Owner, package.Name));
|
||||||
_logger.ProgressInfo($"Installed {package.Name}, restarting");
|
_logger.ProgressInfo($"Plugin [{package.Owner}/{package.Name}] v{package.Version} installed. Please restart Lidarr.");
|
||||||
|
|
||||||
Task.Factory.StartNew(() => _lifecycleService.Restart());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UninstallPlugin(string owner, string name)
|
private void UninstallPlugin(string owner, string name, Version version)
|
||||||
{
|
{
|
||||||
_logger.ProgressInfo($"Uninstalling Plugin {owner}/{name}");
|
_logger.ProgressInfo($"Uninstalling plugin [{owner}/{name}]");
|
||||||
_diskProvider.DeleteFolder(Path.Combine(PluginFolder(), owner, name), true);
|
var pluginFolder = Path.Combine(PluginFolder(), owner, name);
|
||||||
_logger.ProgressInfo($"Uninstalled Plugin {owner}/{name}, restarting");
|
_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()
|
private void EnsurePluginFolder()
|
||||||
|
|
|
||||||
|
|
@ -52,51 +52,69 @@ public PluginService(IHttpClient httpClient,
|
||||||
|
|
||||||
public RemotePlugin GetRemotePlugin(string repoUrl)
|
public RemotePlugin GetRemotePlugin(string repoUrl)
|
||||||
{
|
{
|
||||||
var (owner, name) = ParseUrl(repoUrl);
|
try
|
||||||
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}");
|
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;
|
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()
|
public List<IPlugin> GetInstalledPlugins()
|
||||||
{
|
{
|
||||||
foreach (var plugin in _installedPlugins)
|
foreach (var plugin in _installedPlugins)
|
||||||
{
|
{
|
||||||
var remote = GetRemotePlugin(plugin.GithubUrl);
|
try
|
||||||
plugin.AvailableVersion = remote.Version;
|
{
|
||||||
|
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;
|
return _installedPlugins;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Lifecycle.Commands;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Plugins.Commands;
|
using NzbDrone.Core.Plugins.Commands;
|
||||||
using NzbDrone.Integration.Test.Client;
|
using NzbDrone.Integration.Test.Client;
|
||||||
|
|
@ -16,11 +17,13 @@ public class PluginFixture : IntegrationTest
|
||||||
[Order(0)]
|
[Order(0)]
|
||||||
public void should_install_plugin()
|
public void should_install_plugin()
|
||||||
{
|
{
|
||||||
PostAndWaitForRestart(new InstallPluginCommand
|
PostAndWaitForCompletion(new InstallPluginCommand
|
||||||
{
|
{
|
||||||
GithubUrl = "https://github.com/ta264/Lidarr.Plugin.Deemix"
|
GithubUrl = "https://github.com/ta264/Lidarr.Plugin.Deemix"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
PostAndWaitForRestart(new RestartCommand());
|
||||||
|
|
||||||
WaitForRestart();
|
WaitForRestart();
|
||||||
|
|
||||||
var plugins = Plugins.All();
|
var plugins = Plugins.All();
|
||||||
|
|
@ -36,18 +39,19 @@ public void should_uninstall_plugin()
|
||||||
plugins.Should().HaveCount(1);
|
plugins.Should().HaveCount(1);
|
||||||
plugins[0].Name.Should().Be("Deemix");
|
plugins[0].Name.Should().Be("Deemix");
|
||||||
|
|
||||||
PostAndWaitForRestart(new UninstallPluginCommand
|
PostAndWaitForCompletion(new UninstallPluginCommand
|
||||||
{
|
{
|
||||||
GithubUrl = "https://github.com/ta264/Lidarr.Plugin.Deemix"
|
GithubUrl = "https://github.com/ta264/Lidarr.Plugin.Deemix"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
PostAndWaitForRestart(new RestartCommand());
|
||||||
|
|
||||||
WaitForRestart();
|
WaitForRestart();
|
||||||
|
|
||||||
plugins = Plugins.All();
|
plugins = Plugins.All();
|
||||||
plugins.Should().BeEmpty();
|
plugins.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Installing / uninstalling triggers a restart, so manually shutdown the restarted app
|
|
||||||
[OneTimeTearDown]
|
[OneTimeTearDown]
|
||||||
public void TearDown()
|
public void TearDown()
|
||||||
{
|
{
|
||||||
|
|
@ -57,6 +61,43 @@ public void TearDown()
|
||||||
RestClient.Execute(request);
|
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)
|
private SimpleCommandResource PostAndWaitForRestart<T>(T command)
|
||||||
where T : Command, new()
|
where T : Command, new()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue