Compare commits
32 Commits
typescript
...
develop
Author | SHA1 | Date |
---|---|---|
Bogdan | 2f04b037a1 | |
Bogdan | 7b87de2e93 | |
Bogdan | eb2fd13509 | |
Bogdan | ffdb08cfe6 | |
Mark McDowall | 37c4647f24 | |
Mark McDowall | f7a58aab33 | |
Mark McDowall | 4b186e894e | |
kephasdev | 35a2bc9403 | |
Bogdan | cc03ce04f1 | |
Bogdan | 363f8fc347 | |
RaZaSB | 0877a6718d | |
Bogdan | 8b253c36ea | |
Bogdan | e6f82270a9 | |
Mark McDowall | 813965e6a2 | |
Mark McDowall | 0d914f4c53 | |
Mark McDowall | ae7f73208a | |
Weblate | 4c86d673ea | |
Weblate | b1527f9abb | |
Bogdan | 291d792810 | |
Mark McDowall | 9b528eb829 | |
Mark McDowall | 4c0b896174 | |
Bogdan | 4ff83f9efc | |
Bogdan | 217611d716 | |
Mark McDowall | 1299a97579 | |
Mark McDowall | 4c0de55672 | |
Bogdan | 78a0def46a | |
Mark McDowall | 11a9dcb389 | |
Mark McDowall | 4eab168267 | |
Bogdan | c9b5a1258a | |
Mark McDowall | 9127a91dfc | |
Weblate | cc85a28ff7 | |
Mark McDowall | 72db8099e0 |
|
@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ')
|
|||
app_guid=${app_guid:-media}
|
||||
|
||||
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||
|
||||
# Create User / Group as needed
|
||||
|
@ -114,7 +114,7 @@ case "$ARCH" in
|
|||
esac
|
||||
echo ""
|
||||
echo "Removing previous tarballs"
|
||||
# -f to Force so we fail if it doesnt exist
|
||||
# -f to Force so we fail if it doesn't exist
|
||||
rm -f "${app^}".*.tar.gz
|
||||
echo ""
|
||||
echo "Downloading..."
|
||||
|
|
|
@ -359,11 +359,16 @@ module.exports = {
|
|||
],
|
||||
|
||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
||||
'no-shadow': 'off',
|
||||
// These should be enabled after cleaning things up
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'no-shadow': 'off',
|
||||
'prettier/prettier': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
|
@ -376,7 +381,41 @@ module.exports = {
|
|||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
// React Hooks
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
|
||||
// React
|
||||
'react/function-component-definition': 'error',
|
||||
'react/hook-use-state': 'error',
|
||||
'react/jsx-boolean-value': ['error', 'always'],
|
||||
'react/jsx-curly-brace-presence': [
|
||||
'error',
|
||||
{ props: 'never', children: 'never' }
|
||||
],
|
||||
'react/jsx-fragments': 'error',
|
||||
'react/jsx-handler-names': [
|
||||
'error',
|
||||
{
|
||||
eventHandlerPrefix: 'on',
|
||||
eventHandlerPropPrefix: 'on'
|
||||
}
|
||||
],
|
||||
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||
'react/jsx-sort-props': [
|
||||
'error',
|
||||
{
|
||||
callbacksLast: true,
|
||||
noSortAlphabetically: true,
|
||||
reservedFirst: true
|
||||
}
|
||||
],
|
||||
'react/prop-types': 'off',
|
||||
'react/self-closing-comp': 'error'
|
||||
})
|
||||
},
|
||||
{
|
||||
|
|
|
@ -59,6 +59,7 @@ function Blocklist() {
|
|||
sortKey,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
|
@ -223,6 +224,7 @@ function Blocklist() {
|
|||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton
|
||||
|
@ -264,6 +266,7 @@ function Blocklist() {
|
|||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
|
|
|
@ -53,6 +53,7 @@ function History() {
|
|||
sortKey,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
} = useSelector((state: AppState) => state.history);
|
||||
|
@ -154,6 +155,7 @@ function History() {
|
|||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton
|
||||
|
@ -193,6 +195,7 @@ function History() {
|
|||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
|
|
|
@ -73,6 +73,7 @@ function Queue() {
|
|||
sortKey,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
|
@ -269,8 +270,10 @@ function Queue() {
|
|||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
optionsComponent={QueueOptions}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
onSelectAllChange={handleSelectAllChange}
|
||||
onSortPress={handleSortPress}
|
||||
|
@ -344,6 +347,7 @@ function Queue() {
|
|||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
maxPageSize={200}
|
||||
optionsComponent={QueueOptions}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React, { Fragment, useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { setQueueOption } from 'Store/Actions/queueActions';
|
||||
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
|
@ -22,24 +22,26 @@ function QueueOptions() {
|
|||
[name]: value,
|
||||
})
|
||||
);
|
||||
|
||||
if (name === 'includeUnknownSeriesItems') {
|
||||
dispatch(gotoQueuePage({ page: 1 }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeUnknownSeriesItems"
|
||||
value={includeUnknownSeriesItems}
|
||||
helpText={translate('ShowUnknownSeriesItemsHelpText')}
|
||||
onChange={handleOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Fragment>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeUnknownSeriesItems"
|
||||
value={includeUnknownSeriesItems}
|
||||
helpText={translate('ShowUnknownSeriesItemsHelpText')}
|
||||
onChange={handleOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -26,4 +26,5 @@
|
|||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,10 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
|||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||
|
||||
function RedirectWithUrlBase() {
|
||||
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||
}
|
||||
|
||||
function AppRoutes() {
|
||||
return (
|
||||
<Switch>
|
||||
|
@ -51,9 +55,7 @@ function AppRoutes() {
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
addUrlBase={false}
|
||||
render={() => {
|
||||
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||
}}
|
||||
render={RedirectWithUrlBase}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -61,21 +63,9 @@ function AppRoutes() {
|
|||
|
||||
<Route path="/add/import" component={ImportSeries} />
|
||||
|
||||
<Route
|
||||
path="/serieseditor"
|
||||
exact={true}
|
||||
render={() => {
|
||||
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||
}}
|
||||
/>
|
||||
<Route path="/serieseditor" exact={true} render={RedirectWithUrlBase} />
|
||||
|
||||
<Route
|
||||
path="/seasonpass"
|
||||
exact={true}
|
||||
render={() => {
|
||||
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||
}}
|
||||
/>
|
||||
<Route path="/seasonpass" exact={true} render={RedirectWithUrlBase} />
|
||||
|
||||
<Route path="/series/:titleSlug" component={SeriesDetailsPageConnector} />
|
||||
|
||||
|
|
|
@ -20,7 +20,9 @@ import UiSettings from 'typings/Settings/UiSettings';
|
|||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
AppSectionSaveState {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export type GeneralAppState = AppSectionItemState<General>;
|
||||
|
||||
|
@ -32,7 +34,9 @@ export interface ImportListAppState
|
|||
export interface IndexerAppState
|
||||
extends AppSectionState<Indexer>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
AppSectionSaveState {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationAppState
|
||||
extends AppSectionState<Notification>,
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
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 UpdateAppState = AppSectionState<Update>;
|
||||
export type TaskAppState = AppSectionState<Task>;
|
||||
|
||||
interface SystemAppState {
|
||||
diskSpace: DiskSpaceAppState;
|
||||
health: HealthAppState;
|
||||
updates: UpdateAppState;
|
||||
status: SystemStatusAppState;
|
||||
tasks: TaskAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
|
|
|
@ -64,7 +64,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
|||
<div>{info.componentStack}</div>
|
||||
)}
|
||||
|
||||
{<div className={styles.version}>Version: {window.Sonarr.version}</div>}
|
||||
<div className={styles.version}>Version: {window.Sonarr.version}</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ import Scroller from 'Components/Scroller/Scroller';
|
|||
import { icons } from 'Helpers/Props';
|
||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector';
|
||||
import HealthStatus from 'System/Status/Health/HealthStatus';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MessagesConnector from './Messages/MessagesConnector';
|
||||
import PageSidebarItem from './PageSidebarItem';
|
||||
|
@ -147,7 +147,7 @@ const links = [
|
|||
{
|
||||
title: () => translate('Status'),
|
||||
to: '/system/status',
|
||||
statusComponent: HealthStatusConnector
|
||||
statusComponent: HealthStatus
|
||||
},
|
||||
{
|
||||
title: () => translate('Tasks'),
|
||||
|
|
|
@ -6,6 +6,7 @@ type PropertyFunction<T> = () => T;
|
|||
interface Column {
|
||||
name: string;
|
||||
label: string | PropertyFunction<string> | React.ReactNode;
|
||||
className?: string;
|
||||
columnLabel?: string;
|
||||
isSortable?: boolean;
|
||||
isVisible: boolean;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
|
@ -82,9 +82,7 @@ function EpisodeNumber(props: EpisodeNumberProps) {
|
|||
<Popover
|
||||
anchor={
|
||||
<span>
|
||||
{showSeasonNumber && seasonNumber != null && (
|
||||
<Fragment>{seasonNumber}x</Fragment>
|
||||
)}
|
||||
{showSeasonNumber && seasonNumber != null && <>{seasonNumber}x</>}
|
||||
|
||||
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
|
||||
|
||||
|
@ -111,9 +109,7 @@ function EpisodeNumber(props: EpisodeNumberProps) {
|
|||
/>
|
||||
) : (
|
||||
<span>
|
||||
{showSeasonNumber && seasonNumber != null && (
|
||||
<Fragment>{seasonNumber}x</Fragment>
|
||||
)}
|
||||
{showSeasonNumber && seasonNumber != null && <>{seasonNumber}x</>}
|
||||
|
||||
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
|
||||
|
||||
|
|
|
@ -3,15 +3,15 @@ import { useCallback, useState } from 'react';
|
|||
export default function useModalOpenState(
|
||||
initialState: boolean
|
||||
): [boolean, () => void, () => void] {
|
||||
const [isOpen, setOpen] = useState(initialState);
|
||||
const [isOpen, setIsOpen] = useState(initialState);
|
||||
|
||||
const setModalOpen = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, [setOpen]);
|
||||
setIsOpen(true);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const setModalClosed = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
setIsOpen(false);
|
||||
}, [setIsOpen]);
|
||||
|
||||
return [isOpen, setModalOpen, setModalClosed];
|
||||
}
|
||||
|
|
|
@ -857,7 +857,7 @@ function InteractiveImportModalContent(
|
|||
|
||||
<MenuContent>
|
||||
<SelectedMenuItem
|
||||
name={'all'}
|
||||
name="all"
|
||||
isSelected={!filterExistingFiles}
|
||||
onPress={onFilterExistingFilesChange}
|
||||
>
|
||||
|
@ -865,7 +865,7 @@ function InteractiveImportModalContent(
|
|||
</SelectedMenuItem>
|
||||
|
||||
<SelectedMenuItem
|
||||
name={'new'}
|
||||
name="new"
|
||||
isSelected={filterExistingFiles}
|
||||
onPress={onFilterExistingFilesChange}
|
||||
>
|
||||
|
@ -945,7 +945,7 @@ function InteractiveImportModalContent(
|
|||
<SelectInput
|
||||
className={styles.bulkSelect}
|
||||
name="select"
|
||||
value={'select'}
|
||||
value="select"
|
||||
values={bulkSelectOptions}
|
||||
isDisabled={!selectedIds.length}
|
||||
onChange={onSelectModalSelect}
|
||||
|
|
|
@ -17,7 +17,7 @@ function SelectLanguageModal(props: SelectLanguageModalProps) {
|
|||
props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
|
||||
<SelectLanguageModalContent
|
||||
languageIds={languageIds}
|
||||
modalTitle={modalTitle}
|
||||
|
|
|
@ -64,19 +64,20 @@ interface RowItemData {
|
|||
onSeriesSelect(seriesId: number): void;
|
||||
}
|
||||
|
||||
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
}) => {
|
||||
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||
const { items, columns, onSeriesSelect } = data;
|
||||
const series = index >= items.length ? null : items[index];
|
||||
|
||||
if (index >= items.length) {
|
||||
const handlePress = useCallback(() => {
|
||||
if (series?.id) {
|
||||
onSeriesSelect(series.id);
|
||||
}
|
||||
}, [series?.id, onSeriesSelect]);
|
||||
|
||||
if (series == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const series = items[index];
|
||||
|
||||
return (
|
||||
<VirtualTableRowButton
|
||||
style={{
|
||||
|
@ -84,7 +85,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
|||
justifyContent: 'space-between',
|
||||
...style,
|
||||
}}
|
||||
onPress={() => onSeriesSelect(series.id)}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<SelectSeriesRow
|
||||
key={series.id}
|
||||
|
@ -98,7 +99,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
|||
/>
|
||||
</VirtualTableRowButton>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
|
||||
const { modalTitle, onSeriesSelect, onModalClose } = props;
|
||||
|
@ -197,9 +198,9 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
|
|||
/>
|
||||
|
||||
<Scroller
|
||||
ref={scrollerRef}
|
||||
className={styles.scroller}
|
||||
autoFocus={false}
|
||||
ref={scrollerRef}
|
||||
>
|
||||
<SelectSeriesModalTableHeader columns={columns} />
|
||||
<List<RowItemData>
|
||||
|
|
|
@ -17,7 +17,7 @@ function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
|
|||
props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
|
||||
<SelectDownloadClientModalContent
|
||||
protocol={protocol}
|
||||
modalTitle={modalTitle}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Fragment, useCallback, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ParseModal from 'Parse/ParseModal';
|
||||
|
@ -16,7 +16,7 @@ function ParseToolbarButton() {
|
|||
}, [setIsParseModalOpen]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<>
|
||||
<PageToolbarButton
|
||||
label={translate('TestParsing')}
|
||||
iconName={icons.PARSE}
|
||||
|
@ -24,7 +24,7 @@ function ParseToolbarButton() {
|
|||
/>
|
||||
|
||||
<ParseModal isOpen={isParseModalOpen} onModalClose={onParseModalClose} />
|
||||
</Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ interface RootFolderRowProps {
|
|||
}
|
||||
|
||||
function RootFolderRow(props: RootFolderRowProps) {
|
||||
const { id, path, accessible, freeSpace, unmappedFolders = [] } = props;
|
||||
const { id, path, accessible, freeSpace = 0, unmappedFolders = [] } = props;
|
||||
|
||||
const isUnavailable = !accessible;
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
|
@ -6,14 +5,19 @@ import formatBytes from 'Utilities/Number/formatBytes';
|
|||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SeasonInfo.css';
|
||||
|
||||
function SeasonInfo(props) {
|
||||
const {
|
||||
totalEpisodeCount,
|
||||
monitoredEpisodeCount,
|
||||
episodeFileCount,
|
||||
sizeOnDisk
|
||||
} = props;
|
||||
interface SeasonInfoProps {
|
||||
totalEpisodeCount: number;
|
||||
monitoredEpisodeCount: number;
|
||||
episodeFileCount: number;
|
||||
sizeOnDisk: number;
|
||||
}
|
||||
|
||||
function SeasonInfo({
|
||||
totalEpisodeCount,
|
||||
monitoredEpisodeCount,
|
||||
episodeFileCount,
|
||||
sizeOnDisk,
|
||||
}: SeasonInfoProps) {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
|
@ -47,11 +51,4 @@ function SeasonInfo(props) {
|
|||
);
|
||||
}
|
||||
|
||||
SeasonInfo.propTypes = {
|
||||
totalEpisodeCount: PropTypes.number.isRequired,
|
||||
monitoredEpisodeCount: PropTypes.number.isRequired,
|
||||
episodeFileCount: PropTypes.number.isRequired,
|
||||
sizeOnDisk: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default SeasonInfo;
|
|
@ -212,8 +212,8 @@ class SeriesDetails extends Component {
|
|||
} = this.props;
|
||||
|
||||
const {
|
||||
episodeFileCount,
|
||||
sizeOnDisk
|
||||
episodeFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
|
@ -454,10 +454,9 @@ class SeriesDetails extends Component {
|
|||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
}
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
|
|
|
@ -194,10 +194,12 @@ function getInfoRowProps(
|
|||
}
|
||||
|
||||
if (name === 'sizeOnDisk') {
|
||||
const { sizeOnDisk = 0 } = props;
|
||||
|
||||
return {
|
||||
title: translate('SizeOnDisk'),
|
||||
iconName: icons.DRIVE,
|
||||
label: formatBytes(props.sizeOnDisk),
|
||||
label: formatBytes(sizeOnDisk),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -42,11 +42,7 @@ interface SeriesIndexOverviewsProps {
|
|||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
}) => {
|
||||
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||
const { items, ...otherData } = data;
|
||||
|
||||
if (index >= items.length) {
|
||||
|
@ -60,7 +56,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
|||
<SeriesIndexOverview seriesId={series.id} {...otherData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getWindowScrollTopPosition() {
|
||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
|
|
|
@ -37,7 +37,7 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
|
|||
added,
|
||||
seasonCount,
|
||||
path,
|
||||
sizeOnDisk,
|
||||
sizeOnDisk = 0,
|
||||
tags,
|
||||
sortKey,
|
||||
showRelativeDates,
|
||||
|
|
|
@ -60,12 +60,12 @@ const seriesIndexSelector = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
|
||||
function Cell({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
style,
|
||||
data,
|
||||
}) => {
|
||||
}: GridChildComponentProps<CellItemData>) {
|
||||
const { layout, items, sortKey, isSelectMode } = data;
|
||||
const { columnCount, padding, posterWidth, posterHeight } = layout;
|
||||
const index = rowIndex * columnCount + columnIndex;
|
||||
|
@ -92,7 +92,7 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getWindowScrollTopPosition() {
|
||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
|
|
|
@ -45,11 +45,7 @@ const columnsSelector = createSelector(
|
|||
(columns) => columns
|
||||
);
|
||||
|
||||
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
}) => {
|
||||
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||
const { items, sortKey, columns, isSelectMode } = data;
|
||||
|
||||
if (index >= items.length) {
|
||||
|
@ -75,7 +71,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getWindowScrollTopPosition() {
|
||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Fragment, useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
@ -32,7 +32,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) {
|
|||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowBanners')}</FormLabel>
|
||||
|
||||
|
@ -56,7 +56,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) {
|
|||
onChange={onTableOptionChangeWrapper}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
|
@ -17,11 +17,11 @@ function CustomFormatSettingsPage() {
|
|||
// @ts-ignore
|
||||
showSave={false}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<ParseToolbarButton />
|
||||
</Fragment>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
|
@ -231,9 +231,9 @@ function ManageDownloadClientsModalContent(
|
|||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
|
@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent(
|
|||
|
||||
<ManageDownloadClientsEditModal
|
||||
isOpen={isEditModalOpen}
|
||||
downloadClientIds={selectedIds}
|
||||
onModalClose={onEditModalClose}
|
||||
onSavePress={onSavePress}
|
||||
downloadClientIds={selectedIds}
|
||||
/>
|
||||
|
||||
<TagsModal
|
||||
|
|
|
@ -32,7 +32,7 @@ function EditImportListExclusionModal(
|
|||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}>
|
||||
<EditImportListExclusionModalContent
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
onModalClose={onModalClosePress}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -261,9 +261,9 @@ function ManageImportListsModalContent(
|
|||
|
||||
<ManageImportListsEditModal
|
||||
isOpen={isEditModalOpen}
|
||||
importListIds={selectedIds}
|
||||
onModalClose={onEditModalClose}
|
||||
onSavePress={onSavePress}
|
||||
importListIds={selectedIds}
|
||||
/>
|
||||
|
||||
<TagsModal
|
||||
|
|
|
@ -226,9 +226,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
|
@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||
|
||||
<ManageIndexersEditModal
|
||||
isOpen={isEditModalOpen}
|
||||
indexerIds={selectedIds}
|
||||
onModalClose={onEditModalClose}
|
||||
onSavePress={onSavePress}
|
||||
indexerIds={selectedIds}
|
||||
/>
|
||||
|
||||
<TagsModal
|
||||
|
|
|
@ -7,7 +7,7 @@ function createRemoveItemHandler(section, url) {
|
|||
return function(getState, payload, dispatch) {
|
||||
const {
|
||||
id,
|
||||
...queryParams
|
||||
queryParams
|
||||
} = payload;
|
||||
|
||||
dispatch(set({ section, isDeleting: true }));
|
||||
|
|
|
@ -251,6 +251,11 @@ export const filterBuilderProps = [
|
|||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.SERIES_TYPES
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
type: filterBuilderTypes.STRING
|
||||
},
|
||||
{
|
||||
name: 'network',
|
||||
label: () => translate('Network'),
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import StartTime from './StartTime';
|
||||
import styles from './About.css';
|
||||
|
||||
class About extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
version,
|
||||
packageVersion,
|
||||
packageAuthor,
|
||||
isNetCore,
|
||||
isDocker,
|
||||
runtimeVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
migrationVersion,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('About')}>
|
||||
<DescriptionList className={styles.descriptionList}>
|
||||
<DescriptionListItem
|
||||
title={translate('Version')}
|
||||
data={version}
|
||||
/>
|
||||
|
||||
{
|
||||
packageVersion &&
|
||||
<DescriptionListItem
|
||||
title={translate('PackageVersion')}
|
||||
data={(packageAuthor ?
|
||||
<InlineMarkdown data={translate('PackageVersionInfo', {
|
||||
packageVersion,
|
||||
packageAuthor
|
||||
})}
|
||||
/> :
|
||||
packageVersion
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isNetCore &&
|
||||
<DescriptionListItem
|
||||
title={translate('DotNetVersion')}
|
||||
data={`Yes (${runtimeVersion})`}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isDocker &&
|
||||
<DescriptionListItem
|
||||
title={translate('Docker')}
|
||||
data={'Yes'}
|
||||
/>
|
||||
}
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('DatabaseMigration')}
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AppDataDirectory')}
|
||||
data={appData}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('StartupDirectory')}
|
||||
data={startupPath}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Mode')}
|
||||
data={titleCase(mode)}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Uptime')}
|
||||
data={
|
||||
<StartTime
|
||||
startTime={startTime}
|
||||
timeFormat={timeFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
About.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
packageVersion: PropTypes.string,
|
||||
packageAuthor: PropTypes.string,
|
||||
isNetCore: PropTypes.bool.isRequired,
|
||||
runtimeVersion: PropTypes.string.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
databaseType: PropTypes.string.isRequired,
|
||||
databaseVersion: PropTypes.string.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
startTime: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default About;
|
|
@ -0,0 +1,103 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import StartTime from './StartTime';
|
||||
import styles from './About.css';
|
||||
|
||||
function About() {
|
||||
const dispatch = useDispatch();
|
||||
const { item } = useSelector((state: AppState) => state.system.status);
|
||||
|
||||
const {
|
||||
version,
|
||||
packageVersion,
|
||||
packageAuthor,
|
||||
isNetCore,
|
||||
isDocker,
|
||||
runtimeVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
migrationVersion,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
startTime,
|
||||
} = item;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStatus());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('About')}>
|
||||
<DescriptionList className={styles.descriptionList}>
|
||||
<DescriptionListItem title={translate('Version')} data={version} />
|
||||
|
||||
{packageVersion && (
|
||||
<DescriptionListItem
|
||||
title={translate('PackageVersion')}
|
||||
data={
|
||||
packageAuthor ? (
|
||||
<InlineMarkdown
|
||||
data={translate('PackageVersionInfo', {
|
||||
packageVersion,
|
||||
packageAuthor,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
packageVersion
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isNetCore ? (
|
||||
<DescriptionListItem
|
||||
title={translate('DotNetVersion')}
|
||||
data={`Yes (${runtimeVersion})`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isDocker ? (
|
||||
<DescriptionListItem title={translate('Docker')} data="Yes" />
|
||||
) : null}
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('DatabaseMigration')}
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AppDataDirectory')}
|
||||
data={appData}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('StartupDirectory')}
|
||||
data={startupPath}
|
||||
/>
|
||||
|
||||
<DescriptionListItem title={translate('Mode')} data={titleCase(mode)} />
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Uptime')}
|
||||
data={<StartTime startTime={startTime} />}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default About;
|
|
@ -1,52 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import About from './About';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.status,
|
||||
createUISettingsSelector(),
|
||||
(status, uiSettings) => {
|
||||
return {
|
||||
...status.item,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
longDateFormat: uiSettings.longDateFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchStatus
|
||||
};
|
||||
|
||||
class AboutConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchStatus();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<About
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AboutConnector.propTypes = {
|
||||
fetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AboutConnector);
|
|
@ -1,93 +0,0 @@
|
|||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
|
||||
function getUptime(startTime) {
|
||||
return formatTimeSpan(moment().diff(startTime));
|
||||
}
|
||||
|
||||
class StartTime extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = props;
|
||||
|
||||
this._timeoutId = null;
|
||||
|
||||
this.state = {
|
||||
uptime: getUptime(startTime),
|
||||
startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true })
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._timeoutId = setTimeout(this.onTimeout, 1000);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
startTime !== prevProps.startTime ||
|
||||
timeFormat !== prevProps.timeFormat ||
|
||||
longDateFormat !== prevProps.longDateFormat
|
||||
) {
|
||||
this.setState({
|
||||
uptime: getUptime(startTime),
|
||||
startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._timeoutId) {
|
||||
this._timeoutId = clearTimeout(this._timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTimeout = () => {
|
||||
this.setState({ uptime: getUptime(this.props.startTime) });
|
||||
this._timeoutId = setTimeout(this.onTimeout, 1000);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
uptime,
|
||||
startTime
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<span title={startTime}>
|
||||
{uptime}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StartTime.propTypes = {
|
||||
startTime: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default StartTime;
|
|
@ -0,0 +1,44 @@
|
|||
import moment from 'moment';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
|
||||
interface StartTimeProps {
|
||||
startTime: string;
|
||||
}
|
||||
|
||||
function StartTime(props: StartTimeProps) {
|
||||
const { startTime } = props;
|
||||
const { timeFormat, longDateFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
const [time, setTime] = useState(Date.now());
|
||||
|
||||
const { formattedStartTime, uptime } = useMemo(() => {
|
||||
return {
|
||||
uptime: formatTimeSpan(moment(time).diff(startTime)),
|
||||
formattedStartTime: formatDateTime(
|
||||
startTime,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{
|
||||
includeSeconds: true,
|
||||
}
|
||||
),
|
||||
};
|
||||
}, [startTime, time, longDateFormat, timeFormat]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setTime(Date.now()), 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [setTime]);
|
||||
|
||||
return <span title={formattedStartTime}>{uptime}</span>;
|
||||
}
|
||||
|
||||
export default StartTime;
|
|
@ -1,121 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DiskSpace.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'path',
|
||||
label: () => translate('Location'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'freeSpace',
|
||||
label: () => translate('FreeSpace'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'totalSpace',
|
||||
label: () => translate('TotalSpace'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'progress',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class DiskSpace extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('DiskSpace')}>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
const {
|
||||
freeSpace,
|
||||
totalSpace
|
||||
} = item;
|
||||
|
||||
const diskUsage = (100 - freeSpace / totalSpace * 100);
|
||||
let diskUsageKind = kinds.PRIMARY;
|
||||
|
||||
if (diskUsage > 90) {
|
||||
diskUsageKind = kinds.DANGER;
|
||||
} else if (diskUsage > 80) {
|
||||
diskUsageKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={item.path}>
|
||||
<TableRowCell>
|
||||
{item.path}
|
||||
|
||||
{
|
||||
item.label &&
|
||||
` (${item.label})`
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.space}>
|
||||
{formatBytes(freeSpace)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.space}>
|
||||
{formatBytes(totalSpace)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.space}>
|
||||
<ProgressBar
|
||||
progress={diskUsage}
|
||||
kind={diskUsageKind}
|
||||
size={sizes.MEDIUM}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DiskSpace.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
items: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default DiskSpace;
|
|
@ -0,0 +1,111 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import { fetchDiskSpace } from 'Store/Actions/systemActions';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DiskSpace.css';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: 'path',
|
||||
label: () => translate('Location'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'freeSpace',
|
||||
label: () => translate('FreeSpace'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'totalSpace',
|
||||
label: () => translate('TotalSpace'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'progress',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
function createDiskSpaceSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.system.diskSpace,
|
||||
(diskSpace) => {
|
||||
return diskSpace;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function DiskSpace() {
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, items } = useSelector(createDiskSpaceSelector());
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchDiskSpace());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('DiskSpace')}>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{isFetching ? null : (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
const { freeSpace, totalSpace } = item;
|
||||
|
||||
const diskUsage = 100 - (freeSpace / totalSpace) * 100;
|
||||
let diskUsageKind = kinds.PRIMARY;
|
||||
|
||||
if (diskUsage > 90) {
|
||||
diskUsageKind = kinds.DANGER;
|
||||
} else if (diskUsage > 80) {
|
||||
diskUsageKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={item.path}>
|
||||
<TableRowCell>
|
||||
{item.path}
|
||||
|
||||
{item.label && ` (${item.label})`}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.space}>
|
||||
{formatBytes(freeSpace)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.space}>
|
||||
{formatBytes(totalSpace)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.space}>
|
||||
<ProgressBar
|
||||
progress={diskUsage}
|
||||
kind={diskUsageKind}
|
||||
size={sizes.MEDIUM}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiskSpace;
|
|
@ -1,54 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDiskSpace } from 'Store/Actions/systemActions';
|
||||
import DiskSpace from './DiskSpace';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.diskSpace,
|
||||
(diskSpace) => {
|
||||
const {
|
||||
isFetching,
|
||||
items
|
||||
} = diskSpace;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
items
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchDiskSpace
|
||||
};
|
||||
|
||||
class DiskSpaceConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchDiskSpace();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DiskSpace
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DiskSpaceConnector.propTypes = {
|
||||
fetchDiskSpace: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DiskSpaceConnector);
|
|
@ -1,242 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './Health.css';
|
||||
|
||||
function getInternalLink(source) {
|
||||
switch (source) {
|
||||
case 'IndexerRssCheck':
|
||||
case 'IndexerSearchCheck':
|
||||
case 'IndexerStatusCheck':
|
||||
case 'IndexerJackettAllCheck':
|
||||
case 'IndexerLongTermStatusCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title={translate('Settings')}
|
||||
to="/settings/indexers"
|
||||
/>
|
||||
);
|
||||
case 'DownloadClientCheck':
|
||||
case 'DownloadClientStatusCheck':
|
||||
case 'ImportMechanismCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title={translate('Settings')}
|
||||
to="/settings/downloadclients"
|
||||
/>
|
||||
);
|
||||
case 'NotificationStatusCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title={translate('Settings')}
|
||||
to="/settings/connect"
|
||||
/>
|
||||
);
|
||||
case 'RootFolderCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SERIES_CONTINUING}
|
||||
title={translate('SeriesEditor')}
|
||||
to="/serieseditor"
|
||||
/>
|
||||
);
|
||||
case 'UpdateCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.UPDATE}
|
||||
title={translate('Updates')}
|
||||
to="/system/updates"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function getTestLink(source, props) {
|
||||
switch (source) {
|
||||
case 'IndexerStatusCheck':
|
||||
case 'IndexerLongTermStatusCheck':
|
||||
return (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
title={translate('TestAll')}
|
||||
isSpinning={props.isTestingAllIndexers}
|
||||
onPress={props.dispatchTestAllIndexers}
|
||||
/>
|
||||
);
|
||||
case 'DownloadClientCheck':
|
||||
case 'DownloadClientStatusCheck':
|
||||
return (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
title={translate('TestAll')}
|
||||
isSpinning={props.isTestingAllDownloadClients}
|
||||
onPress={props.dispatchTestAllDownloadClients}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
className: styles.status,
|
||||
name: 'type',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
label: () => translate('Message'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: () => translate('Actions'),
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class Health extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const healthIssues = !!items.length;
|
||||
|
||||
return (
|
||||
<FieldSet
|
||||
legend={
|
||||
<div className={styles.legend}>
|
||||
{translate('Health')}
|
||||
|
||||
{
|
||||
isFetching && isPopulated &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!healthIssues &&
|
||||
<div className={styles.healthOk}>
|
||||
{translate('NoIssuesWithYourConfiguration')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
healthIssues &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
const internalLink = getInternalLink(item.source);
|
||||
const testLink = getTestLink(item.source, this.props);
|
||||
|
||||
let kind = kinds.WARNING;
|
||||
switch (item.type.toLowerCase()) {
|
||||
case 'error':
|
||||
kind = kinds.DANGER;
|
||||
break;
|
||||
default:
|
||||
case 'warning':
|
||||
kind = kinds.WARNING;
|
||||
break;
|
||||
case 'notice':
|
||||
kind = kinds.INFO;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={`health${item.message}`}>
|
||||
<TableRowCell>
|
||||
<Icon
|
||||
name={icons.DANGER}
|
||||
kind={kind}
|
||||
title={titleCase(item.type)}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>{item.message}</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<IconButton
|
||||
name={icons.WIKI}
|
||||
to={item.wikiUrl}
|
||||
title={translate('ReadTheWikiForMoreInformation')}
|
||||
/>
|
||||
|
||||
{
|
||||
internalLink
|
||||
}
|
||||
|
||||
{
|
||||
!!testLink &&
|
||||
testLink
|
||||
}
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
{
|
||||
healthIssues &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
<InlineMarkdown data={translate('HealthMessagesInfoBox', { link: '/system/logs/files' })} />
|
||||
</Alert>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Health.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
isTestingAllDownloadClients: PropTypes.bool.isRequired,
|
||||
isTestingAllIndexers: PropTypes.bool.isRequired,
|
||||
dispatchTestAllDownloadClients: PropTypes.func.isRequired,
|
||||
dispatchTestAllIndexers: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Health;
|
|
@ -0,0 +1,174 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
testAllDownloadClients,
|
||||
testAllIndexers,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import createHealthSelector from './createHealthSelector';
|
||||
import HealthItemLink from './HealthItemLink';
|
||||
import styles from './Health.css';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
className: styles.status,
|
||||
name: 'type',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
label: () => translate('Message'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: () => translate('Actions'),
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
function Health() {
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, isPopulated, items } = useSelector(
|
||||
createHealthSelector()
|
||||
);
|
||||
const isTestingAllDownloadClients = useSelector(
|
||||
(state: AppState) => state.settings.downloadClients.isTestingAll
|
||||
);
|
||||
const isTestingAllIndexers = useSelector(
|
||||
(state: AppState) => state.settings.indexers.isTestingAll
|
||||
);
|
||||
|
||||
const healthIssues = !!items.length;
|
||||
|
||||
const handleTestAllDownloadClientsPress = useCallback(() => {
|
||||
dispatch(testAllDownloadClients());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleTestAllIndexersPress = useCallback(() => {
|
||||
dispatch(testAllIndexers());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchHealth());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet
|
||||
legend={
|
||||
<div className={styles.legend}>
|
||||
{translate('Health')}
|
||||
|
||||
{isFetching && isPopulated ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{isPopulated && !healthIssues ? (
|
||||
<div className={styles.healthOk}>
|
||||
{translate('NoIssuesWithYourConfiguration')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{healthIssues ? (
|
||||
<>
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
const source = item.source;
|
||||
|
||||
let kind = kinds.WARNING;
|
||||
switch (item.type.toLowerCase()) {
|
||||
case 'error':
|
||||
kind = kinds.DANGER;
|
||||
break;
|
||||
default:
|
||||
case 'warning':
|
||||
kind = kinds.WARNING;
|
||||
break;
|
||||
case 'notice':
|
||||
kind = kinds.INFO;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={`health${item.message}`}>
|
||||
<TableRowCell>
|
||||
<Icon
|
||||
name={icons.DANGER}
|
||||
kind={kind}
|
||||
title={titleCase(item.type)}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>{item.message}</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<IconButton
|
||||
name={icons.WIKI}
|
||||
to={item.wikiUrl}
|
||||
title={translate('ReadTheWikiForMoreInformation')}
|
||||
/>
|
||||
|
||||
<HealthItemLink source={source} />
|
||||
|
||||
{source === 'IndexerStatusCheck' ||
|
||||
source === 'IndexerLongTermStatusCheck' ? (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
title={translate('TestAll')}
|
||||
isSpinning={isTestingAllIndexers}
|
||||
onPress={handleTestAllIndexersPress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{source === 'DownloadClientCheck' ||
|
||||
source === 'DownloadClientStatusCheck' ? (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
title={translate('TestAll')}
|
||||
isSpinning={isTestingAllDownloadClients}
|
||||
onPress={handleTestAllDownloadClientsPress}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<InlineMarkdown
|
||||
data={translate('HealthMessagesInfoBox', {
|
||||
link: '/system/logs/files',
|
||||
})}
|
||||
/>
|
||||
</Alert>
|
||||
</>
|
||||
) : null}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default Health;
|
|
@ -1,68 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { testAllDownloadClients, testAllIndexers } from 'Store/Actions/settingsActions';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import Health from './Health';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.health,
|
||||
(state) => state.settings.downloadClients.isTestingAll,
|
||||
(state) => state.settings.indexers.isTestingAll,
|
||||
(health, isTestingAllDownloadClients, isTestingAllIndexers) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items
|
||||
} = health;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items,
|
||||
isTestingAllDownloadClients,
|
||||
isTestingAllIndexers
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchHealth: fetchHealth,
|
||||
dispatchTestAllDownloadClients: testAllDownloadClients,
|
||||
dispatchTestAllIndexers: testAllIndexers
|
||||
};
|
||||
|
||||
class HealthConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchHealth();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchHealth,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Health
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HealthConnector.propTypes = {
|
||||
dispatchFetchHealth: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(HealthConnector);
|
|
@ -0,0 +1,65 @@
|
|||
import React from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface HealthItemLinkProps {
|
||||
source: string;
|
||||
}
|
||||
|
||||
function HealthItemLink(props: HealthItemLinkProps) {
|
||||
const { source } = props;
|
||||
|
||||
switch (source) {
|
||||
case 'IndexerRssCheck':
|
||||
case 'IndexerSearchCheck':
|
||||
case 'IndexerStatusCheck':
|
||||
case 'IndexerJackettAllCheck':
|
||||
case 'IndexerLongTermStatusCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title={translate('Settings')}
|
||||
to="/settings/indexers"
|
||||
/>
|
||||
);
|
||||
case 'DownloadClientCheck':
|
||||
case 'DownloadClientStatusCheck':
|
||||
case 'ImportMechanismCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title={translate('Settings')}
|
||||
to="/settings/downloadclients"
|
||||
/>
|
||||
);
|
||||
case 'NotificationStatusCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title={translate('Settings')}
|
||||
to="/settings/connect"
|
||||
/>
|
||||
);
|
||||
case 'RootFolderCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SERIES_CONTINUING}
|
||||
title={translate('SeriesEditor')}
|
||||
to="/serieseditor"
|
||||
/>
|
||||
);
|
||||
case 'UpdateCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.UPDATE}
|
||||
title={translate('Updates')}
|
||||
to="/system/updates"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default HealthItemLink;
|
|
@ -0,0 +1,56 @@
|
|||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import createHealthSelector from './createHealthSelector';
|
||||
|
||||
function HealthStatus() {
|
||||
const dispatch = useDispatch();
|
||||
const { isConnected, isReconnecting } = useSelector(
|
||||
(state: AppState) => state.app
|
||||
);
|
||||
const { isPopulated, items } = useSelector(createHealthSelector());
|
||||
|
||||
const wasReconnecting = usePrevious(isReconnecting);
|
||||
|
||||
const { count, errors, warnings } = useMemo(() => {
|
||||
let errors = false;
|
||||
let warnings = false;
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item.type === 'error') {
|
||||
errors = true;
|
||||
}
|
||||
|
||||
if (item.type === 'warning') {
|
||||
warnings = true;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
count: items.length,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPopulated) {
|
||||
dispatch(fetchHealth());
|
||||
}
|
||||
}, [isPopulated, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && wasReconnecting) {
|
||||
dispatch(fetchHealth());
|
||||
}
|
||||
}, [isConnected, wasReconnecting, dispatch]);
|
||||
|
||||
return (
|
||||
<PageSidebarStatus count={count} errors={errors} warnings={warnings} />
|
||||
);
|
||||
}
|
||||
|
||||
export default HealthStatus;
|
|
@ -1,79 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app,
|
||||
(state) => state.system.health,
|
||||
(app, health) => {
|
||||
const count = health.items.length;
|
||||
let errors = false;
|
||||
let warnings = false;
|
||||
|
||||
health.items.forEach((item) => {
|
||||
if (item.type === 'error') {
|
||||
errors = true;
|
||||
}
|
||||
|
||||
if (item.type === 'warning') {
|
||||
warnings = true;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isConnected: app.isConnected,
|
||||
isReconnecting: app.isReconnecting,
|
||||
isPopulated: health.isPopulated,
|
||||
count,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchHealth
|
||||
};
|
||||
|
||||
class HealthStatusConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.fetchHealth();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.isConnected && prevProps.isReconnecting) {
|
||||
this.props.fetchHealth();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageSidebarStatus
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HealthStatusConnector.propTypes = {
|
||||
isConnected: PropTypes.bool.isRequired,
|
||||
isReconnecting: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
fetchHealth: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(HealthStatusConnector);
|
|
@ -0,0 +1,13 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createHealthSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.system.health,
|
||||
(health) => {
|
||||
return health;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createHealthSelector;
|
|
@ -1,101 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Link from 'Components/Link/Link';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class MoreInfo extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FieldSet legend={translate('MoreInfo')}>
|
||||
<DescriptionList>
|
||||
<DescriptionListItemTitle>
|
||||
{translate('HomePage')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://sonarr.tv/">sonarr.tv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Wiki')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://wiki.servarr.com/sonarr">wiki.servarr.com/sonarr</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Forums')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://forums.sonarr.tv/">forums.sonarr.tv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Twitter')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://twitter.com/sonarrtv">@sonarrtv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Discord')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://discord.sonarr.tv/">discord.sonarr.tv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('IRC')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="irc://irc.libera.chat/#sonarr">
|
||||
{translate('IRCLinkText')}
|
||||
</Link>
|
||||
</DescriptionListItemDescription>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://web.libera.chat/?channels=#sonarr">
|
||||
{translate('LiberaWebchat')}
|
||||
</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Donations')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://sonarr.tv/donate">sonarr.tv/donate</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Source')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://github.com/Sonarr/Sonarr/">github.com/Sonarr/Sonarr</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('FeatureRequests')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://forums.sonarr.tv/">forums.sonarr.tv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://github.com/Sonarr/Sonarr/issues">github.com/Sonarr/Sonarr/issues</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
</DescriptionList>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MoreInfo.propTypes = {
|
||||
|
||||
};
|
||||
|
||||
export default MoreInfo;
|
|
@ -0,0 +1,92 @@
|
|||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Link from 'Components/Link/Link';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function MoreInfo() {
|
||||
return (
|
||||
<FieldSet legend={translate('MoreInfo')}>
|
||||
<DescriptionList>
|
||||
<DescriptionListItemTitle>
|
||||
{translate('HomePage')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://sonarr.tv/">sonarr.tv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>{translate('Wiki')}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://wiki.servarr.com/sonarr">
|
||||
wiki.servarr.com/sonarr
|
||||
</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Forums')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://forums.sonarr.tv/">forums.sonarr.tv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Twitter')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://twitter.com/sonarrtv">@sonarrtv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Discord')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://discord.sonarr.tv/">discord.sonarr.tv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>{translate('IRC')}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="irc://irc.libera.chat/#sonarr">
|
||||
{translate('IRCLinkText')}
|
||||
</Link>
|
||||
</DescriptionListItemDescription>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://web.libera.chat/?channels=#sonarr">
|
||||
{translate('LiberaWebchat')}
|
||||
</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Donations')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://sonarr.tv/donate">sonarr.tv/donate</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('Source')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://github.com/Sonarr/Sonarr/">
|
||||
github.com/Sonarr/Sonarr
|
||||
</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>
|
||||
{translate('FeatureRequests')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://forums.sonarr.tv/">forums.sonarr.tv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://github.com/Sonarr/Sonarr/issues">
|
||||
github.com/Sonarr/Sonarr/issues
|
||||
</Link>
|
||||
</DescriptionListItemDescription>
|
||||
</DescriptionList>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoreInfo;
|
|
@ -2,13 +2,12 @@ import React, { Component } from 'react';
|
|||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AboutConnector from './About/AboutConnector';
|
||||
import DiskSpaceConnector from './DiskSpace/DiskSpaceConnector';
|
||||
import HealthConnector from './Health/HealthConnector';
|
||||
import About from './About/About';
|
||||
import DiskSpace from './DiskSpace/DiskSpace';
|
||||
import Health from './Health/Health';
|
||||
import MoreInfo from './MoreInfo/MoreInfo';
|
||||
|
||||
class Status extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -16,15 +15,14 @@ class Status extends Component {
|
|||
return (
|
||||
<PageContent title={translate('Status')}>
|
||||
<PageContentBody>
|
||||
<HealthConnector />
|
||||
<DiskSpaceConnector />
|
||||
<AboutConnector />
|
||||
<Health />
|
||||
<DiskSpace />
|
||||
<About />
|
||||
<MoreInfo />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Status;
|
|
@ -1,203 +0,0 @@
|
|||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import styles from './ScheduledTaskRow.css';
|
||||
|
||||
function getFormattedDates(props) {
|
||||
const {
|
||||
lastExecution,
|
||||
nextExecution,
|
||||
interval,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
} = props;
|
||||
|
||||
const isDisabled = interval === 0;
|
||||
|
||||
if (showRelativeDates) {
|
||||
return {
|
||||
lastExecutionTime: moment(lastExecution).fromNow(),
|
||||
nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lastExecutionTime: formatDate(lastExecution, shortDateFormat),
|
||||
nextExecutionTime: isDisabled ? '-' : formatDate(nextExecution, shortDateFormat)
|
||||
};
|
||||
}
|
||||
|
||||
class ScheduledTaskRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = getFormattedDates(props);
|
||||
|
||||
this._updateTimeoutId = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setUpdateTimer();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
lastExecution,
|
||||
nextExecution
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
lastExecution !== prevProps.lastExecution ||
|
||||
nextExecution !== prevProps.nextExecution
|
||||
) {
|
||||
this.setState(getFormattedDates(this.props));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._updateTimeoutId) {
|
||||
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
setUpdateTimer() {
|
||||
const { interval } = this.props;
|
||||
const timeout = interval < 60 ? 10000 : 60000;
|
||||
|
||||
this._updateTimeoutId = setTimeout(() => {
|
||||
this.setState(getFormattedDates(this.props));
|
||||
this.setUpdateTimer();
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
interval,
|
||||
lastExecution,
|
||||
lastStartTime,
|
||||
lastDuration,
|
||||
nextExecution,
|
||||
isQueued,
|
||||
isExecuting,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onExecutePress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
lastExecutionTime,
|
||||
nextExecutionTime
|
||||
} = this.state;
|
||||
|
||||
const isDisabled = interval === 0;
|
||||
const executeNow = !isDisabled && moment().isAfter(nextExecution);
|
||||
const hasNextExecutionTime = !isDisabled && !executeNow;
|
||||
const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1');
|
||||
const hasLastStartTime = moment(lastStartTime).isAfter('2010-01-01');
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>{name}</TableRowCell>
|
||||
<TableRowCell
|
||||
className={styles.interval}
|
||||
>
|
||||
{isDisabled ? 'disabled' : duration}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.lastExecution}
|
||||
title={formatDateTime(lastExecution, longDateFormat, timeFormat)}
|
||||
>
|
||||
{lastExecutionTime}
|
||||
</TableRowCell>
|
||||
|
||||
{
|
||||
!hasLastStartTime &&
|
||||
<TableRowCell className={styles.lastDuration}>-</TableRowCell>
|
||||
}
|
||||
|
||||
{
|
||||
hasLastStartTime &&
|
||||
<TableRowCell
|
||||
className={styles.lastDuration}
|
||||
title={lastDuration}
|
||||
>
|
||||
{formatTimeSpan(lastDuration)}
|
||||
</TableRowCell>
|
||||
}
|
||||
|
||||
{
|
||||
isDisabled &&
|
||||
<TableRowCell className={styles.nextExecution}>-</TableRowCell>
|
||||
}
|
||||
|
||||
{
|
||||
executeNow && isQueued &&
|
||||
<TableRowCell className={styles.nextExecution}>queued</TableRowCell>
|
||||
}
|
||||
|
||||
{
|
||||
executeNow && !isQueued &&
|
||||
<TableRowCell className={styles.nextExecution}>now</TableRowCell>
|
||||
}
|
||||
|
||||
{
|
||||
hasNextExecutionTime &&
|
||||
<TableRowCell
|
||||
className={styles.nextExecution}
|
||||
title={formatDateTime(nextExecution, longDateFormat, timeFormat, { includeSeconds: true })}
|
||||
>
|
||||
{nextExecutionTime}
|
||||
</TableRowCell>
|
||||
}
|
||||
|
||||
<TableRowCell
|
||||
className={styles.actions}
|
||||
>
|
||||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isExecuting}
|
||||
onPress={onExecutePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScheduledTaskRow.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
interval: PropTypes.number.isRequired,
|
||||
lastExecution: PropTypes.string.isRequired,
|
||||
lastStartTime: PropTypes.string.isRequired,
|
||||
lastDuration: PropTypes.string.isRequired,
|
||||
nextExecution: PropTypes.string.isRequired,
|
||||
isQueued: PropTypes.bool.isRequired,
|
||||
isExecuting: PropTypes.bool.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onExecutePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ScheduledTaskRow;
|
|
@ -0,0 +1,170 @@
|
|||
import moment from 'moment';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchTask } from 'Store/Actions/systemActions';
|
||||
import createCommandSelector from 'Store/Selectors/createCommandSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import styles from './ScheduledTaskRow.css';
|
||||
|
||||
interface ScheduledTaskRowProps {
|
||||
id: number;
|
||||
taskName: string;
|
||||
name: string;
|
||||
interval: number;
|
||||
lastExecution: string;
|
||||
lastStartTime: string;
|
||||
lastDuration: string;
|
||||
nextExecution: string;
|
||||
}
|
||||
|
||||
function ScheduledTaskRow(props: ScheduledTaskRowProps) {
|
||||
const {
|
||||
id,
|
||||
taskName,
|
||||
name,
|
||||
interval,
|
||||
lastExecution,
|
||||
lastStartTime,
|
||||
lastDuration,
|
||||
nextExecution,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { showRelativeDates, longDateFormat, shortDateFormat, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
const command = useSelector(createCommandSelector(taskName));
|
||||
|
||||
const [time, setTime] = useState(Date.now());
|
||||
|
||||
const isQueued = !!(command && command.status === 'queued');
|
||||
const isExecuting = isCommandExecuting(command);
|
||||
const wasExecuting = usePrevious(isExecuting);
|
||||
const isDisabled = interval === 0;
|
||||
const executeNow = !isDisabled && moment().isAfter(nextExecution);
|
||||
const hasNextExecutionTime = !isDisabled && !executeNow;
|
||||
const hasLastStartTime = moment(lastStartTime).isAfter('2010-01-01');
|
||||
|
||||
const duration = useMemo(() => {
|
||||
return moment
|
||||
.duration(interval, 'minutes')
|
||||
.humanize()
|
||||
.replace(/an?(?=\s)/, '1');
|
||||
}, [interval]);
|
||||
|
||||
const { lastExecutionTime, nextExecutionTime } = useMemo(() => {
|
||||
const isDisabled = interval === 0;
|
||||
|
||||
if (showRelativeDates && time) {
|
||||
return {
|
||||
lastExecutionTime: moment(lastExecution).fromNow(),
|
||||
nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lastExecutionTime: formatDate(lastExecution, shortDateFormat),
|
||||
nextExecutionTime: isDisabled
|
||||
? '-'
|
||||
: formatDate(nextExecution, shortDateFormat),
|
||||
};
|
||||
}, [
|
||||
time,
|
||||
interval,
|
||||
lastExecution,
|
||||
nextExecution,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
]);
|
||||
|
||||
const handleExecutePress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: taskName,
|
||||
})
|
||||
);
|
||||
}, [taskName, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExecuting && wasExecuting) {
|
||||
setTimeout(() => {
|
||||
dispatch(fetchTask({ id }));
|
||||
}, 1000);
|
||||
}
|
||||
}, [id, isExecuting, wasExecuting, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setTime(Date.now()), 1000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [setTime]);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>{name}</TableRowCell>
|
||||
<TableRowCell className={styles.interval}>
|
||||
{isDisabled ? 'disabled' : duration}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.lastExecution}
|
||||
title={formatDateTime(lastExecution, longDateFormat, timeFormat)}
|
||||
>
|
||||
{lastExecutionTime}
|
||||
</TableRowCell>
|
||||
|
||||
{hasLastStartTime ? (
|
||||
<TableRowCell className={styles.lastDuration} title={lastDuration}>
|
||||
{formatTimeSpan(lastDuration)}
|
||||
</TableRowCell>
|
||||
) : (
|
||||
<TableRowCell className={styles.lastDuration}>-</TableRowCell>
|
||||
)}
|
||||
|
||||
{isDisabled ? (
|
||||
<TableRowCell className={styles.nextExecution}>-</TableRowCell>
|
||||
) : null}
|
||||
|
||||
{executeNow && isQueued ? (
|
||||
<TableRowCell className={styles.nextExecution}>queued</TableRowCell>
|
||||
) : null}
|
||||
|
||||
{executeNow && !isQueued ? (
|
||||
<TableRowCell className={styles.nextExecution}>now</TableRowCell>
|
||||
) : null}
|
||||
|
||||
{hasNextExecutionTime ? (
|
||||
<TableRowCell
|
||||
className={styles.nextExecution}
|
||||
title={formatDateTime(nextExecution, longDateFormat, timeFormat, {
|
||||
includeSeconds: true,
|
||||
})}
|
||||
>
|
||||
{nextExecutionTime}
|
||||
</TableRowCell>
|
||||
) : null}
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isExecuting}
|
||||
onPress={handleExecutePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScheduledTaskRow;
|
|
@ -1,92 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchTask } from 'Store/Actions/systemActions';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||
import ScheduledTaskRow from './ScheduledTaskRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { taskName }) => taskName,
|
||||
createCommandsSelector(),
|
||||
createUISettingsSelector(),
|
||||
(taskName, commands, uiSettings) => {
|
||||
const command = findCommand(commands, { name: taskName });
|
||||
|
||||
return {
|
||||
isQueued: !!(command && command.state === 'queued'),
|
||||
isExecuting: isCommandExecuting(command),
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
const taskName = props.taskName;
|
||||
|
||||
return {
|
||||
dispatchFetchTask() {
|
||||
dispatch(fetchTask({
|
||||
id: props.id
|
||||
}));
|
||||
},
|
||||
|
||||
onExecutePress() {
|
||||
dispatch(executeCommand({
|
||||
name: taskName
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ScheduledTaskRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isExecuting,
|
||||
dispatchFetchTask
|
||||
} = this.props;
|
||||
|
||||
if (!isExecuting && prevProps.isExecuting) {
|
||||
// Give the host a moment to update after the command completes
|
||||
setTimeout(() => {
|
||||
dispatchFetchTask();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchTask,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ScheduledTaskRow
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScheduledTaskRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
isExecuting: PropTypes.bool.isRequired,
|
||||
dispatchFetchTask: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(ScheduledTaskRowConnector);
|
|
@ -1,85 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'name',
|
||||
label: () => translate('Name'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'interval',
|
||||
label: () => translate('Interval'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'lastExecution',
|
||||
label: () => translate('LastExecution'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'lastDuration',
|
||||
label: () => translate('LastDuration'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'nextExecution',
|
||||
label: () => translate('NextExecution'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function ScheduledTasks(props) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Scheduled')}>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<ScheduledTaskRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
ScheduledTasks.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
items: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default ScheduledTasks;
|
|
@ -0,0 +1,73 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { fetchTasks } from 'Store/Actions/systemActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ScheduledTaskRow from './ScheduledTaskRow';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: () => translate('Name'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'interval',
|
||||
label: () => translate('Interval'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'lastExecution',
|
||||
label: () => translate('LastExecution'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'lastDuration',
|
||||
label: () => translate('LastDuration'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'nextExecution',
|
||||
label: () => translate('NextExecution'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
function ScheduledTasks() {
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, isPopulated, items } = useSelector(
|
||||
(state: AppState) => state.system.tasks
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchTasks());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Scheduled')}>
|
||||
{isFetching && !isPopulated && <LoadingIndicator />}
|
||||
|
||||
{isPopulated && (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return <ScheduledTaskRow key={item.id} {...item} />;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScheduledTasks;
|
|
@ -1,46 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchTasks } from 'Store/Actions/systemActions';
|
||||
import ScheduledTasks from './ScheduledTasks';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.tasks,
|
||||
(tasks) => {
|
||||
return tasks;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchTasks: fetchTasks
|
||||
};
|
||||
|
||||
class ScheduledTasksConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchTasks();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ScheduledTasks
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScheduledTasksConnector.propTypes = {
|
||||
dispatchFetchTasks: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ScheduledTasksConnector);
|
|
@ -3,13 +3,13 @@ import PageContent from 'Components/Page/PageContent';
|
|||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTasks from './Queued/QueuedTasks';
|
||||
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
||||
import ScheduledTasks from './Scheduled/ScheduledTasks';
|
||||
|
||||
function Tasks() {
|
||||
return (
|
||||
<PageContent title={translate('Tasks')}>
|
||||
<PageContentBody>
|
||||
<ScheduledTasksConnector />
|
||||
<ScheduledTasks />
|
||||
<QueuedTasks />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
|
@ -1,10 +1,4 @@
|
|||
import React, {
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
@ -158,7 +152,7 @@ function Updates() {
|
|||
{translate('InstallLatest')}
|
||||
</SpinnerButton>
|
||||
) : (
|
||||
<Fragment>
|
||||
<>
|
||||
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
|
||||
|
||||
<div className={styles.message}>
|
||||
|
@ -171,7 +165,7 @@ function Updates() {
|
|||
}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isFetching ? (
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { filesize } from 'filesize';
|
||||
|
||||
function formatBytes(input?: string | number) {
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatBytes(input: string | number) {
|
||||
const size = Number(input);
|
||||
|
||||
if (isNaN(size)) {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
interface DiskSpace {
|
||||
path: string;
|
||||
label: string;
|
||||
freeSpace: number;
|
||||
totalSpace: number;
|
||||
}
|
||||
|
||||
export default DiskSpace;
|
|
@ -0,0 +1,8 @@
|
|||
interface Health {
|
||||
source: string;
|
||||
type: string;
|
||||
message: string;
|
||||
wikiUrl: string;
|
||||
}
|
||||
|
||||
export default Health;
|
|
@ -4,6 +4,8 @@ interface SystemStatus {
|
|||
authentication: string;
|
||||
branch: string;
|
||||
buildTime: string;
|
||||
databaseVersion: string;
|
||||
databaseType: string;
|
||||
instanceName: string;
|
||||
isAdmin: boolean;
|
||||
isDebug: boolean;
|
||||
|
@ -18,8 +20,10 @@ interface SystemStatus {
|
|||
mode: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
packageAuthor: string;
|
||||
packageUpdateMechanism: string;
|
||||
packageUpdateMechanismMessage: string;
|
||||
packageVersion: string;
|
||||
runtimeName: string;
|
||||
runtimeVersion: string;
|
||||
sqliteVersion: string;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
interface Task extends ModelBase {
|
||||
name: string;
|
||||
taskName: string;
|
||||
interval: number;
|
||||
lastExecution: string;
|
||||
lastStartTime: string;
|
||||
nextExecution: string;
|
||||
lastDuration: string;
|
||||
}
|
||||
|
||||
export default Task;
|
|
@ -1,11 +1,11 @@
|
|||
using FluentAssertions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Common.Test.ExtensionTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class Int64ExtensionFixture
|
||||
public class NumberExtensionFixture
|
||||
{
|
||||
[TestCase(0, "0 B")]
|
||||
[TestCase(1000, "1,000.0 B")]
|
|
@ -380,8 +380,17 @@ namespace NzbDrone.Common.Test
|
|||
[TestCase(@" C:\Test\TV\")]
|
||||
[TestCase(@" C:\Test\TV")]
|
||||
|
||||
public void IsPathValid_should_be_false(string path)
|
||||
public void IsPathValid_should_be_false_on_windows(string path)
|
||||
{
|
||||
WindowsOnly();
|
||||
path.IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase(@"")]
|
||||
[TestCase(@"relative/path")]
|
||||
public void IsPathValid_should_be_false_on_unix(string path)
|
||||
{
|
||||
PosixOnly();
|
||||
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
public static class Int64Extensions
|
||||
public static class NumberExtensions
|
||||
{
|
||||
private static readonly string[] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
|
||||
|
||||
|
@ -26,5 +26,25 @@ namespace NzbDrone.Common.Extensions
|
|||
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0:n1} {1}", adjustedSize, SizeSuffixes[mag]);
|
||||
}
|
||||
|
||||
public static long Megabytes(this int megabytes)
|
||||
{
|
||||
return Convert.ToInt64(megabytes * 1024L * 1024L);
|
||||
}
|
||||
|
||||
public static long Gigabytes(this int gigabytes)
|
||||
{
|
||||
return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L);
|
||||
}
|
||||
|
||||
public static long Megabytes(this double megabytes)
|
||||
{
|
||||
return Convert.ToInt64(megabytes * 1024L * 1024L);
|
||||
}
|
||||
|
||||
public static long Gigabytes(this double gigabytes)
|
||||
{
|
||||
return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -152,16 +152,20 @@ namespace NzbDrone.Common.Extensions
|
|||
return false;
|
||||
}
|
||||
|
||||
var directoryInfo = new DirectoryInfo(path);
|
||||
|
||||
while (directoryInfo != null)
|
||||
// Only check for leading or trailing spaces for path when running on Windows.
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
if (directoryInfo.Name.Trim() != directoryInfo.Name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var directoryInfo = new DirectoryInfo(path);
|
||||
|
||||
directoryInfo = directoryInfo.Parent;
|
||||
while (directoryInfo != null)
|
||||
{
|
||||
if (directoryInfo.Name.Trim() != directoryInfo.Name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
directoryInfo = directoryInfo.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
if (validationType == PathValidationType.AnyOs)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NLog.Fluent;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation.Extensions
|
||||
{
|
||||
|
@ -8,47 +8,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
|
|||
{
|
||||
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
|
||||
|
||||
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return logBuilder.Property("Sentry", fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
|
||||
}
|
||||
|
||||
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||
private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||
{
|
||||
SentryLogger.Log(level)
|
||||
.CopyLogEvent(logBuilder.LogEventInfo)
|
||||
SentryLogger.ForLogEvent(level)
|
||||
.CopyLogEvent(logBuilder.LogEvent)
|
||||
.SentryFingerprint(fingerprint)
|
||||
.Write();
|
||||
.Log();
|
||||
|
||||
return logBuilder.Property("Sentry", null);
|
||||
return logBuilder.Property<string>("Sentry", null);
|
||||
}
|
||||
|
||||
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
|
||||
private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent)
|
||||
{
|
||||
return logBuilder.LoggerName(logEvent.LoggerName)
|
||||
.TimeStamp(logEvent.TimeStamp)
|
||||
return logBuilder.TimeStamp(logEvent.TimeStamp)
|
||||
.Message(logEvent.Message, logEvent.Parameters)
|
||||
.Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
|
||||
.Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value)))
|
||||
.Exception(logEvent.Exception);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
using NLog;
|
||||
using System.Text;
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation
|
||||
{
|
||||
public class NzbDroneFileTarget : FileTarget
|
||||
{
|
||||
protected override string GetFormattedMessage(LogEventInfo logEvent)
|
||||
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
|
||||
{
|
||||
return CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||
var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||
target.Append(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using NLog;
|
||||
using NLog.Config;
|
||||
using NLog.Layouts.ClefJsonLayout;
|
||||
using NLog.Targets;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
@ -13,6 +14,8 @@ namespace NzbDrone.Common.Instrumentation
|
|||
public static class NzbDroneLogger
|
||||
{
|
||||
private const string FILE_LOG_LAYOUT = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}}";
|
||||
public const string ConsoleLogLayout = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}}";
|
||||
public static CompactJsonLayout ClefLogLayout = new CompactJsonLayout();
|
||||
|
||||
private static bool _isConfigured;
|
||||
|
||||
|
@ -34,6 +37,8 @@ namespace NzbDrone.Common.Instrumentation
|
|||
|
||||
var appFolderInfo = new AppFolderInfo(startupContext);
|
||||
|
||||
RegisterGlobalFilters();
|
||||
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
RegisterDebugger();
|
||||
|
@ -122,7 +127,16 @@ namespace NzbDrone.Common.Instrumentation
|
|||
var coloredConsoleTarget = new ColoredConsoleTarget();
|
||||
|
||||
coloredConsoleTarget.Name = "consoleLogger";
|
||||
coloredConsoleTarget.Layout = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}}";
|
||||
|
||||
var logFormat = Enum.TryParse<ConsoleLogFormat>(Environment.GetEnvironmentVariable("SONARR__LOG__CONSOLEFORMAT"), out var formatEnumValue)
|
||||
? formatEnumValue
|
||||
: ConsoleLogFormat.Standard;
|
||||
|
||||
coloredConsoleTarget.Layout = logFormat switch
|
||||
{
|
||||
ConsoleLogFormat.Clef => ClefLogLayout,
|
||||
_ => ConsoleLogLayout
|
||||
};
|
||||
|
||||
var loggingRule = new LoggingRule("*", level, coloredConsoleTarget);
|
||||
|
||||
|
@ -148,7 +162,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||
fileTarget.ConcurrentWrites = false;
|
||||
fileTarget.ConcurrentWriteAttemptDelay = 50;
|
||||
fileTarget.ConcurrentWriteAttempts = 10;
|
||||
fileTarget.ArchiveAboveSize = 1024000;
|
||||
fileTarget.ArchiveAboveSize = 1.Megabytes();
|
||||
fileTarget.MaxArchiveFiles = maxArchiveFiles;
|
||||
fileTarget.EnableFileDelete = true;
|
||||
fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling;
|
||||
|
@ -196,6 +210,17 @@ namespace NzbDrone.Common.Instrumentation
|
|||
LogManager.Configuration.LoggingRules.Insert(0, rule);
|
||||
}
|
||||
|
||||
private static void RegisterGlobalFilters()
|
||||
{
|
||||
LogManager.Setup().LoadConfiguration(c =>
|
||||
{
|
||||
c.ForLogger("System.*").WriteToNil(LogLevel.Warn);
|
||||
c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn);
|
||||
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
|
||||
c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal);
|
||||
});
|
||||
}
|
||||
|
||||
public static Logger GetLogger(Type obj)
|
||||
{
|
||||
return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", ""));
|
||||
|
@ -206,4 +231,10 @@ namespace NzbDrone.Common.Instrumentation
|
|||
return GetLogger(obj.GetType());
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConsoleLogFormat
|
||||
{
|
||||
Standard,
|
||||
Clef
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@ public class LogOptions
|
|||
public string Level { get; set; }
|
||||
public bool? FilterSentryEvents { get; set; }
|
||||
public int? Rotate { get; set; }
|
||||
public int? SizeLimit { get; set; }
|
||||
public bool? Sql { get; set; }
|
||||
public string ConsoleLevel { get; set; }
|
||||
public string ConsoleFormat { get; set; }
|
||||
public bool? AnalyticsEnabled { get; set; }
|
||||
public string SyslogServer { get; set; }
|
||||
public int? SyslogPort { get; set; }
|
||||
|
|
|
@ -8,9 +8,10 @@
|
|||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.14" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||
<PackageReference Include="NLog" Version="5.3.2" />
|
||||
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.0" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.11" />
|
||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
||||
|
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
|||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
|
|
|
@ -4,6 +4,7 @@ using FizzWare.NBuilder;
|
|||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
|
|
@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
};
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()))
|
||||
.Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()))
|
||||
.Returns(_remoteEpisode);
|
||||
}
|
||||
|
||||
|
@ -154,7 +154,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
|
||||
Subject.GetRssDecision(_reports).ToList();
|
||||
|
||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
||||
|
||||
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||
|
@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
|
||||
var results = Subject.GetRssDecision(_reports).ToList();
|
||||
|
||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
||||
|
||||
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||
|
@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
|
||||
Subject.GetSearchDecision(_reports, new SingleEpisodeSearchCriteria()).ToList();
|
||||
|
||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
||||
|
||||
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||
|
@ -212,7 +212,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
{
|
||||
GivenSpecifications(_pass1);
|
||||
|
||||
Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()))
|
||||
Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()))
|
||||
.Throws<TestException>();
|
||||
|
||||
_reports = new List<ReleaseInfo>
|
||||
|
@ -224,7 +224,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
|
||||
Subject.GetRssDecision(_reports);
|
||||
|
||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Exactly(_reports.Count));
|
||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Exactly(_reports.Count));
|
||||
|
||||
ExceptionVerification.ExpectedErrors(3);
|
||||
}
|
||||
|
@ -263,8 +263,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
}).ToList();
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(v => v.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()))
|
||||
.Returns<ParsedEpisodeInfo, int, int, SearchCriteriaBase>((p, tvdbid, tvrageid, c) =>
|
||||
.Setup(v => v.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()))
|
||||
.Returns<ParsedEpisodeInfo, int, int, string, SearchCriteriaBase>((p, _, _, _, _) =>
|
||||
new RemoteEpisode
|
||||
{
|
||||
DownloadAllowed = true,
|
||||
|
@ -318,7 +318,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
{
|
||||
GivenSpecifications(_pass1);
|
||||
|
||||
Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()))
|
||||
Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()))
|
||||
.Throws<TestException>();
|
||||
|
||||
_reports = new List<ReleaseInfo>
|
||||
|
|
|
@ -4,6 +4,8 @@ using FizzWare.NBuilder;
|
|||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Download.Aggregation.Aggregators;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.TorrentRss;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
@ -62,6 +64,63 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
|||
Subject.Aggregate(_remoteEpisode).Languages.Should().Equal(_remoteEpisode.ParsedEpisodeInfo.Languages);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_multi_languages_when_indexer_has_multi_languages_configuration()
|
||||
{
|
||||
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||
var indexerDefinition = new IndexerDefinition
|
||||
{
|
||||
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||
};
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(v => v.Get(1))
|
||||
.Returns(indexerDefinition);
|
||||
|
||||
_remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle);
|
||||
_remoteEpisode.Release.IndexerId = 1;
|
||||
_remoteEpisode.Release.Title = releaseTitle;
|
||||
|
||||
Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_multi_languages_when_release_as_unknown_as_default_language_and_indexer_has_multi_languages_configuration()
|
||||
{
|
||||
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||
var indexerDefinition = new IndexerDefinition
|
||||
{
|
||||
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||
};
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(v => v.Get(1))
|
||||
.Returns(indexerDefinition);
|
||||
|
||||
_remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { Language.Unknown }, releaseTitle);
|
||||
_remoteEpisode.Release.IndexerId = 1;
|
||||
_remoteEpisode.Release.Title = releaseTitle;
|
||||
|
||||
Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_original_when_indexer_has_no_multi_languages_configuration()
|
||||
{
|
||||
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||
var indexerDefinition = new IndexerDefinition
|
||||
{
|
||||
Settings = new TorrentRssIndexerSettings { }
|
||||
};
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(v => v.Get(1))
|
||||
.Returns(indexerDefinition);
|
||||
|
||||
_remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle);
|
||||
_remoteEpisode.Release.IndexerId = 1;
|
||||
_remoteEpisode.Release.Title = releaseTitle;
|
||||
|
||||
Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_exclude_language_that_is_part_of_episode_title_when_release_tokens_contains_episode_title()
|
||||
{
|
||||
|
|
|
@ -10,7 +10,6 @@ using NzbDrone.Common.Http;
|
|||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
@ -35,7 +34,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
|
|||
.Returns(30);
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), (SearchCriteriaBase)null))
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||
.Returns(() => CreateRemoteEpisode());
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
|
|
|
@ -49,10 +49,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void magnet_download_should_not_return_the_item()
|
||||
public void magnet_download_should_be_returned_as_queued()
|
||||
{
|
||||
PrepareClientToReturnMagnetItem();
|
||||
Subject.GetItems().Count().Should().Be(0);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
|
||||
item.Status.Should().Be(DownloadItemStatus.Queued);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
|
@ -60,7 +60,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
|
|||
public void magnet_download_should_not_return_the_item()
|
||||
{
|
||||
PrepareClientToReturnMagnetItem();
|
||||
Subject.GetItems().Count().Should().Be(0);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
|
||||
item.Status.Should().Be(DownloadItemStatus.Queued);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
.Returns(remoteEpisode);
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.ParseSpecialEpisodeTitle(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
||||
.Setup(s => s.ParseSpecialEpisodeTitle(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||
.Returns(remoteEpisode.ParsedEpisodeInfo);
|
||||
|
||||
var client = new DownloadClientDefinition()
|
||||
|
@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
};
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||
.Returns(remoteEpisode);
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
|
@ -199,7 +199,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||
.Returns(default(RemoteEpisode));
|
||||
|
||||
Subject.Handle(new EpisodeInfoRefreshedEvent(remoteEpisode.Series, new List<Episode>(), new List<Episode>(), remoteEpisode.Episodes));
|
||||
|
@ -228,7 +228,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
};
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||
.Returns(default(RemoteEpisode));
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
|
@ -258,7 +258,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||
.Returns(default(RemoteEpisode));
|
||||
|
||||
Subject.Handle(new EpisodeInfoRefreshedEvent(remoteEpisode.Series, new List<Episode>(), new List<Episode>(), remoteEpisode.Episodes));
|
||||
|
@ -287,7 +287,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
};
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||
.Returns(default(RemoteEpisode));
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
|
@ -317,7 +317,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||
.Returns(default(RemoteEpisode));
|
||||
|
||||
Subject.Handle(new SeriesDeletedEvent(new List<Series> { remoteEpisode.Series }, true, true));
|
||||
|
|
|
@ -15,6 +15,10 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
|
|||
[TestCase("Franklin & Bash", "Franklin+and+Bash")]
|
||||
[TestCase("Chicago P.D.", "Chicago+PD")]
|
||||
[TestCase("Kourtney And Khlo\u00E9 Take The Hamptons", "Kourtney+And+Khloe+Take+The+Hamptons")]
|
||||
[TestCase("Betty White`s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
|
||||
[TestCase("Betty White\u00b4s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
|
||||
[TestCase("Betty White‘s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
|
||||
[TestCase("Betty White’s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
|
||||
public void should_replace_some_special_characters(string input, string expected)
|
||||
{
|
||||
Subject.SceneTitles = new List<string> { input };
|
||||
|
|
|
@ -7,6 +7,7 @@ using FluentAssertions;
|
|||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
|
|
|
@ -6,6 +6,7 @@ using FluentAssertions;
|
|||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.History;
|
||||
|
@ -74,8 +75,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
|||
.Returns(new List<EpisodeHistory>());
|
||||
|
||||
_downloadClientItem = Builder<DownloadClientItem>.CreateNew()
|
||||
.With(d => d.OutputPath = new OsPath(outputPath))
|
||||
.Build();
|
||||
.With(d => d.OutputPath = new OsPath(outputPath))
|
||||
.Build();
|
||||
}
|
||||
|
||||
private void GivenNewDownload()
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
|
|
@ -233,11 +233,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
|||
GivenEpisodes(actualInfo, actualInfo.EpisodeNumbers);
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(v => v.ParseSpecialEpisodeTitle(fileInfo, It.IsAny<string>(), 0, 0, null))
|
||||
.Setup(v => v.ParseSpecialEpisodeTitle(fileInfo, It.IsAny<string>(), 0, 0, null, null))
|
||||
.Returns(actualInfo);
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(v => v.ParseSpecialEpisodeTitle(folderInfo, It.IsAny<string>(), 0, 0, null))
|
||||
.Setup(v => v.ParseSpecialEpisodeTitle(folderInfo, It.IsAny<string>(), 0, 0, null, null))
|
||||
.Returns(actualInfo);
|
||||
|
||||
Subject.IsSatisfiedBy(localEpisode, null).Accepted.Should().BeTrue();
|
||||
|
|
|
@ -91,7 +91,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
GivenDailySeries();
|
||||
GivenDailyParseResult();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once());
|
||||
|
@ -103,7 +103,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
GivenDailySeries();
|
||||
GivenDailyParseResult();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never());
|
||||
|
@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
GivenDailySeries();
|
||||
_parsedEpisodeInfo.AirDate = DateTime.Today.AddDays(-5).ToString(Episode.AIR_DATE_FORMAT);
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once());
|
||||
|
@ -128,7 +128,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
GivenDailyParseResult();
|
||||
_parsedEpisodeInfo.DailyPart = 1;
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), 1), Times.Once());
|
||||
|
@ -143,7 +143,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
.Setup(s => s.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>()))
|
||||
.Returns(new List<Episode>());
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never());
|
||||
|
@ -154,7 +154,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenSceneNumberingSeries();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
||||
|
@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenSceneNumberingSeries();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
|
||||
|
@ -177,7 +177,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
GivenSceneNumberingSeries();
|
||||
_episodes.First().SceneEpisodeNumber = 10;
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
||||
|
@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
[Test]
|
||||
public void should_find_episode()
|
||||
{
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
||||
|
@ -195,7 +195,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
[Test]
|
||||
public void should_match_episode_with_search_criteria()
|
||||
{
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
|
||||
|
@ -206,7 +206,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
_episodes.First().EpisodeNumber = 10;
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
||||
|
@ -537,7 +537,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
.With(e => e.EpisodeNumber = 1)
|
||||
.Build());
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(_series.TvdbId, 0, 1), Times.Once());
|
||||
|
@ -555,7 +555,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
.Setup(s => s.FindEpisodeByTitle(_series.TvdbId, 0, _parsedEpisodeInfo.ReleaseTitle))
|
||||
.Returns((Episode)null);
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(_series.TvdbId, _parsedEpisodeInfo.SeasonNumber, _parsedEpisodeInfo.EpisodeNumbers.First()), Times.Once());
|
||||
|
|
|
@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenMatchBySeriesTitle();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Once());
|
||||
|
@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenMatchByTvdbId();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once());
|
||||
|
@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenMatchByTvRageId();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, 0, _series.TvRageId);
|
||||
Subject.Map(_parsedEpisodeInfo, 0, _series.TvRageId, null);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Once());
|
||||
|
@ -123,7 +123,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
.Setup(v => v.FindSceneMapping(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
.Returns(new SceneMapping { TvdbId = 10 });
|
||||
|
||||
var result = Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
var result = Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Never());
|
||||
|
@ -136,7 +136,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenMatchBySeriesTitle();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
|
||||
|
@ -147,7 +147,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Once());
|
||||
|
@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
.Setup(s => s.FindByTitle(_parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, _parsedEpisodeInfo.SeriesTitleInfo.Year))
|
||||
.Returns(_series);
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTitle(It.IsAny<string>(), It.IsAny<int>()), Times.Once());
|
||||
|
@ -180,7 +180,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once());
|
||||
|
@ -191,7 +191,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, 0, 10, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, 0, 10, null, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Once());
|
||||
|
@ -202,12 +202,34 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_FindByImdbId_when_search_criteria_and_FindByTitle_matching_fails()
|
||||
{
|
||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, 0, 0, "tt12345", _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByImdbId(It.IsAny<string>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_FindByImdbId_when_search_criteria_and_FindByTitle_matching_fails_and_tvdb_id_is_specified()
|
||||
{
|
||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, 10, 10, "tt12345", _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByImdbId(It.IsAny<string>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_tvdbid_matching_when_alias_is_found()
|
||||
{
|
||||
|
@ -215,7 +237,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
.Setup(s => s.FindTvdbId(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
.Returns(_series.TvdbId);
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
|
||||
|
@ -226,7 +248,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||
{
|
||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
|
||||
|
|
|
@ -185,8 +185,10 @@ namespace NzbDrone.Core.Blocklisting
|
|||
Indexer = message.Data.GetValueOrDefault("indexer"),
|
||||
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
|
||||
Message = message.Message,
|
||||
TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"),
|
||||
Languages = message.Languages
|
||||
Languages = message.Languages,
|
||||
TorrentInfoHash = message.TrackedDownload?.Protocol == DownloadProtocol.Torrent
|
||||
? message.TrackedDownload.DownloadItem.DownloadId
|
||||
: message.Data.GetValueOrDefault("torrentInfoHash", null)
|
||||
};
|
||||
|
||||
if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
|
||||
|
|
|
@ -10,6 +10,7 @@ using NzbDrone.Common.Cache;
|
|||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Common.Options;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
|
@ -38,8 +39,10 @@ namespace NzbDrone.Core.Configuration
|
|||
bool AnalyticsEnabled { get; }
|
||||
string LogLevel { get; }
|
||||
string ConsoleLogLevel { get; }
|
||||
ConsoleLogFormat ConsoleLogFormat { get; }
|
||||
bool LogSql { get; }
|
||||
int LogRotate { get; }
|
||||
int LogSizeLimit { get; }
|
||||
bool FilterSentryEvents { get; }
|
||||
string Branch { get; }
|
||||
string ApiKey { get; }
|
||||
|
@ -220,9 +223,14 @@ namespace NzbDrone.Core.Configuration
|
|||
|
||||
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "main").ToLowerInvariant();
|
||||
|
||||
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant();
|
||||
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "debug").ToLowerInvariant();
|
||||
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
|
||||
public ConsoleLogFormat ConsoleLogFormat =>
|
||||
Enum.TryParse<ConsoleLogFormat>(_logOptions.ConsoleFormat, out var enumValue)
|
||||
? enumValue
|
||||
: GetValueEnum("ConsoleLogFormat", ConsoleLogFormat.Standard, false);
|
||||
|
||||
public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false);
|
||||
|
||||
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
|
||||
|
@ -234,6 +242,7 @@ namespace NzbDrone.Core.Configuration
|
|||
public bool LogDbEnabled => _logOptions.DbEnabled ?? GetValueBoolean("LogDbEnabled", true, persist: false);
|
||||
public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false);
|
||||
public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false);
|
||||
public int LogSizeLimit => Math.Min(Math.Max(_logOptions.SizeLimit ?? GetValueInt("LogSizeLimit", 1, persist: false), 0), 10);
|
||||
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
|
||||
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
|
|
|
@ -63,7 +63,9 @@ namespace NzbDrone.Core.DataAugmentation.Scene
|
|||
sceneSeasonNumbers.Contains(n.SceneSeasonNumber ?? -1) ||
|
||||
((n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1 && n.SceneOrigin != "tvdb"))
|
||||
.Where(n => IsEnglish(n.SearchTerm))
|
||||
.Select(n => n.SearchTerm).Distinct().ToList();
|
||||
.Select(n => n.SearchTerm)
|
||||
.Distinct(StringComparer.InvariantCultureIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
return names;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
|
|
@ -80,7 +80,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
|
||||
if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode)
|
||||
{
|
||||
var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, report.Title, report.TvdbId, report.TvRageId, searchCriteria);
|
||||
var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, report.Title, report.TvdbId, report.TvRageId, report.ImdbId, searchCriteria);
|
||||
|
||||
if (specialEpisodeInfo != null)
|
||||
{
|
||||
|
@ -90,7 +90,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
|
||||
if (parsedEpisodeInfo != null && !parsedEpisodeInfo.SeriesTitle.IsNullOrWhiteSpace())
|
||||
{
|
||||
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvdbId, report.TvRageId, searchCriteria);
|
||||
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvdbId, report.TvRageId, report.ImdbId, searchCriteria);
|
||||
remoteEpisode.Release = report;
|
||||
|
||||
if (remoteEpisode.Series == null)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue