mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-06 04:52:21 -07:00
New: Allow major version updates to be installed
(cherry picked from commit 0e95ba2021b23cc65bce0a0620dd48e355250dab)
This commit is contained in:
parent
c645afc389
commit
55626594c5
21 changed files with 456 additions and 408 deletions
|
@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
import Logs from 'System/Logs/Logs';
|
import Logs from 'System/Logs/Logs';
|
||||||
import Status from 'System/Status/Status';
|
import Status from 'System/Status/Status';
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import Updates from 'System/Updates/Updates';
|
||||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
|
@ -248,7 +248,7 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/system/updates"
|
path="/system/updates"
|
||||||
component={UpdatesConnector}
|
component={Updates}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -44,6 +44,7 @@ export interface CustomFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
|
version: string;
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
|
|
|
@ -13,13 +13,16 @@ import MetadataProfile from 'typings/MetadataProfile';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import RootFolder from 'typings/RootFolder';
|
import RootFolder from 'typings/RootFolder';
|
||||||
import { UiSettings } from 'typings/UiSettings';
|
import General from 'typings/Settings/General';
|
||||||
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export type GeneralAppState = AppSectionItemState<General>;
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
@ -59,6 +62,7 @@ interface SettingsAppState {
|
||||||
advancedSettings: boolean;
|
advancedSettings: boolean;
|
||||||
customFormats: CustomFormatAppState;
|
customFormats: CustomFormatAppState;
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
|
general: GeneralAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import { AppSectionItemState } from './AppSectionState';
|
import Update from 'typings/Update';
|
||||||
|
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||||
|
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
|
export type UpdateAppState = AppSectionState<Update>;
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
|
updates: UpdateAppState;
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('Last Album')}
|
{translate('LastAlbum')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import { UiSettings } from 'typings/UiSettings';
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
|
|
@ -1,48 +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) => {
|
|
||||||
return (
|
|
||||||
<li key={index}>
|
|
||||||
<InlineMarkdown data={change} />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateChanges.propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
changes: PropTypes.arrayOf(PropTypes.string)
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UpdateChanges;
|
|
43
frontend/src/System/Updates/UpdateChanges.tsx
Normal file
43
frontend/src/System/Updates/UpdateChanges.tsx
Normal 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/Lidarr/Lidarr/issues/${match.substring(
|
||||||
|
1
|
||||||
|
)})`
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
<InlineMarkdown data={checkChange} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateChanges;
|
|
@ -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 (
|
|
||||||
<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}
|
|
||||||
>
|
|
||||||
Install Latest
|
|
||||||
</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}>
|
|
||||||
The latest version of Lidarr is already installed
|
|
||||||
</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}>—</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)}
|
|
||||||
>
|
|
||||||
Currently Installed
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
update.version !== currentVersion && update.installedOn ?
|
|
||||||
<Label
|
|
||||||
className={styles.label}
|
|
||||||
kind={kinds.INVERSE}
|
|
||||||
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
|
||||||
>
|
|
||||||
Previously Installed
|
|
||||||
</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>
|
|
||||||
Failed to fetch updates
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!generalSettingsError &&
|
|
||||||
<div>
|
|
||||||
Failed to update settings
|
|
||||||
</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;
|
|
303
frontend/src/System/Updates/Updates.tsx
Normal file
303
frontend/src/System/Updates/Updates.tsx
Normal 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}>—</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>
|
||||||
|
|
||||||
|
{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: 'lidarr.audio',
|
||||||
|
url: 'https://lidarr.audio/#downloads',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
confirmLabel={translate('Install')}
|
||||||
|
onConfirm={handleInstallLatestMajorVersionPress}
|
||||||
|
onCancel={handleCancelMajorVersionPress}
|
||||||
|
/>
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Updates;
|
|
@ -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);
|
|
45
frontend/src/typings/Settings/General.ts
Normal file
45
frontend/src/typings/Settings/General.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
export interface UiSettings {
|
export default interface UiSettings {
|
||||||
theme: 'auto' | 'dark' | 'light';
|
theme: 'auto' | 'dark' | 'light';
|
||||||
showRelativeDates: boolean;
|
showRelativeDates: boolean;
|
||||||
shortDateFormat: string;
|
shortDateFormat: string;
|
|
@ -19,6 +19,7 @@ interface SystemStatus {
|
||||||
osName: string;
|
osName: string;
|
||||||
osVersion: string;
|
osVersion: string;
|
||||||
packageUpdateMechanism: string;
|
packageUpdateMechanism: string;
|
||||||
|
packageUpdateMechanismMessage: string;
|
||||||
runtimeName: string;
|
runtimeName: string;
|
||||||
runtimeVersion: string;
|
runtimeVersion: string;
|
||||||
sqliteVersion: string;
|
sqliteVersion: string;
|
||||||
|
|
20
frontend/src/typings/Update.ts
Normal file
20
frontend/src/typings/Update.ts
Normal 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;
|
|
@ -109,6 +109,7 @@
|
||||||
"ApplyTagsHelpTextHowToApplyIndexers": "How to apply tags to the selected indexers",
|
"ApplyTagsHelpTextHowToApplyIndexers": "How to apply tags to the selected indexers",
|
||||||
"ApplyTagsHelpTextRemove": "Remove: Remove the entered tags",
|
"ApplyTagsHelpTextRemove": "Remove: Remove the entered tags",
|
||||||
"ApplyTagsHelpTextReplace": "Replace: Replace the tags with the entered tags (enter no tags to clear all 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?",
|
"AreYouSure": "Are you sure?",
|
||||||
"Artist": "Artist",
|
"Artist": "Artist",
|
||||||
"ArtistAlbumClickToChangeTrack": "Click to change track",
|
"ArtistAlbumClickToChangeTrack": "Click to change track",
|
||||||
|
@ -187,6 +188,7 @@
|
||||||
"CalendarWeekColumnHeaderHelpText": "Shown above each column when week is the active view",
|
"CalendarWeekColumnHeaderHelpText": "Shown above each column when week is the active view",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"CancelMessageText": "Are you sure you want to cancel this pending task?",
|
"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",
|
"CatalogNumber": "Catalog Number",
|
||||||
"CertificateValidation": "Certificate Validation",
|
"CertificateValidation": "Certificate Validation",
|
||||||
"CertificateValidationHelpText": "Change how strict HTTPS certification validation is. Do not change unless you understand the risks.",
|
"CertificateValidationHelpText": "Change how strict HTTPS certification validation is. Do not change unless you understand the risks.",
|
||||||
|
@ -367,6 +369,7 @@
|
||||||
"DoNotPrefer": "Do Not Prefer",
|
"DoNotPrefer": "Do Not Prefer",
|
||||||
"DoNotUpgradeAutomatically": "Do not Upgrade Automatically",
|
"DoNotUpgradeAutomatically": "Do not Upgrade Automatically",
|
||||||
"Docker": "Docker",
|
"Docker": "Docker",
|
||||||
|
"DockerUpdater": "Update the docker container to receive the update",
|
||||||
"Donate": "Donate",
|
"Donate": "Donate",
|
||||||
"Donations": "Donations",
|
"Donations": "Donations",
|
||||||
"DoneEditingGroups": "Done Editing Groups",
|
"DoneEditingGroups": "Done Editing Groups",
|
||||||
|
@ -469,6 +472,7 @@
|
||||||
"ExpandSingleByDefaultHelpText": "Singles",
|
"ExpandSingleByDefaultHelpText": "Singles",
|
||||||
"ExportCustomFormat": "Export Custom Format",
|
"ExportCustomFormat": "Export Custom Format",
|
||||||
"External": "External",
|
"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)",
|
"ExtraFileExtensionsHelpText": "Comma separated list of extra files to import (.nfo will be imported as .nfo-orig)",
|
||||||
"ExtraFileExtensionsHelpTextsExamples": "Examples: '.sub, .nfo' or 'sub,nfo'",
|
"ExtraFileExtensionsHelpTextsExamples": "Examples: '.sub, .nfo' or 'sub,nfo'",
|
||||||
"FailedDownloadHandling": "Failed Download Handling",
|
"FailedDownloadHandling": "Failed Download Handling",
|
||||||
|
@ -616,6 +620,11 @@
|
||||||
"IndexersSettingsSummary": "Indexers and indexer options",
|
"IndexersSettingsSummary": "Indexers and indexer options",
|
||||||
"Info": "Info",
|
"Info": "Info",
|
||||||
"InfoUrl": "Info URL",
|
"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",
|
"InstanceName": "Instance Name",
|
||||||
"InstanceNameHelpText": "Instance name in tab and for Syslog app name",
|
"InstanceNameHelpText": "Instance name in tab and for Syslog app name",
|
||||||
"InteractiveImport": "Interactive Import",
|
"InteractiveImport": "Interactive Import",
|
||||||
|
@ -835,6 +844,7 @@
|
||||||
"OnHealthIssue": "On Health Issue",
|
"OnHealthIssue": "On Health Issue",
|
||||||
"OnHealthRestored": "On Health Restored",
|
"OnHealthRestored": "On Health Restored",
|
||||||
"OnImportFailure": "On Import Failure",
|
"OnImportFailure": "On Import Failure",
|
||||||
|
"OnLatestVersion": "The latest version of {appName} is already installed",
|
||||||
"OnReleaseImport": "On Release Import",
|
"OnReleaseImport": "On Release Import",
|
||||||
"OnRename": "On Rename",
|
"OnRename": "On Rename",
|
||||||
"OnTrackRetag": "On Track Retag",
|
"OnTrackRetag": "On Track Retag",
|
||||||
|
@ -884,6 +894,7 @@
|
||||||
"Presets": "Presets",
|
"Presets": "Presets",
|
||||||
"PreviewRename": "Preview Rename",
|
"PreviewRename": "Preview Rename",
|
||||||
"PreviewRetag": "Preview Retag",
|
"PreviewRetag": "Preview Retag",
|
||||||
|
"PreviouslyInstalled": "Previously Installed",
|
||||||
"PrimaryAlbumTypes": "Primary Album Types",
|
"PrimaryAlbumTypes": "Primary Album Types",
|
||||||
"PrimaryTypes": "Primary Types",
|
"PrimaryTypes": "Primary Types",
|
||||||
"Priority": "Priority",
|
"Priority": "Priority",
|
||||||
|
@ -1126,6 +1137,7 @@
|
||||||
"ShowUnknownArtistItems": "Show Unknown Artist Items",
|
"ShowUnknownArtistItems": "Show Unknown Artist Items",
|
||||||
"ShownAboveEachColumnWhenWeekIsTheActiveView": "Shown above each column when week is the active view",
|
"ShownAboveEachColumnWhenWeekIsTheActiveView": "Shown above each column when week is the active view",
|
||||||
"ShownClickToHide": "Shown, click to hide",
|
"ShownClickToHide": "Shown, click to hide",
|
||||||
|
"Shutdown": "Shutdown",
|
||||||
"Size": " Size",
|
"Size": " Size",
|
||||||
"SizeLimit": "Size Limit",
|
"SizeLimit": "Size Limit",
|
||||||
"SizeOnDisk": "Size on Disk",
|
"SizeOnDisk": "Size on Disk",
|
||||||
|
@ -1274,6 +1286,7 @@
|
||||||
"UnmonitoredHelpText": "Include unmonitored albums in the iCal feed",
|
"UnmonitoredHelpText": "Include unmonitored albums in the iCal feed",
|
||||||
"UnmonitoredOnly": "Unmonitored Only",
|
"UnmonitoredOnly": "Unmonitored Only",
|
||||||
"UpdateAll": "Update 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",
|
"UpdateAutomaticallyHelpText": "Automatically download and install updates. You will still be able to install from System: Updates",
|
||||||
"UpdateAvailableHealthCheckMessage": "New update is available: {version}",
|
"UpdateAvailableHealthCheckMessage": "New update is available: {version}",
|
||||||
"UpdateCheckStartupNotWritableMessage": "Cannot install update because startup folder '{0}' is not writable by the user '{1}'.",
|
"UpdateCheckStartupNotWritableMessage": "Cannot install update because startup folder '{0}' is not writable by the user '{1}'.",
|
||||||
|
|
|
@ -7,5 +7,7 @@ namespace NzbDrone.Core.Update.Commands
|
||||||
public override bool SendUpdatesToClient => true;
|
public override bool SendUpdatesToClient => true;
|
||||||
|
|
||||||
public override string CompletionMessage => null;
|
public override string CompletionMessage => null;
|
||||||
|
|
||||||
|
public bool InstallMajorUpdate { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace NzbDrone.Core.Update.Commands
|
||||||
{
|
{
|
||||||
public class ApplicationUpdateCommand : Command
|
public class ApplicationUpdateCommand : Command
|
||||||
{
|
{
|
||||||
|
public bool InstallMajorUpdate { get; set; }
|
||||||
public override bool SendUpdatesToClient => true;
|
public override bool SendUpdatesToClient => true;
|
||||||
public override bool IsExclusive => true;
|
public override bool IsExclusive => true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ using NzbDrone.Core.Update.Commands;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Update
|
namespace NzbDrone.Core.Update
|
||||||
{
|
{
|
||||||
public class InstallUpdateService : IExecute<ApplicationUpdateCheckCommand>, IExecute<ApplicationUpdateCommand>, IHandle<ApplicationStartingEvent>
|
public class InstallUpdateService : IExecute<ApplicationUpdateCommand>, IExecute<ApplicationUpdateCheckCommand>, IHandle<ApplicationStartingEvent>
|
||||||
{
|
{
|
||||||
private readonly ICheckUpdateService _checkUpdateService;
|
private readonly ICheckUpdateService _checkUpdateService;
|
||||||
private readonly Logger _logger;
|
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");
|
_logger.ProgressDebug("Checking for updates");
|
||||||
|
|
||||||
|
@ -244,7 +244,13 @@ namespace NzbDrone.Core.Update
|
||||||
return null;
|
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.");
|
_logger.ProgressDebug("Auto-update not enabled, not installing available update.");
|
||||||
return null;
|
return null;
|
||||||
|
@ -273,7 +279,7 @@ namespace NzbDrone.Core.Update
|
||||||
|
|
||||||
public void Execute(ApplicationUpdateCheckCommand message)
|
public void Execute(ApplicationUpdateCheckCommand message)
|
||||||
{
|
{
|
||||||
if (GetUpdatePackage(message.Trigger) != null)
|
if (GetUpdatePackage(message.Trigger, true) != null)
|
||||||
{
|
{
|
||||||
_commandQueueManager.Push(new ApplicationUpdateCommand(), trigger: message.Trigger);
|
_commandQueueManager.Push(new ApplicationUpdateCommand(), trigger: message.Trigger);
|
||||||
}
|
}
|
||||||
|
@ -281,7 +287,7 @@ namespace NzbDrone.Core.Update
|
||||||
|
|
||||||
public void Execute(ApplicationUpdateCommand message)
|
public void Execute(ApplicationUpdateCommand message)
|
||||||
{
|
{
|
||||||
var latestAvailable = GetUpdatePackage(message.Trigger);
|
var latestAvailable = GetUpdatePackage(message.Trigger, message.InstallMajorUpdate);
|
||||||
|
|
||||||
if (latestAvailable != null)
|
if (latestAvailable != null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Update
|
namespace NzbDrone.Core.Update
|
||||||
|
|
|
@ -42,6 +42,7 @@ namespace NzbDrone.Core.Update
|
||||||
.AddQueryParam("runtime", "netcore")
|
.AddQueryParam("runtime", "netcore")
|
||||||
.AddQueryParam("runtimeVer", _platformInfo.Version)
|
.AddQueryParam("runtimeVer", _platformInfo.Version)
|
||||||
.AddQueryParam("dbType", _mainDatabase.DatabaseType)
|
.AddQueryParam("dbType", _mainDatabase.DatabaseType)
|
||||||
|
.AddQueryParam("includeMajorVersion", true)
|
||||||
.SetSegment("branch", branch);
|
.SetSegment("branch", branch);
|
||||||
|
|
||||||
if (_analyticsService.IsEnabled)
|
if (_analyticsService.IsEnabled)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue