From 55626594c59c9a3fefa53d738f2a6ee68342cff2 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 14 Jul 2024 16:42:35 -0700 Subject: [PATCH] New: Allow major version updates to be installed (cherry picked from commit 0e95ba2021b23cc65bce0a0620dd48e355250dab) --- frontend/src/App/AppRoutes.js | 4 +- frontend/src/App/State/AppState.ts | 1 + frontend/src/App/State/SettingsAppState.ts | 6 +- frontend/src/App/State/SystemAppState.ts | 5 +- .../Index/Menus/ArtistIndexSortMenu.tsx | 2 +- .../Overview/ArtistIndexOverviewInfo.tsx | 2 +- frontend/src/System/Updates/UpdateChanges.js | 48 --- frontend/src/System/Updates/UpdateChanges.tsx | 43 +++ frontend/src/System/Updates/Updates.js | 249 -------------- frontend/src/System/Updates/Updates.tsx | 303 ++++++++++++++++++ .../src/System/Updates/UpdatesConnector.js | 98 ------ frontend/src/typings/Settings/General.ts | 45 +++ .../src/typings/{ => Settings}/UiSettings.ts | 2 +- frontend/src/typings/SystemStatus.ts | 1 + frontend/src/typings/Update.ts | 20 ++ src/NzbDrone.Core/Localization/Core/en.json | 13 + .../Commands/ApplicationUpdateCheckCommand.cs | 2 + .../Commands/ApplicationUpdateCommand.cs | 1 + .../Update/InstallUpdateService.cs | 16 +- .../Update/UpdateCheckService.cs | 2 +- .../Update/UpdatePackageProvider.cs | 1 + 21 files changed, 456 insertions(+), 408 deletions(-) delete mode 100644 frontend/src/System/Updates/UpdateChanges.js create mode 100644 frontend/src/System/Updates/UpdateChanges.tsx delete mode 100644 frontend/src/System/Updates/Updates.js create mode 100644 frontend/src/System/Updates/Updates.tsx delete mode 100644 frontend/src/System/Updates/UpdatesConnector.js create mode 100644 frontend/src/typings/Settings/General.ts rename frontend/src/typings/{ => Settings}/UiSettings.ts (79%) create mode 100644 frontend/src/typings/Update.ts diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index d12f3c4b0..c1004d36d 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector'; import Logs from 'System/Logs/Logs'; import Status from 'System/Status/Status'; import Tasks from 'System/Tasks/Tasks'; -import UpdatesConnector from 'System/Updates/UpdatesConnector'; +import Updates from 'System/Updates/Updates'; import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector'; import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; @@ -248,7 +248,7 @@ function AppRoutes(props) { , AppSectionDeleteState, AppSectionSaveState {} +export type GeneralAppState = AppSectionItemState; + export interface ImportListAppState extends AppSectionState, AppSectionDeleteState, @@ -59,6 +62,7 @@ interface SettingsAppState { advancedSettings: boolean; customFormats: CustomFormatAppState; downloadClients: DownloadClientAppState; + general: GeneralAppState; importLists: ImportListAppState; indexerFlags: IndexerFlagSettingsAppState; indexers: IndexerAppState; diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts index d43c1d0ee..3c150fcfb 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -1,9 +1,12 @@ import SystemStatus from 'typings/SystemStatus'; -import { AppSectionItemState } from './AppSectionState'; +import Update from 'typings/Update'; +import AppSectionState, { AppSectionItemState } from './AppSectionState'; export type SystemStatusAppState = AppSectionItemState; +export type UpdateAppState = AppSectionState; interface SystemAppState { + updates: UpdateAppState; status: SystemStatusAppState; } diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx b/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx index 9bae4dabf..1b72d0f4c 100644 --- a/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx +++ b/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx @@ -79,7 +79,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) { sortDirection={sortDirection} onPress={onSortSelect} > - {translate('Last Album')} + {translate('LastAlbum')} -
{title}
-
    - { - uniqueChanges.map((change, index) => { - return ( -
  • - -
  • - ); - }) - } -
- - ); - } - -} - -UpdateChanges.propTypes = { - title: PropTypes.string.isRequired, - changes: PropTypes.arrayOf(PropTypes.string) -}; - -export default UpdateChanges; diff --git a/frontend/src/System/Updates/UpdateChanges.tsx b/frontend/src/System/Updates/UpdateChanges.tsx new file mode 100644 index 000000000..3e5ba1c9b --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import styles from './UpdateChanges.css'; + +interface UpdateChangesProps { + title: string; + changes: string[]; +} + +function UpdateChanges(props: UpdateChangesProps) { + const { title, changes } = props; + + if (changes.length === 0) { + return null; + } + + const uniqueChanges = [...new Set(changes)]; + + return ( +
+
{title}
+
    + {uniqueChanges.map((change, index) => { + const checkChange = change.replace( + /#\d{4,5}\b/g, + (match) => + `[${match}](https://github.com/Lidarr/Lidarr/issues/${match.substring( + 1 + )})` + ); + + return ( +
  • + +
  • + ); + })} +
+
+ ); +} + +export default UpdateChanges; diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js deleted file mode 100644 index 528441cbe..000000000 --- a/frontend/src/System/Updates/Updates.js +++ /dev/null @@ -1,249 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import Label from 'Components/Label'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import { icons, kinds } from 'Helpers/Props'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import translate from 'Utilities/String/translate'; -import UpdateChanges from './UpdateChanges'; -import styles from './Updates.css'; - -class Updates extends Component { - - // - // Render - - render() { - const { - currentVersion, - isFetching, - isPopulated, - updatesError, - generalSettingsError, - items, - isInstallingUpdate, - updateMechanism, - updateMechanismMessage, - shortDateFormat, - longDateFormat, - timeFormat, - onInstallLatestPress - } = this.props; - - const hasError = !!(updatesError || generalSettingsError); - const hasUpdates = isPopulated && !hasError && items.length > 0; - const noUpdates = isPopulated && !hasError && !items.length; - const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); - const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; - - const externalUpdaterPrefix = 'Unable to update Lidarr directly,'; - const externalUpdaterMessages = { - external: 'Lidarr is configured to use an external update mechanism', - apt: 'use apt to install the update', - docker: 'update the docker container to receive the update' - }; - - return ( - - - { - !isPopulated && !hasError && - - } - - { - noUpdates && - - {translate('NoUpdatesAreAvailable')} - - } - - { - hasUpdateToInstall && -
- { - updateMechanism === 'builtIn' || updateMechanism === 'script' ? - - Install Latest - : - - - - -
- {externalUpdaterPrefix} -
-
- } - - { - isFetching && - - } -
- } - - { - noUpdateToInstall && -
- -
- The latest version of Lidarr is already installed -
- - { - isFetching && - - } -
- } - - { - hasUpdates && -
- { - items.map((update) => { - const hasChanges = !!update.changes; - - return ( -
-
-
{update.version}
-
-
- {formatDate(update.releaseDate, shortDateFormat)} -
- - { - update.branch === 'master' ? - null : - - } - - { - update.version === currentVersion ? - : - null - } - - { - update.version !== currentVersion && update.installedOn ? - : - null - } -
- - { - !hasChanges && -
- {translate('MaintenanceRelease')} -
- } - - { - hasChanges && -
- - - -
- } -
- ); - }) - } -
- } - - { - !!updatesError && -
- Failed to fetch updates -
- } - - { - !!generalSettingsError && -
- Failed to update settings -
- } -
-
- ); - } - -} - -Updates.propTypes = { - currentVersion: PropTypes.string.isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - updatesError: PropTypes.object, - generalSettingsError: PropTypes.object, - items: PropTypes.array.isRequired, - isInstallingUpdate: PropTypes.bool.isRequired, - updateMechanism: PropTypes.string, - updateMechanismMessage: PropTypes.string, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onInstallLatestPress: PropTypes.func.isRequired -}; - -export default Updates; diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx new file mode 100644 index 000000000..ef3d20288 --- /dev/null +++ b/frontend/src/System/Updates/Updates.tsx @@ -0,0 +1,303 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Alert from 'Components/Alert'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import { icons, kinds } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { UpdateMechanism } from 'typings/Settings/General'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import translate from 'Utilities/String/translate'; +import UpdateChanges from './UpdateChanges'; +import styles from './Updates.css'; + +const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i; + +function createUpdatesSelector() { + return createSelector( + (state: AppState) => state.system.updates, + (state: AppState) => state.settings.general, + (updates, generalSettings) => { + const { error: updatesError, items } = updates; + + const isFetching = updates.isFetching || generalSettings.isFetching; + const isPopulated = updates.isPopulated && generalSettings.isPopulated; + + return { + isFetching, + isPopulated, + updatesError, + generalSettingsError: generalSettings.error, + items, + updateMechanism: generalSettings.item.updateMechanism, + }; + } + ); +} + +function Updates() { + const currentVersion = useSelector((state: AppState) => state.app.version); + const { packageUpdateMechanismMessage } = useSelector( + createSystemStatusSelector() + ); + const { shortDateFormat, longDateFormat, timeFormat } = useSelector( + createUISettingsSelector() + ); + const isInstallingUpdate = useSelector( + createCommandExecutingSelector(commandNames.APPLICATION_UPDATE) + ); + + const { + isFetching, + isPopulated, + updatesError, + generalSettingsError, + items, + updateMechanism, + } = useSelector(createUpdatesSelector()); + + const dispatch = useDispatch(); + const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false); + const hasError = !!(updatesError || generalSettingsError); + const hasUpdates = isPopulated && !hasError && items.length > 0; + const noUpdates = isPopulated && !hasError && !items.length; + + const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError'); + const externalUpdaterMessages: Partial> = { + external: translate('ExternalUpdater'), + apt: translate('AptUpdater'), + docker: translate('DockerUpdater'), + }; + + const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => { + const majorVersion = parseInt( + currentVersion.match(VERSION_REGEX)?.[0] ?? '0' + ); + + const latestVersion = items[0]?.version; + const latestMajorVersion = parseInt( + latestVersion?.match(VERSION_REGEX)?.[0] ?? '0' + ); + + return { + isMajorUpdate: latestMajorVersion > majorVersion, + hasUpdateToInstall: items.some( + (update) => update.installable && update.latest + ), + }; + }, [currentVersion, items]); + + const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; + + const handleInstallLatestPress = useCallback(() => { + if (isMajorUpdate) { + setIsMajorUpdateModalOpen(true); + } else { + dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE })); + } + }, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]); + + const handleInstallLatestMajorVersionPress = useCallback(() => { + setIsMajorUpdateModalOpen(false); + + dispatch( + executeCommand({ + name: commandNames.APPLICATION_UPDATE, + installMajorUpdate: true, + }) + ); + }, [setIsMajorUpdateModalOpen, dispatch]); + + const handleCancelMajorVersionPress = useCallback(() => { + setIsMajorUpdateModalOpen(false); + }, [setIsMajorUpdateModalOpen]); + + useEffect(() => { + dispatch(fetchUpdates()); + dispatch(fetchGeneralSettings()); + }, [dispatch]); + + return ( + + + {isPopulated || hasError ? null : } + + {noUpdates ? ( + {translate('NoUpdatesAreAvailable')} + ) : null} + + {hasUpdateToInstall ? ( +
+ {updateMechanism === 'builtIn' || updateMechanism === 'script' ? ( + + {translate('InstallLatest')} + + ) : ( + <> + + +
+ {externalUpdaterPrefix}{' '} + +
+ + )} + + {isFetching ? ( + + ) : null} +
+ ) : null} + + {noUpdateToInstall && ( +
+ +
{translate('OnLatestVersion')}
+ + {isFetching && ( + + )} +
+ )} + + {hasUpdates && ( +
+ {items.map((update) => { + return ( +
+
+
{update.version}
+
+
+ {formatDate(update.releaseDate, shortDateFormat)} +
+ + {update.branch === 'master' ? null : ( + + )} + + {update.version === currentVersion ? ( + + ) : null} + + {update.version !== currentVersion && update.installedOn ? ( + + ) : null} +
+ + {update.changes ? ( +
+ + + +
+ ) : ( +
{translate('MaintenanceRelease')}
+ )} +
+ ); + })} +
+ )} + + {updatesError ? ( + + {translate('FailedToFetchUpdates')} + + ) : null} + + {generalSettingsError ? ( + + {translate('FailedToUpdateSettings')} + + ) : null} + + +
{translate('InstallMajorVersionUpdateMessage')}
+
+ +
+ + } + confirmLabel={translate('Install')} + onConfirm={handleInstallLatestMajorVersionPress} + onCancel={handleCancelMajorVersionPress} + /> +
+
+ ); +} + +export default Updates; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js deleted file mode 100644 index 77d75dbda..000000000 --- a/frontend/src/System/Updates/UpdatesConnector.js +++ /dev/null @@ -1,98 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; -import { fetchUpdates } from 'Store/Actions/systemActions'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import Updates from './Updates'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app.version, - createSystemStatusSelector(), - (state) => state.system.updates, - (state) => state.settings.general, - createUISettingsSelector(), - createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), - ( - currentVersion, - status, - updates, - generalSettings, - uiSettings, - isInstallingUpdate - ) => { - const { - error: updatesError, - items - } = updates; - - const isFetching = updates.isFetching || generalSettings.isFetching; - const isPopulated = updates.isPopulated && generalSettings.isPopulated; - - return { - currentVersion, - isFetching, - isPopulated, - updatesError, - generalSettingsError: generalSettings.error, - items, - isInstallingUpdate, - updateMechanism: generalSettings.item.updateMechanism, - updateMechanismMessage: status.packageUpdateMechanismMessage, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchUpdates: fetchUpdates, - dispatchFetchGeneralSettings: fetchGeneralSettings, - dispatchExecuteCommand: executeCommand -}; - -class UpdatesConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchUpdates(); - this.props.dispatchFetchGeneralSettings(); - } - - // - // Listeners - - onInstallLatestPress = () => { - this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -UpdatesConnector.propTypes = { - dispatchFetchUpdates: PropTypes.func.isRequired, - dispatchFetchGeneralSettings: PropTypes.func.isRequired, - dispatchExecuteCommand: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector); diff --git a/frontend/src/typings/Settings/General.ts b/frontend/src/typings/Settings/General.ts new file mode 100644 index 000000000..c867bed74 --- /dev/null +++ b/frontend/src/typings/Settings/General.ts @@ -0,0 +1,45 @@ +export type UpdateMechanism = + | 'builtIn' + | 'script' + | 'external' + | 'apt' + | 'docker'; + +export default interface General { + bindAddress: string; + port: number; + sslPort: number; + enableSsl: boolean; + launchBrowser: boolean; + authenticationMethod: string; + authenticationRequired: string; + analyticsEnabled: boolean; + username: string; + password: string; + passwordConfirmation: string; + logLevel: string; + consoleLogLevel: string; + branch: string; + apiKey: string; + sslCertPath: string; + sslCertPassword: string; + urlBase: string; + instanceName: string; + applicationUrl: string; + updateAutomatically: boolean; + updateMechanism: UpdateMechanism; + updateScriptPath: string; + proxyEnabled: boolean; + proxyType: string; + proxyHostname: string; + proxyPort: number; + proxyUsername: string; + proxyPassword: string; + proxyBypassFilter: string; + proxyBypassLocalAddresses: boolean; + certificateValidation: string; + backupFolder: string; + backupInterval: number; + backupRetention: number; + id: number; +} diff --git a/frontend/src/typings/UiSettings.ts b/frontend/src/typings/Settings/UiSettings.ts similarity index 79% rename from frontend/src/typings/UiSettings.ts rename to frontend/src/typings/Settings/UiSettings.ts index 3c23a0356..656c4518b 100644 --- a/frontend/src/typings/UiSettings.ts +++ b/frontend/src/typings/Settings/UiSettings.ts @@ -1,4 +1,4 @@ -export interface UiSettings { +export default interface UiSettings { theme: 'auto' | 'dark' | 'light'; showRelativeDates: boolean; shortDateFormat: string; diff --git a/frontend/src/typings/SystemStatus.ts b/frontend/src/typings/SystemStatus.ts index e72be2c5c..47f2b3552 100644 --- a/frontend/src/typings/SystemStatus.ts +++ b/frontend/src/typings/SystemStatus.ts @@ -19,6 +19,7 @@ interface SystemStatus { osName: string; osVersion: string; packageUpdateMechanism: string; + packageUpdateMechanismMessage: string; runtimeName: string; runtimeVersion: string; sqliteVersion: string; diff --git a/frontend/src/typings/Update.ts b/frontend/src/typings/Update.ts new file mode 100644 index 000000000..448b1728d --- /dev/null +++ b/frontend/src/typings/Update.ts @@ -0,0 +1,20 @@ +export interface Changes { + new: string[]; + fixed: string[]; +} + +interface Update { + version: string; + branch: string; + releaseDate: string; + fileName: string; + url: string; + installed: boolean; + installedOn: string; + installable: boolean; + latest: boolean; + changes: Changes | null; + hash: string; +} + +export default Update; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 4280a6933..f3558baf4 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -109,6 +109,7 @@ "ApplyTagsHelpTextHowToApplyIndexers": "How to apply tags to the selected indexers", "ApplyTagsHelpTextRemove": "Remove: Remove the entered tags", "ApplyTagsHelpTextReplace": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)", + "AptUpdater": "Use apt to install the update", "AreYouSure": "Are you sure?", "Artist": "Artist", "ArtistAlbumClickToChangeTrack": "Click to change track", @@ -187,6 +188,7 @@ "CalendarWeekColumnHeaderHelpText": "Shown above each column when week is the active view", "Cancel": "Cancel", "CancelMessageText": "Are you sure you want to cancel this pending task?", + "CancelPendingTask": "Are you sure you want to cancel this pending task?", "CatalogNumber": "Catalog Number", "CertificateValidation": "Certificate Validation", "CertificateValidationHelpText": "Change how strict HTTPS certification validation is. Do not change unless you understand the risks.", @@ -367,6 +369,7 @@ "DoNotPrefer": "Do Not Prefer", "DoNotUpgradeAutomatically": "Do not Upgrade Automatically", "Docker": "Docker", + "DockerUpdater": "Update the docker container to receive the update", "Donate": "Donate", "Donations": "Donations", "DoneEditingGroups": "Done Editing Groups", @@ -469,6 +472,7 @@ "ExpandSingleByDefaultHelpText": "Singles", "ExportCustomFormat": "Export Custom Format", "External": "External", + "ExternalUpdater": "{appName} is configured to use an external update mechanism", "ExtraFileExtensionsHelpText": "Comma separated list of extra files to import (.nfo will be imported as .nfo-orig)", "ExtraFileExtensionsHelpTextsExamples": "Examples: '.sub, .nfo' or 'sub,nfo'", "FailedDownloadHandling": "Failed Download Handling", @@ -616,6 +620,11 @@ "IndexersSettingsSummary": "Indexers and indexer options", "Info": "Info", "InfoUrl": "Info URL", + "Install": "Install", + "InstallLatest": "Install Latest", + "InstallMajorVersionUpdate": "Install Update", + "InstallMajorVersionUpdateMessage": "This update will install a new major version and may not be compatible with your system. Are you sure you want to install this update?", + "InstallMajorVersionUpdateMessageLink": "Please check [{domain}]({url}) for more information.", "InstanceName": "Instance Name", "InstanceNameHelpText": "Instance name in tab and for Syslog app name", "InteractiveImport": "Interactive Import", @@ -835,6 +844,7 @@ "OnHealthIssue": "On Health Issue", "OnHealthRestored": "On Health Restored", "OnImportFailure": "On Import Failure", + "OnLatestVersion": "The latest version of {appName} is already installed", "OnReleaseImport": "On Release Import", "OnRename": "On Rename", "OnTrackRetag": "On Track Retag", @@ -884,6 +894,7 @@ "Presets": "Presets", "PreviewRename": "Preview Rename", "PreviewRetag": "Preview Retag", + "PreviouslyInstalled": "Previously Installed", "PrimaryAlbumTypes": "Primary Album Types", "PrimaryTypes": "Primary Types", "Priority": "Priority", @@ -1126,6 +1137,7 @@ "ShowUnknownArtistItems": "Show Unknown Artist Items", "ShownAboveEachColumnWhenWeekIsTheActiveView": "Shown above each column when week is the active view", "ShownClickToHide": "Shown, click to hide", + "Shutdown": "Shutdown", "Size": " Size", "SizeLimit": "Size Limit", "SizeOnDisk": "Size on Disk", @@ -1274,6 +1286,7 @@ "UnmonitoredHelpText": "Include unmonitored albums in the iCal feed", "UnmonitoredOnly": "Unmonitored Only", "UpdateAll": "Update All", + "UpdateAppDirectlyLoadError": "Unable to update {appName} directly,", "UpdateAutomaticallyHelpText": "Automatically download and install updates. You will still be able to install from System: Updates", "UpdateAvailableHealthCheckMessage": "New update is available: {version}", "UpdateCheckStartupNotWritableMessage": "Cannot install update because startup folder '{0}' is not writable by the user '{1}'.", diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs index ece18a111..fa2cfbf41 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs @@ -7,5 +7,7 @@ namespace NzbDrone.Core.Update.Commands public override bool SendUpdatesToClient => true; public override string CompletionMessage => null; + + public bool InstallMajorUpdate { get; set; } } } diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs index 59a827a0b..6980af708 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs @@ -4,6 +4,7 @@ namespace NzbDrone.Core.Update.Commands { public class ApplicationUpdateCommand : Command { + public bool InstallMajorUpdate { get; set; } public override bool SendUpdatesToClient => true; public override bool IsExclusive => true; } diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index c25e0ef3e..5bce96c47 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -20,7 +20,7 @@ using NzbDrone.Core.Update.Commands; namespace NzbDrone.Core.Update { - public class InstallUpdateService : IExecute, IExecute, IHandle + public class InstallUpdateService : IExecute, IExecute, IHandle { private readonly ICheckUpdateService _checkUpdateService; private readonly Logger _logger; @@ -232,7 +232,7 @@ namespace NzbDrone.Core.Update } } - private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger) + private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger, bool installMajorUpdate) { _logger.ProgressDebug("Checking for updates"); @@ -244,7 +244,13 @@ namespace NzbDrone.Core.Update return null; } - if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && updateTrigger != CommandTrigger.Manual) + if (latestAvailable.Version.Major > BuildInfo.Version.Major && !installMajorUpdate) + { + _logger.ProgressInfo("Unable to install major update, please update update manually from System: Updates"); + return null; + } + + if (!_configFileProvider.UpdateAutomatically && updateTrigger != CommandTrigger.Manual) { _logger.ProgressDebug("Auto-update not enabled, not installing available update."); return null; @@ -273,7 +279,7 @@ namespace NzbDrone.Core.Update public void Execute(ApplicationUpdateCheckCommand message) { - if (GetUpdatePackage(message.Trigger) != null) + if (GetUpdatePackage(message.Trigger, true) != null) { _commandQueueManager.Push(new ApplicationUpdateCommand(), trigger: message.Trigger); } @@ -281,7 +287,7 @@ namespace NzbDrone.Core.Update public void Execute(ApplicationUpdateCommand message) { - var latestAvailable = GetUpdatePackage(message.Trigger); + var latestAvailable = GetUpdatePackage(message.Trigger, message.InstallMajorUpdate); if (latestAvailable != null) { diff --git a/src/NzbDrone.Core/Update/UpdateCheckService.cs b/src/NzbDrone.Core/Update/UpdateCheckService.cs index 4f0e7d4ec..46e4b6b63 100644 --- a/src/NzbDrone.Core/Update/UpdateCheckService.cs +++ b/src/NzbDrone.Core/Update/UpdateCheckService.cs @@ -1,4 +1,4 @@ -using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; namespace NzbDrone.Core.Update diff --git a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs index 2e957b374..08761fde7 100644 --- a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -42,6 +42,7 @@ namespace NzbDrone.Core.Update .AddQueryParam("runtime", "netcore") .AddQueryParam("runtimeVer", _platformInfo.Version) .AddQueryParam("dbType", _mainDatabase.DatabaseType) + .AddQueryParam("includeMajorVersion", true) .SetSegment("branch", branch); if (_analyticsService.IsEnabled)