1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-10-29 23:22:39 +01:00

New: Allow major version updates to be installed

(cherry picked from commit 0e95ba2021b23cc65bce0a0620dd48e355250dab)
This commit is contained in:
Mark McDowall 2024-07-14 16:42:35 -07:00 committed by Bogdan
parent 84b507faf3
commit f900d623dc
42 changed files with 419 additions and 435 deletions

View File

@ -31,7 +31,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 getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
@ -228,7 +228,7 @@ function AppRoutes(props) {
<Route
path="/system/updates"
component={UpdatesConnector}
component={Updates}
/>
<Route

View File

@ -2,18 +2,21 @@ import DiskSpace from 'typings/DiskSpace';
import Health from 'typings/Health';
import SystemStatus from 'typings/SystemStatus';
import Task from 'typings/Task';
import Update from 'typings/Update';
import AppSectionState, { AppSectionItemState } from './AppSectionState';
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
export type HealthAppState = AppSectionState<Health>;
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
export type TaskAppState = AppSectionState<Task>;
export type UpdateAppState = AppSectionState<Update>;
interface SystemAppState {
diskSpace: DiskSpaceAppState;
health: HealthAppState;
status: SystemStatusAppState;
tasks: TaskAppState;
updates: UpdateAppState;
}
export default SystemAppState;

View File

@ -1,52 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import styles from './UpdateChanges.css';
class UpdateChanges extends Component {
//
// Render
render() {
const {
title,
changes
} = this.props;
if (changes.length === 0) {
return null;
}
const uniqueChanges = [...new Set(changes)];
return (
<div>
<div className={styles.title}>{title}</div>
<ul>
{
uniqueChanges.map((change, index) => {
const checkChange = change.replace(/#\d{4,5}\b/g, (match, contents) => {
return `[${match}](https://github.com/Radarr/Radarr/issues/${match.substring(1)})`;
});
return (
<li key={index}>
<InlineMarkdown data={checkChange} />
</li>
);
})
}
</ul>
</div>
);
}
}
UpdateChanges.propTypes = {
title: PropTypes.string.isRequired,
changes: PropTypes.arrayOf(PropTypes.string)
};
export default UpdateChanges;

View File

@ -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 (
<div>
<div className={styles.title}>{title}</div>
<ul>
{uniqueChanges.map((change, index) => {
const checkChange = change.replace(
/#\d{4,5}\b/g,
(match) =>
`[${match}](https://github.com/Radarr/Radarr/issues/${match.substring(
1
)})`
);
return (
<li key={index}>
<InlineMarkdown data={checkChange} />
</li>
);
})}
</ul>
</div>
);
}
export default UpdateChanges;

View File

@ -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 = translate('UpdateRadarrDirectlyLoadError');
const externalUpdaterMessages = {
external: translate('ExternalUpdater'),
apt: translate('AptUpdater'),
docker: translate('DockerUpdater')
};
return (
<PageContent title={translate('Updates')}>
<PageContentBody>
{
!isPopulated && !hasError &&
<LoadingIndicator />
}
{
noUpdates &&
<Alert kind={kinds.INFO}>
{translate('NoUpdatesAreAvailable')}
</Alert>
}
{
hasUpdateToInstall &&
<div className={styles.messageContainer}>
{
updateMechanism === 'builtIn' || updateMechanism === 'script' ?
<SpinnerButton
className={styles.updateAvailable}
kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate}
onPress={onInstallLatestPress}
>
{translate('InstallLatest')}
</SpinnerButton> :
<Fragment>
<Icon
name={icons.WARNING}
kind={kinds.WARNING}
size={30}
/>
<div className={styles.message}>
{externalUpdaterPrefix} <InlineMarkdown data={updateMechanismMessage || externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} />
</div>
</Fragment>
}
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
</div>
}
{
noUpdateToInstall &&
<div className={styles.messageContainer}>
<Icon
className={styles.upToDateIcon}
name={icons.CHECK_CIRCLE}
size={30}
/>
<div className={styles.message}>
{translate('OnLatestVersion')}
</div>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
</div>
}
{
hasUpdates &&
<div>
{
items.map((update) => {
const hasChanges = !!update.changes;
return (
<div
key={update.version}
className={styles.update}
>
<div className={styles.info}>
<div className={styles.version}>{update.version}</div>
<div className={styles.space}>&mdash;</div>
<div
className={styles.date}
title={formatDateTime(update.releaseDate, longDateFormat, timeFormat)}
>
{formatDate(update.releaseDate, shortDateFormat)}
</div>
{
update.branch === 'master' ?
null :
<Label
className={styles.label}
>
{update.branch}
</Label>
}
{
update.version === currentVersion ?
<Label
className={styles.label}
kind={kinds.SUCCESS}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
>
{translate('CurrentlyInstalled')}
</Label> :
null
}
{
update.version !== currentVersion && update.installedOn ?
<Label
className={styles.label}
kind={kinds.INVERSE}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
>
{translate('PreviouslyInstalled')}
</Label> :
null
}
</div>
{
!hasChanges &&
<div>
{translate('MaintenanceRelease')}
</div>
}
{
hasChanges &&
<div className={styles.changes}>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
}
</div>
);
})
}
</div>
}
{
!!updatesError &&
<div>
{translate('FailedToFetchUpdates')}
</div>
}
{
!!generalSettingsError &&
<div>
{translate('FailedToUpdateSettings')}
</div>
}
</PageContentBody>
</PageContent>
);
}
}
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;

View File

@ -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<Record<UpdateMechanism, string>> = {
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 (
<PageContent title={translate('Updates')}>
<PageContentBody>
{isPopulated || hasError ? null : <LoadingIndicator />}
{noUpdates ? (
<Alert kind={kinds.INFO}>{translate('NoUpdatesAreAvailable')}</Alert>
) : null}
{hasUpdateToInstall ? (
<div className={styles.messageContainer}>
{updateMechanism === 'builtIn' || updateMechanism === 'script' ? (
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate}
onPress={handleInstallLatestPress}
>
{translate('InstallLatest')}
</SpinnerButton>
) : (
<>
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
<div className={styles.message}>
{externalUpdaterPrefix}{' '}
<InlineMarkdown
data={
packageUpdateMechanismMessage ||
externalUpdaterMessages[updateMechanism] ||
externalUpdaterMessages.external
}
/>
</div>
</>
)}
{isFetching ? (
<LoadingIndicator className={styles.loading} size={20} />
) : null}
</div>
) : null}
{noUpdateToInstall && (
<div className={styles.messageContainer}>
<Icon
className={styles.upToDateIcon}
name={icons.CHECK_CIRCLE}
size={30}
/>
<div className={styles.message}>{translate('OnLatestVersion')}</div>
{isFetching && (
<LoadingIndicator className={styles.loading} size={20} />
)}
</div>
)}
{hasUpdates && (
<div>
{items.map((update) => {
return (
<div key={update.version} className={styles.update}>
<div className={styles.info}>
<div className={styles.version}>{update.version}</div>
<div className={styles.space}>&mdash;</div>
<div
className={styles.date}
title={formatDateTime(
update.releaseDate,
longDateFormat,
timeFormat
)}
>
{formatDate(update.releaseDate, shortDateFormat)}
</div>
{update.branch === 'main' ? null : (
<Label className={styles.label}>{update.branch}</Label>
)}
{update.version === currentVersion ? (
<Label
className={styles.label}
kind={kinds.SUCCESS}
title={formatDateTime(
update.installedOn,
longDateFormat,
timeFormat
)}
>
{translate('CurrentlyInstalled')}
</Label>
) : null}
{update.version !== currentVersion && update.installedOn ? (
<Label
className={styles.label}
kind={kinds.INVERSE}
title={formatDateTime(
update.installedOn,
longDateFormat,
timeFormat
)}
>
{translate('PreviouslyInstalled')}
</Label>
) : null}
</div>
{update.changes ? (
<div>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
) : (
<div>{translate('MaintenanceRelease')}</div>
)}
</div>
);
})}
</div>
)}
{updatesError ? (
<Alert kind={kinds.WARNING}>
{translate('FailedToFetchUpdates')}
</Alert>
) : null}
{generalSettingsError ? (
<Alert kind={kinds.DANGER}>
{translate('FailedToUpdateSettings')}
</Alert>
) : null}
<ConfirmModal
isOpen={isMajorUpdateModalOpen}
kind={kinds.WARNING}
title={translate('InstallMajorVersionUpdate')}
message={
<div>
<div>{translate('InstallMajorVersionUpdateMessage')}</div>
<div>
<InlineMarkdown
data={translate('InstallMajorVersionUpdateMessageLink', {
domain: 'radarr.video',
url: 'https://radarr.video/#downloads',
})}
/>
</div>
</div>
}
confirmLabel={translate('Install')}
onConfirm={handleInstallLatestMajorVersionPress}
onCancel={handleCancelMajorVersionPress}
/>
</PageContentBody>
</PageContent>
);
}
export default Updates;

View File

@ -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 (
<Updates
onInstallLatestPress={this.onInstallLatestPress}
{...this.props}
/>
);
}
}
UpdatesConnector.propTypes = {
dispatchFetchUpdates: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector);

View File

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

View File

@ -56,7 +56,7 @@
"Unlimited": "غير محدود",
"Ungroup": "فك التجميع",
"Unavailable": "غير متوفره",
"UpdateRadarrDirectlyLoadError": "تعذر تحديث {appName} مباشرة ،",
"UpdateAppDirectlyLoadError": "تعذر تحديث {appName} مباشرة ،",
"UiSettingsLoadError": "تعذر تحميل إعدادات واجهة المستخدم",
"CalendarLoadError": "تعذر تحميل التقويم",
"TagsLoadError": "تعذر تحميل العلامات",

View File

@ -621,7 +621,7 @@
"UnableToLoadRootFolders": "Не може да се заредят коренови папки",
"TagsLoadError": "Не може да се заредят маркери",
"CalendarLoadError": "Календарът не може да се зареди",
"UpdateRadarrDirectlyLoadError": "Не може да се актуализира {appName} директно,",
"UpdateAppDirectlyLoadError": "Не може да се актуализира {appName} директно,",
"Ungroup": "Разгрупиране",
"Unlimited": "Неограничен",
"UnmappedFilesOnly": "Само немапирани файлове",

View File

@ -909,7 +909,7 @@
"TagsLoadError": "No es poden carregar les etiquetes",
"CalendarLoadError": "No es pot carregar el calendari",
"UiSettingsLoadError": "No es pot carregar la configuració de la IU",
"UpdateRadarrDirectlyLoadError": "No es pot actualitzar {appName} directament,",
"UpdateAppDirectlyLoadError": "No es pot actualitzar {appName} directament,",
"Unreleased": "No disponible",
"UnselectAll": "Desseleccioneu-ho tot",
"UpdateCheckStartupNotWritableMessage": "L'actualització no es pot instal·lar perquè la carpeta d'inici '{startupFolder}' no té permisos d'escriptura per a l'usuari '{userName}'.",

View File

@ -901,7 +901,7 @@
"QualityProfilesLoadError": "Nelze načíst profily kvality",
"UnableToLoadRootFolders": "Nelze načíst kořenové složky",
"CalendarLoadError": "Kalendář nelze načíst",
"UpdateRadarrDirectlyLoadError": "{appName} nelze aktualizovat přímo,",
"UpdateAppDirectlyLoadError": "{appName} nelze aktualizovat přímo,",
"UnmappedFilesOnly": "Pouze nezmapované soubory",
"UnmappedFolders": "Nezmapované složky",
"Unmonitored": "Nemonitorováno",

View File

@ -159,7 +159,7 @@
"TagsSettingsSummary": "Se alle tags og hvordan de bruges. Ubrugte tags kan fjernes",
"Time": "Tid",
"MediaManagementSettingsLoadError": "Kan ikke indlæse indstillinger for mediestyring",
"UpdateRadarrDirectlyLoadError": "Kan ikke opdatere {appName} direkte,",
"UpdateAppDirectlyLoadError": "Kan ikke opdatere {appName} direkte,",
"BindAddressHelpText": "Gyldig IP4-adresse, 'localhost' eller '*' for alle grænseflader",
"CreateEmptyMovieFoldersHelpText": "Opret manglende filmmapper under diskscanning",
"CouldNotConnectSignalR": "Kunne ikke oprette forbindelse til SignalR, UI opdateres ikke",

View File

@ -774,7 +774,7 @@
"UpgradesAllowed": "Upgrades erlaubt",
"UnmappedFilesOnly": "Nur nicht zugeordnete Dateien",
"Unlimited": "Unlimitiert",
"UpdateRadarrDirectlyLoadError": "{appName} konnte nicht direkt aktualisiert werden,",
"UpdateAppDirectlyLoadError": "{appName} konnte nicht direkt aktualisiert werden,",
"UnableToLoadManualImportItems": "Einträge für manuelles importieren konnten nicht geladen werden",
"AlternativeTitlesLoadError": "Alternative Titel konnten nicht geladen werden.",
"Trigger": "Auslöser",

View File

@ -893,7 +893,7 @@
"UnableToLoadRootFolders": "Δεν είναι δυνατή η φόρτωση ριζικών φακέλων",
"TagsLoadError": "Δεν είναι δυνατή η φόρτωση ετικετών",
"CalendarLoadError": "Δεν είναι δυνατή η φόρτωση του ημερολογίου",
"UpdateRadarrDirectlyLoadError": "Δεν είναι δυνατή η απευθείας ενημέρωση του {appName},",
"UpdateAppDirectlyLoadError": "Δεν είναι δυνατή η απευθείας ενημέρωση του {appName},",
"Ungroup": "Κατάργηση ομάδας",
"Unlimited": "Απεριόριστος",
"UnmappedFilesOnly": "Μόνο μη αντιστοιχισμένα αρχεία",

View File

@ -795,7 +795,11 @@
"IndexersSettingsSummary": "Indexers and release restrictions",
"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",
@ -1774,6 +1778,7 @@
"UnsavedChanges": "Unsaved Changes",
"UnselectAll": "Unselect All",
"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 '{startupFolder}' is not writable by the user '{userName}'.",
@ -1781,7 +1786,6 @@
"UpdateCheckUINotWritableMessage": "Cannot install update because UI folder '{uiFolder}' is not writable by the user '{userName}'.",
"UpdateFiltered": "Update Filtered",
"UpdateMechanismHelpText": "Use {appName}'s built-in updater or a script",
"UpdateRadarrDirectlyLoadError": "Unable to update {appName} directly,",
"UpdateScriptPathHelpText": "Path to a custom script that takes an extracted update package and handle the remainder of the update process",
"UpdateSelected": "Update Selected",
"UpdaterLogFiles": "Updater Log Files",

View File

@ -926,7 +926,7 @@
"Trigger": "Desencadenar",
"Unlimited": "Ilimitado",
"UnableToLoadManualImportItems": "No se pueden cargar elementos de importación manual",
"UpdateRadarrDirectlyLoadError": "No se puede actualizar {appName} directamente,",
"UpdateAppDirectlyLoadError": "No se puede actualizar {appName} directamente,",
"UnmappedFilesOnly": "Solo archivos sin mapear",
"UpgradeUntilCustomFormatScore": "Actualizar hasta la puntuación de formato personalizado",
"UpgradeUntil": "Actualizar hasta",

View File

@ -896,7 +896,7 @@
"UnableToLoadRootFolders": "Juurikansioiden lataus epäonnistui.",
"TagsLoadError": "Tunnisteiden lataus ei onnistu",
"CalendarLoadError": "Kalenterin lataus epäonnistui.",
"UpdateRadarrDirectlyLoadError": "{appName}ia ei voida päivittää suoraan,",
"UpdateAppDirectlyLoadError": "{appName}ia ei voida päivittää suoraan,",
"Ungroup": "Pura ryhmä",
"UnmappedFilesOnly": "Vain kohdistamattomat tiedostot",
"UnmappedFolders": "Kohdistamattomat kansiot",

View File

@ -934,7 +934,7 @@
"Trakt": "Trakt",
"Trigger": "Déclencheur",
"UnableToLoadManualImportItems": "Impossible de charger les éléments d'importation manuelle",
"UpdateRadarrDirectlyLoadError": "Impossible de mettre à jour {appName} directement,",
"UpdateAppDirectlyLoadError": "Impossible de mettre à jour {appName} directement,",
"Unlimited": "Illimité",
"UnmappedFilesOnly": "Fichiers non mappés uniquement",
"UpgradeUntilCustomFormatScore": "Mise à niveau jusqu'au score de format personnalisé",

View File

@ -898,7 +898,7 @@
"UnableToLoadRootFolders": "לא ניתן לטעון תיקיות שורש",
"CalendarLoadError": "לא ניתן לטעון את היומן",
"Unlimited": "ללא הגבלה",
"UpdateRadarrDirectlyLoadError": "לא ניתן לעדכן את {appName} ישירות,",
"UpdateAppDirectlyLoadError": "לא ניתן לעדכן את {appName} ישירות,",
"Ungroup": "בטל קבוצה",
"UnmappedFilesOnly": "קבצים שלא ממופים בלבד",
"UnmappedFolders": "תיקיות לא ממופות",

View File

@ -241,7 +241,7 @@
"QualityProfilesLoadError": "गुणवत्ता प्रोफ़ाइल लोड करने में असमर्थ",
"RemotePathMappingsLoadError": "दूरस्थ पथ मैपिंग लोड करने में असमर्थ",
"CalendarLoadError": "कैलेंडर लोड करने में असमर्थ",
"UpdateRadarrDirectlyLoadError": "सीधे {appName} अद्यतन करने में असमर्थ,",
"UpdateAppDirectlyLoadError": "सीधे {appName} अद्यतन करने में असमर्थ,",
"UnmappedFolders": "बिना मोड़े हुए फोल्डर",
"ICalIncludeUnmonitoredMoviesHelpText": "ICal फीड में अनऑमिटर की गई फिल्में शामिल करें",
"UnselectAll": "सभी का चयन रद्द",

View File

@ -846,7 +846,7 @@
"UpgradesAllowed": "Frissítések Engedélyezve",
"UnmappedFilesOnly": "Kizárólag fel nem térképezett fájlokat",
"Unlimited": "korlátlan",
"UpdateRadarrDirectlyLoadError": "Nem lehetséges közvetlenül frissíteni a {appName}-t",
"UpdateAppDirectlyLoadError": "Nem lehetséges közvetlenül frissíteni a {appName}-t",
"UnableToLoadManualImportItems": "Nem lehetséges betölteni a manuálisan importált elemeket",
"AlternativeTitlesLoadError": "Nem lehetséges betölteni az alternatív címeket.",
"Trigger": "Trigger",

View File

@ -899,7 +899,7 @@
"UnableToLoadRootFolders": "Ekki er hægt að hlaða rótarmöppum",
"TagsLoadError": "Ekki er hægt að hlaða merkin",
"CalendarLoadError": "Ekki er hægt að hlaða dagatalið",
"UpdateRadarrDirectlyLoadError": "Ekki er hægt að uppfæra {appName} beint,",
"UpdateAppDirectlyLoadError": "Ekki er hægt að uppfæra {appName} beint,",
"Ungroup": "Aftengja hópinn",
"Unlimited": "Ótakmarkað",
"UnmappedFilesOnly": "Aðeins ókortlagðar skrár",

View File

@ -932,7 +932,7 @@
"Trigger": "Trigger",
"UnableToLoadManualImportItems": "Impossibile caricare gli elementi di importazione manuale",
"Unlimited": "Illimitato",
"UpdateRadarrDirectlyLoadError": "Impossibile aggiornare {appName} direttamente,",
"UpdateAppDirectlyLoadError": "Impossibile aggiornare {appName} direttamente,",
"UnmappedFilesOnly": "Solo file non mappati",
"UpgradeUntilCustomFormatScore": "Aggiorna fino al punteggio formato personalizzato",
"UpgradeUntil": "Upgrade fino alla qualità",

View File

@ -896,7 +896,7 @@
"UnableToLoadRootFolders": "ルートフォルダを読み込めません",
"TagsLoadError": "タグを読み込めません",
"CalendarLoadError": "カレンダーを読み込めません",
"UpdateRadarrDirectlyLoadError": "{appName}を直接更新できません。",
"UpdateAppDirectlyLoadError": "{appName}を直接更新できません。",
"Ungroup": "グループ化を解除",
"UnmappedFilesOnly": "マップされていないファイルのみ",
"UnmappedFolders": "マップされていないフォルダ",

View File

@ -899,7 +899,7 @@
"UnableToLoadRestrictions": "제한을 불러올 수 없습니다.",
"UnableToLoadRootFolders": "루트 폴더를 불러올 수 없습니다.",
"CalendarLoadError": "달력을 불러올 수 없습니다.",
"UpdateRadarrDirectlyLoadError": "{appName}를 직접 업데이트 할 수 없습니다.",
"UpdateAppDirectlyLoadError": "{appName}를 직접 업데이트 할 수 없습니다.",
"Ungroup": "그룹 해제",
"UnmappedFilesOnly": "매핑되지 않은 파일 만",
"UnmappedFolders": "매핑되지 않은 폴더",

View File

@ -934,7 +934,7 @@
"Trakt": "Trakt",
"Trigger": "In gang zetten",
"UnableToLoadManualImportItems": "Kan items voor handmatig importeren niet laden",
"UpdateRadarrDirectlyLoadError": "Kan {appName} niet rechtstreeks updaten,",
"UpdateAppDirectlyLoadError": "Kan {appName} niet rechtstreeks updaten,",
"Unlimited": "Onbeperkt",
"UnmappedFilesOnly": "Alleen niet-toegewezen bestanden",
"UpgradeUntilCustomFormatScore": "Upgraden tot Score aangepast formaat",

View File

@ -901,7 +901,7 @@
"UnableToLoadRootFolders": "Nie można załadować folderów głównych",
"TagsLoadError": "Nie można załadować tagów",
"CalendarLoadError": "Nie można załadować kalendarza",
"UpdateRadarrDirectlyLoadError": "Nie można bezpośrednio zaktualizować {appName},",
"UpdateAppDirectlyLoadError": "Nie można bezpośrednio zaktualizować {appName},",
"Ungroup": "Rozgrupuj",
"Unlimited": "Nieograniczony",
"Unmonitored": "Niemonitorowane",

View File

@ -934,7 +934,7 @@
"Trakt": "Trakt",
"Trigger": "Acionador",
"UnableToLoadManualImportItems": "Não foi possível carregar os itens de importação manual",
"UpdateRadarrDirectlyLoadError": "Não foi possível atualizar o {appName} diretamente,",
"UpdateAppDirectlyLoadError": "Não foi possível atualizar o {appName} diretamente,",
"Unlimited": "Ilimitado",
"UnmappedFilesOnly": "Somente ficheiros não mapeados",
"UpgradeUntilCustomFormatScore": "Atualizar até a pontuação do formato personalizado",

View File

@ -906,7 +906,7 @@
"Unlimited": "Ilimitado",
"Ungroup": "Desagrupar",
"Unavailable": "Indisponível",
"UpdateRadarrDirectlyLoadError": "Não foi possível carregar o {appName} diretamente,",
"UpdateAppDirectlyLoadError": "Não foi possível carregar o {appName} diretamente,",
"UiSettingsLoadError": "Não foi possível carregar as configurações da interface",
"CalendarLoadError": "Não foi possível carregar o calendário",
"TagsLoadError": "Não foi possível carregar as tags",

View File

@ -912,7 +912,7 @@
"UnableToLoadRootFolders": "Imposibil de încărcat folderele rădăcină",
"TagsLoadError": "Nu se pot încărca etichete",
"CalendarLoadError": "Calendarul nu poate fi încărcat",
"UpdateRadarrDirectlyLoadError": "Imposibil de actualizat direct {appName},",
"UpdateAppDirectlyLoadError": "Imposibil de actualizat direct {appName},",
"Ungroup": "Dezgrupează",
"Unlimited": "Nelimitat",
"UnmappedFilesOnly": "Numai fișiere nemapate",

View File

@ -720,7 +720,7 @@
"Unlimited": "Неограниченно",
"Ungroup": "Разгруппировать",
"Unavailable": "Недоступно",
"UpdateRadarrDirectlyLoadError": "Невозможно обновить {appName} напрямую,",
"UpdateAppDirectlyLoadError": "Невозможно обновить {appName} напрямую,",
"UiSettingsLoadError": "Не удалось загрузить настройки пользовательского интерфейса",
"CalendarLoadError": "Не удалось загрузить календарь",
"TagsLoadError": "Невозможно загрузить теги",

View File

@ -935,7 +935,7 @@
"UnableToLoadRestrictions": "Det gick inte att ladda begränsningar",
"UnableToLoadRootFolders": "Det gick inte att ladda rotmappar",
"CalendarLoadError": "Det gick inte att ladda kalendern",
"UpdateRadarrDirectlyLoadError": "Det går inte att uppdatera {appName} direkt,",
"UpdateAppDirectlyLoadError": "Det går inte att uppdatera {appName} direkt,",
"UpgradeUntilCustomFormatScore": "Uppgradera tills anpassat formatpoäng",
"UpgradeUntil": "Uppgradera tills kvalitet",
"UpgradeUntilThisQualityIsMetOrExceeded": "Uppgradera tills den här kvaliteten uppfylls eller överskrids",

View File

@ -907,7 +907,7 @@
"UnableToLoadRestrictions": "ไม่สามารถโหลดข้อ จำกัด",
"UnableToLoadRootFolders": "ไม่สามารถโหลดโฟลเดอร์รูท",
"CalendarLoadError": "ไม่สามารถโหลดปฏิทิน",
"UpdateRadarrDirectlyLoadError": "ไม่สามารถอัปเดต {appName} ได้โดยตรง",
"UpdateAppDirectlyLoadError": "ไม่สามารถอัปเดต {appName} ได้โดยตรง",
"Ungroup": "ยกเลิกการจัดกลุ่ม",
"Unlimited": "ไม่ จำกัด",
"UnmappedFilesOnly": "ไฟล์ที่ไม่ได้แมปเท่านั้น",

View File

@ -338,7 +338,7 @@
"UnableToLoadRestrictions": "Kısıtlamalar yüklenemiyor",
"UnableToLoadRootFolders": "Kök klasörler yüklenemiyor",
"TagsLoadError": "Etiketler yüklenemiyor",
"UpdateRadarrDirectlyLoadError": "{appName} doğrudan güncellenemiyor,",
"UpdateAppDirectlyLoadError": "{appName} doğrudan güncellenemiyor,",
"Ungroup": "Grubu çöz",
"Unlimited": "Sınırsız",
"UnmappedFilesOnly": "Yalnızca Eşlenmemiş Dosyalar",

View File

@ -892,7 +892,7 @@
"QualityDefinitionsLoadError": "Не вдалося завантажити визначення якості",
"RemotePathMappingsLoadError": "Неможливо завантажити віддалені відображення шляхів",
"UnableToLoadRootFolders": "Не вдалося завантажити кореневі папки",
"UpdateRadarrDirectlyLoadError": "Неможливо оновити {appName} безпосередньо,",
"UpdateAppDirectlyLoadError": "Неможливо оновити {appName} безпосередньо,",
"Unavailable": "Недоступний",
"Unlimited": "Необмежений",
"UnmappedFilesOnly": "Лише незіставлені файли",

View File

@ -912,7 +912,7 @@
"QualityProfilesLoadError": "Không thể tải Hồ sơ chất lượng",
"RemotePathMappingsLoadError": "Không thể tải Ánh xạ đường dẫn từ xa",
"UnableToLoadRootFolders": "Không thể tải các thư mục gốc",
"UpdateRadarrDirectlyLoadError": "Không thể cập nhật {appName} trực tiếp,",
"UpdateAppDirectlyLoadError": "Không thể cập nhật {appName} trực tiếp,",
"Ungroup": "Bỏ nhóm",
"Unlimited": "Vô hạn",
"UnmappedFilesOnly": "Chỉ các tệp chưa được ánh xạ",

View File

@ -127,7 +127,7 @@
"Unmonitored": "未追踪项",
"Unlimited": "无限制",
"Unavailable": "不可用",
"UpdateRadarrDirectlyLoadError": "无法直接更新{appName}",
"UpdateAppDirectlyLoadError": "无法直接更新{appName}",
"UiSettingsLoadError": "无法加载UI设置",
"CalendarLoadError": "无法加载日历",
"TagsLoadError": "无法加载标签",

View File

@ -7,5 +7,7 @@ public class ApplicationCheckUpdateCommand : Command
public override bool SendUpdatesToClient => true;
public override string CompletionMessage => null;
public bool InstallMajorUpdate { get; set; }
}
}

View File

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

View File

@ -231,7 +231,7 @@ private void EnsureAppDataSafety()
}
}
private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger)
private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger, bool installMajorUpdate)
{
_logger.ProgressDebug("Checking for updates");
@ -243,7 +243,13 @@ private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger)
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;
@ -272,7 +278,7 @@ private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger)
public void Execute(ApplicationCheckUpdateCommand message)
{
if (GetUpdatePackage(message.Trigger) != null)
if (GetUpdatePackage(message.Trigger, true) != null)
{
_commandQueueManager.Push(new ApplicationUpdateCommand(), trigger: message.Trigger);
}
@ -280,7 +286,7 @@ public void Execute(ApplicationCheckUpdateCommand message)
public void Execute(ApplicationUpdateCommand message)
{
var latestAvailable = GetUpdatePackage(message.Trigger);
var latestAvailable = GetUpdatePackage(message.Trigger, message.InstallMajorUpdate);
if (latestAvailable != null)
{

View File

@ -42,6 +42,7 @@ public UpdatePackage GetLatestUpdate(string branch, Version currentVersion)
.AddQueryParam("runtime", "netcore")
.AddQueryParam("runtimeVer", _platformInfo.Version)
.AddQueryParam("dbType", _mainDatabase.DatabaseType)
.AddQueryParam("includeMajorVersion", true)
.SetSegment("branch", branch);
if (_analyticsService.IsEnabled)