mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-01-13 10:32:21 +02:00
parent
a77ef187af
commit
85e2855981
@ -11,7 +11,7 @@ import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnector';
|
||||
import SeriesIndex from 'Series/Index/SeriesIndex';
|
||||
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
|
||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||
@ -179,7 +179,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/settings/customformats"
|
||||
component={CustomFormatSettingsConnector}
|
||||
component={CustomFormatSettingsPage}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
@ -2,6 +2,7 @@ import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||
import EpisodesAppState from './EpisodesAppState';
|
||||
import ParseAppState from './ParseAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
@ -44,6 +45,7 @@ interface AppState {
|
||||
episodesSelection: EpisodesAppState;
|
||||
episodeFiles: EpisodeFilesAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
parse: ParseAppState;
|
||||
seriesIndex: SeriesIndexAppState;
|
||||
settings: SettingsAppState;
|
||||
series: SeriesAppState;
|
||||
|
54
frontend/src/App/State/ParseAppState.ts
Normal file
54
frontend/src/App/State/ParseAppState.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import { AppSectionItemState } from 'App/State/AppSectionState';
|
||||
import Episode from 'Episode/Episode';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import Series from 'Series/Series';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
|
||||
export interface SeriesTitleInfo {
|
||||
title: string;
|
||||
titleWithoutYear: string;
|
||||
year: number;
|
||||
allTitles: string[];
|
||||
}
|
||||
|
||||
export interface ParsedEpisodeInfo {
|
||||
releaseTitle: string;
|
||||
seriesTitle: string;
|
||||
seriesTitleInfo: SeriesTitleInfo;
|
||||
quality: QualityModel;
|
||||
seasonNumber: number;
|
||||
episodeNumbers: number[];
|
||||
absoluteEpisodeNumbers: number[];
|
||||
specialAbsoluteEpisodeNumbers: number[];
|
||||
languages: Language[];
|
||||
fullSeason: boolean;
|
||||
isPartialSeason: boolean;
|
||||
isMultiSeason: boolean;
|
||||
isSeasonExtra: boolean;
|
||||
special: boolean;
|
||||
releaseHash: string;
|
||||
seasonPart: number;
|
||||
releaseGroup?: string;
|
||||
releaseTokens: string;
|
||||
airDate?: string;
|
||||
isDaily: boolean;
|
||||
isAbsoluteNumbering: boolean;
|
||||
isPossibleSpecialEpisode: boolean;
|
||||
isPossibleSceneSeasonSpecial: boolean;
|
||||
}
|
||||
|
||||
export interface ParseModel extends ModelBase {
|
||||
title: string;
|
||||
parsedEpisodeInfo: ParsedEpisodeInfo;
|
||||
series?: Series;
|
||||
episodes: Episode[];
|
||||
languages?: Language[];
|
||||
customFormats?: CustomFormat[];
|
||||
customFormatScore?: number;
|
||||
}
|
||||
|
||||
type ParseAppState = AppSectionItemState<ParseModel>;
|
||||
|
||||
export default ParseAppState;
|
@ -5,8 +5,8 @@ import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
||||
interface PageContentBodyProps {
|
||||
className: string;
|
||||
innerClassName: string;
|
||||
className?: string;
|
||||
innerClassName?: string;
|
||||
children: ReactNode;
|
||||
initialScrollTop?: number;
|
||||
onScroll?: (payload: OnScroll) => void;
|
||||
|
@ -167,6 +167,10 @@ const links = [
|
||||
{
|
||||
title: 'Log Files',
|
||||
to: '/system/logs/files'
|
||||
},
|
||||
{
|
||||
title: 'Parse Testing',
|
||||
to: '/system/parse'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
faBookReader as fasBookReader,
|
||||
faBroadcastTower as fasBroadcastTower,
|
||||
faBug as fasBug,
|
||||
faCalculator as fasCalculator,
|
||||
faCalendarAlt as fasCalendarAlt,
|
||||
faCaretDown as fasCaretDown,
|
||||
faCheck as fasCheck,
|
||||
@ -174,6 +175,7 @@ export const PAGE_PREVIOUS = fasBackward;
|
||||
export const PAGE_NEXT = fasForward;
|
||||
export const PAGE_LAST = fasFastForward;
|
||||
export const PARENT = fasLevelUpAlt;
|
||||
export const PARSE = fasCalculator;
|
||||
export const PAUSED = fasPause;
|
||||
export const PENDING = farClock;
|
||||
export const PROFILE = fasUser;
|
||||
|
45
frontend/src/Parse/Parse.css
Normal file
45
frontend/src/Parse/Parse.css
Normal file
@ -0,0 +1,45 @@
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inputIconContainer {
|
||||
width: 58px;
|
||||
height: 46px;
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
border-right: none;
|
||||
border-radius: 4px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-color: var(--inputIconContainerBackgroundColor);
|
||||
text-align: center;
|
||||
line-height: 46px;
|
||||
}
|
||||
|
||||
.input {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
height: 46px;
|
||||
border-radius: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.clearButton {
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
border-left: none;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: $largeFontSize;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
}
|
12
frontend/src/Parse/Parse.css.d.ts
vendored
Normal file
12
frontend/src/Parse/Parse.css.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'clearButton': string;
|
||||
'helpText': string;
|
||||
'input': string;
|
||||
'inputContainer': string;
|
||||
'inputIconContainer': string;
|
||||
'message': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
111
frontend/src/Parse/Parse.tsx
Normal file
111
frontend/src/Parse/Parse.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { clear, fetch } from 'Store/Actions/parseActions';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import ParseResult from './ParseResult';
|
||||
import parseStateSelector from './parseStateSelector';
|
||||
import styles from './Parse.css';
|
||||
|
||||
function Parse() {
|
||||
const { isFetching, error, item } = useSelector(parseStateSelector());
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
setTitle(value);
|
||||
|
||||
if (trimmedValue === '') {
|
||||
dispatch(clear());
|
||||
} else {
|
||||
dispatch(fetch({ title: trimmedValue }));
|
||||
}
|
||||
},
|
||||
[setTitle, dispatch]
|
||||
);
|
||||
|
||||
const onClearPress = useCallback(() => {
|
||||
setTitle('');
|
||||
dispatch(fetch({ title: '' }));
|
||||
}, [setTitle, dispatch]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
return () => {
|
||||
dispatch(clear());
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContent title="Parse">
|
||||
<PageContentBody>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.inputIconContainer}>
|
||||
<Icon name={icons.PARSE} size={20} />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={styles.input}
|
||||
name="title"
|
||||
value={title}
|
||||
placeholder="eg. Series.Title.S01E05.720p.HDTV-RlsGroup"
|
||||
autoFocus={true}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
<Button className={styles.clearButton} onPress={onClearPress}>
|
||||
<Icon name={icons.REMOVE} size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
Error parsing, please try again.
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isFetching && title && !error && !item.parsedEpisodeInfo ? (
|
||||
<div className={styles.message}>
|
||||
Unable to parse the provided title, please try again.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error && item.parsedEpisodeInfo ? (
|
||||
<ParseResult item={item} />
|
||||
) : null}
|
||||
|
||||
{title ? null : (
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
Enter a release title in the input above
|
||||
</div>
|
||||
<div>
|
||||
Sonarr will attempt to parse the title and show you details about
|
||||
it
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Parse;
|
20
frontend/src/Parse/ParseModal.tsx
Normal file
20
frontend/src/Parse/ParseModal.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ParseModalContent from './ParseModalContent';
|
||||
|
||||
interface ParseModalProps {
|
||||
isOpen: boolean;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function ParseModal(props: ParseModalProps) {
|
||||
const { isOpen, onModalClose } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<ParseModalContent onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ParseModal;
|
45
frontend/src/Parse/ParseModalContent.css
Normal file
45
frontend/src/Parse/ParseModalContent.css
Normal file
@ -0,0 +1,45 @@
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inputIconContainer {
|
||||
width: 58px;
|
||||
height: 46px;
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
border-right: none;
|
||||
border-radius: 4px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-color: var(--inputIconContainerBackgroundColor);
|
||||
text-align: center;
|
||||
line-height: 46px;
|
||||
}
|
||||
|
||||
.input {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
height: 46px;
|
||||
border-radius: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.clearButton {
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
border-left: none;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: $largeFontSize;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
}
|
12
frontend/src/Parse/ParseModalContent.css.d.ts
vendored
Normal file
12
frontend/src/Parse/ParseModalContent.css.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'clearButton': string;
|
||||
'helpText': string;
|
||||
'input': string;
|
||||
'inputContainer': string;
|
||||
'inputIconContainer': string;
|
||||
'message': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
124
frontend/src/Parse/ParseModalContent.tsx
Normal file
124
frontend/src/Parse/ParseModalContent.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { clear, fetch } from 'Store/Actions/parseActions';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import ParseResult from './ParseResult';
|
||||
import parseStateSelector from './parseStateSelector';
|
||||
import styles from './ParseModalContent.css';
|
||||
|
||||
interface ParseModalContentProps {
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function ParseModalContent(props: ParseModalContentProps) {
|
||||
const { onModalClose } = props;
|
||||
const { isFetching, error, item } = useSelector(parseStateSelector());
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
setTitle(value);
|
||||
|
||||
if (trimmedValue === '') {
|
||||
dispatch(clear());
|
||||
} else {
|
||||
dispatch(fetch({ title: trimmedValue }));
|
||||
}
|
||||
},
|
||||
[setTitle, dispatch]
|
||||
);
|
||||
|
||||
const onClearPress = useCallback(() => {
|
||||
setTitle('');
|
||||
dispatch(fetch({ title: '' }));
|
||||
}, [setTitle, dispatch]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
return () => {
|
||||
dispatch(clear());
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Test Parsing</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.inputIconContainer}>
|
||||
<Icon name={icons.PARSE} size={20} />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={styles.input}
|
||||
name="title"
|
||||
value={title}
|
||||
placeholder="eg. Series.Title.S01E05.720p.HDTV-RlsGroup"
|
||||
autoFocus={true}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
<Button className={styles.clearButton} onPress={onClearPress}>
|
||||
<Icon name={icons.REMOVE} size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
Error parsing, please try again.
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isFetching && title && !error && !item.parsedEpisodeInfo ? (
|
||||
<div className={styles.message}>
|
||||
Unable to parse the provided title, please try again.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error && item.parsedEpisodeInfo ? (
|
||||
<ParseResult item={item} />
|
||||
) : null}
|
||||
|
||||
{title ? null : (
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
Enter a release title in the input above
|
||||
</div>
|
||||
<div>
|
||||
Sonarr will attempt to parse the title and show you details about
|
||||
it
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ParseModalContent;
|
20
frontend/src/Parse/ParseResult.css
Normal file
20
frontend/src/Parse/ParseResult.css
Normal file
@ -0,0 +1,20 @@
|
||||
.item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-right: 20px;
|
||||
width: 250px;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
/* composes: description from '~Components/DescriptionList/DescriptionListItemTitle.css'; */
|
||||
}
|
||||
|
||||
@media (max-width: $breakpointSmall) {
|
||||
.item {
|
||||
display: block;
|
||||
}
|
||||
}
|
9
frontend/src/Parse/ParseResult.css.d.ts
vendored
Normal file
9
frontend/src/Parse/ParseResult.css.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'description': string;
|
||||
'item': string;
|
||||
'title': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
234
frontend/src/Parse/ParseResult.tsx
Normal file
234
frontend/src/Parse/ParseResult.tsx
Normal file
@ -0,0 +1,234 @@
|
||||
import React from 'react';
|
||||
import { ParseModel } from 'App/State/ParseAppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ParseResultItem from './ParseResultItem';
|
||||
|
||||
interface ParseResultProps {
|
||||
item: ParseModel;
|
||||
}
|
||||
|
||||
function ParseResult(props: ParseResultProps) {
|
||||
const { item } = props;
|
||||
const {
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
episodes,
|
||||
languages,
|
||||
parsedEpisodeInfo,
|
||||
series,
|
||||
} = item;
|
||||
|
||||
const {
|
||||
releaseTitle,
|
||||
seriesTitle,
|
||||
seriesTitleInfo,
|
||||
releaseGroup,
|
||||
releaseHash,
|
||||
seasonNumber,
|
||||
episodeNumbers,
|
||||
absoluteEpisodeNumbers,
|
||||
special,
|
||||
fullSeason,
|
||||
isMultiSeason,
|
||||
isPartialSeason,
|
||||
isDaily,
|
||||
airDate,
|
||||
quality,
|
||||
} = parsedEpisodeInfo;
|
||||
|
||||
const finalLanguages = languages ?? parsedEpisodeInfo.languages;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FieldSet legend={translate('Release')}>
|
||||
<ParseResultItem
|
||||
title={translate('Release Title')}
|
||||
data={releaseTitle}
|
||||
/>
|
||||
|
||||
<ParseResultItem title={translate('Series Title')} data={seriesTitle} />
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Year')}
|
||||
data={seriesTitleInfo.year > 0 ? seriesTitleInfo.year : '-'}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('All Titles')}
|
||||
data={
|
||||
seriesTitleInfo.allTitles?.length > 0
|
||||
? seriesTitleInfo.allTitles.join(', ')
|
||||
: '-'
|
||||
}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Release Group')}
|
||||
data={releaseGroup ?? '-'}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Release Hash')}
|
||||
data={releaseHash ? releaseHash : '-'}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
{/*
|
||||
|
||||
Year
|
||||
Secondary titles
|
||||
special episode
|
||||
|
||||
*/}
|
||||
|
||||
<FieldSet legend={translate('Episode Info')}>
|
||||
<ParseResultItem
|
||||
title={translate('Season Number')}
|
||||
data={
|
||||
seasonNumber === 0 && absoluteEpisodeNumbers.length
|
||||
? '-'
|
||||
: seasonNumber
|
||||
}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Episode Number(s)')}
|
||||
data={episodeNumbers.join(', ') || '-'}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Absolute Episode Number(s)')}
|
||||
data={
|
||||
absoluteEpisodeNumbers.length
|
||||
? absoluteEpisodeNumbers.join(', ')
|
||||
: '-'
|
||||
}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Special')}
|
||||
data={special ? 'True' : 'False'}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Full Season')}
|
||||
data={fullSeason ? 'True' : 'False'}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Multi-Season')}
|
||||
data={isMultiSeason ? 'True' : 'False'}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Partial Season')}
|
||||
data={isPartialSeason ? 'True' : 'False'}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Daily')}
|
||||
data={isDaily ? 'True' : 'False'}
|
||||
/>
|
||||
|
||||
<ParseResultItem title={translate('Air Date')} data={airDate ?? '-'} />
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Quality')}>
|
||||
<ParseResultItem
|
||||
title={translate('Quality')}
|
||||
data={quality.quality.name}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Version')}
|
||||
data={quality.revision.version > 1 ? quality.revision.version : '-'}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Real')}
|
||||
data={quality.revision.real ? 'True' : '-'}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Proper')}
|
||||
data={
|
||||
quality.revision.version > 1 && !quality.revision.isRepack
|
||||
? 'True'
|
||||
: '-'
|
||||
}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Repack')}
|
||||
data={quality.revision.isRepack ? 'True' : '-'}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Languages')}>
|
||||
<ParseResultItem
|
||||
title={translate('Languages')}
|
||||
data={finalLanguages.map((l) => l.name).join(', ')}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Details')}>
|
||||
<ParseResultItem
|
||||
title={translate('Matched to Series')}
|
||||
data={
|
||||
series ? (
|
||||
<SeriesTitleLink
|
||||
titleSlug={series.titleSlug}
|
||||
title={series.title}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Matched to Season')}
|
||||
data={episodes.length ? episodes[0].seasonNumber : '-'}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Matched to Episodes')}
|
||||
data={
|
||||
episodes.length ? (
|
||||
<div>
|
||||
{episodes.map((e) => {
|
||||
return (
|
||||
<div key={e.id}>
|
||||
{e.episodeNumber}
|
||||
{series?.seriesType === 'anime' && e.absoluteEpisodeNumber
|
||||
? ` (${e.absoluteEpisodeNumber})`
|
||||
: ''}{' '}
|
||||
{` - ${e.title}`}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
'-'
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Custom Formats')}
|
||||
data={<EpisodeFormats formats={customFormats} />}
|
||||
/>
|
||||
|
||||
<ParseResultItem
|
||||
title={translate('Custom Format Score')}
|
||||
data={customFormatScore}
|
||||
/>
|
||||
</FieldSet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ParseResult;
|
21
frontend/src/Parse/ParseResultItem.css
Normal file
21
frontend/src/Parse/ParseResultItem.css
Normal file
@ -0,0 +1,21 @@
|
||||
.item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-right: 20px;
|
||||
width: 250px;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpointSmall) {
|
||||
.item {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
8
frontend/src/Parse/ParseResultItem.css.d.ts
vendored
Normal file
8
frontend/src/Parse/ParseResultItem.css.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'item': string;
|
||||
'title': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
20
frontend/src/Parse/ParseResultItem.tsx
Normal file
20
frontend/src/Parse/ParseResultItem.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import styles from './ParseResultItem.css';
|
||||
|
||||
interface ParseResultItemProps {
|
||||
title: string;
|
||||
data: string | number | ReactNode;
|
||||
}
|
||||
|
||||
function ParseResultItem(props: ParseResultItemProps) {
|
||||
const { title, data } = props;
|
||||
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div>{data}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ParseResultItem;
|
30
frontend/src/Parse/ParseToolbarButton.tsx
Normal file
30
frontend/src/Parse/ParseToolbarButton.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { Fragment, useCallback, useState } from 'react';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ParseModal from 'Parse/ParseModal';
|
||||
|
||||
function ParseToolbarButton() {
|
||||
const [isParseModalOpen, setIsParseModalOpen] = useState(false);
|
||||
|
||||
const onOpenParseModalPress = useCallback(() => {
|
||||
setIsParseModalOpen(true);
|
||||
}, [setIsParseModalOpen]);
|
||||
|
||||
const onParseModalClose = useCallback(() => {
|
||||
setIsParseModalOpen(false);
|
||||
}, [setIsParseModalOpen]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageToolbarButton
|
||||
label="Test Parsing"
|
||||
iconName={icons.PARSE}
|
||||
onPress={onOpenParseModalPress}
|
||||
/>
|
||||
|
||||
<ParseModal isOpen={isParseModalOpen} onModalClose={onParseModalClose} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default ParseToolbarButton;
|
12
frontend/src/Parse/parseStateSelector.ts
Normal file
12
frontend/src/Parse/parseStateSelector.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import ParseAppState from 'App/State/ParseAppState';
|
||||
|
||||
export default function parseStateSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.parse,
|
||||
(parse: ParseAppState) => {
|
||||
return parse;
|
||||
}
|
||||
);
|
||||
}
|
@ -23,6 +23,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import ParseToolbarButton from 'Parse/ParseToolbarButton';
|
||||
import NoSeries from 'Series/NoSeries';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
@ -246,6 +247,9 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
||||
isSelectMode={isSelectMode}
|
||||
overflowComponent={SeriesIndexSelectAllMenuItem}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
<ParseToolbarButton />
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
|
@ -1,32 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||
|
||||
class CustomFormatSettingsConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageContent title="Custom Format Settings">
|
||||
<SettingsToolbarConnector
|
||||
showSave={false}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<CustomFormatsConnector />
|
||||
</DndProvider>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomFormatSettingsConnector;
|
||||
|
@ -0,0 +1,41 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import ParseToolbarButton from 'Parse/ParseToolbarButton';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||
|
||||
function CustomFormatSettingsPage() {
|
||||
return (
|
||||
<PageContent title="Custom Format Settings">
|
||||
<SettingsToolbarConnector
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
showSave={false}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<ParseToolbarButton />
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
{/* TODO: Upgrade react-dnd to get typings, we're 2 major versions behind */}
|
||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||
{/* @ts-ignore */}
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||
{/* @ts-ignore */}
|
||||
<CustomFormatsConnector />
|
||||
</DndProvider>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomFormatSettingsPage;
|
@ -134,6 +134,7 @@ const historyShape = {
|
||||
};
|
||||
|
||||
SettingsToolbarConnector.propTypes = {
|
||||
showSave: PropTypes.bool,
|
||||
hasPendingChanges: PropTypes.bool.isRequired,
|
||||
history: PropTypes.shape(historyShape).isRequired,
|
||||
onSavePress: PropTypes.func,
|
||||
|
@ -14,6 +14,7 @@ import * as importSeries from './importSeriesActions';
|
||||
import * as interactiveImportActions from './interactiveImportActions';
|
||||
import * as oAuth from './oAuthActions';
|
||||
import * as organizePreview from './organizePreviewActions';
|
||||
import * as parse from './parseActions';
|
||||
import * as paths from './pathActions';
|
||||
import * as providerOptions from './providerOptionActions';
|
||||
import * as queue from './queueActions';
|
||||
@ -44,6 +45,7 @@ export default [
|
||||
interactiveImportActions,
|
||||
oAuth,
|
||||
organizePreview,
|
||||
parse,
|
||||
paths,
|
||||
providerOptions,
|
||||
queue,
|
||||
|
109
frontend/src/Store/Actions/parseActions.ts
Normal file
109
frontend/src/Store/Actions/parseActions.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { Dispatch } from 'redux';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import { set, update } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createClearReducer from './Creators/Reducers/createClearReducer';
|
||||
|
||||
interface FetchPayload {
|
||||
title: string;
|
||||
}
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'parse';
|
||||
let parseTimeout: number | null = null;
|
||||
let abortCurrentRequest: (() => void) | null = null;
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
item: {},
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH = 'parse/fetch';
|
||||
export const CLEAR = 'parse/clear';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetch = createThunk(FETCH);
|
||||
export const clear = createAction(CLEAR);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH]: function (
|
||||
_getState: () => AppState,
|
||||
payload: FetchPayload,
|
||||
dispatch: Dispatch
|
||||
) {
|
||||
if (parseTimeout) {
|
||||
clearTimeout(parseTimeout);
|
||||
}
|
||||
|
||||
parseTimeout = window.setTimeout(async () => {
|
||||
if (abortCurrentRequest) {
|
||||
abortCurrentRequest();
|
||||
}
|
||||
|
||||
const { request, abortRequest } = createAjaxRequest({
|
||||
url: '/parse',
|
||||
data: {
|
||||
title: payload.title,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await request;
|
||||
|
||||
dispatch(
|
||||
batchActions([
|
||||
update({ section, data }),
|
||||
|
||||
set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null,
|
||||
}),
|
||||
])
|
||||
);
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
set({
|
||||
section,
|
||||
isAdding: false,
|
||||
isAdded: false,
|
||||
addError: error,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
abortCurrentRequest = abortRequest;
|
||||
}, 300);
|
||||
},
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions(
|
||||
{
|
||||
[CLEAR]: createClearReducer(section, defaultState),
|
||||
},
|
||||
defaultState,
|
||||
section
|
||||
);
|
@ -4,23 +4,25 @@ import AppState from 'App/State/AppState';
|
||||
type GetState = () => AppState;
|
||||
type Thunk = (
|
||||
getState: GetState,
|
||||
identity: unknown,
|
||||
identityFn: never,
|
||||
dispatch: Dispatch
|
||||
) => unknown;
|
||||
|
||||
const thunks: Record<string, Thunk> = {};
|
||||
|
||||
function identity(payload: unknown) {
|
||||
return payload;
|
||||
function identity<T, TResult>(payload: T): TResult {
|
||||
return payload as unknown as TResult;
|
||||
}
|
||||
|
||||
export function createThunk(type: string, identityFunction = identity) {
|
||||
return function (payload: unknown = {}) {
|
||||
return function <T>(payload?: T) {
|
||||
return function (dispatch: Dispatch, getState: GetState) {
|
||||
const thunk = thunks[type];
|
||||
|
||||
if (thunk) {
|
||||
return thunk(getState, identityFunction(payload), dispatch);
|
||||
const finalPayload = payload ?? {};
|
||||
|
||||
return thunk(getState, identityFunction(finalPayload), dispatch);
|
||||
}
|
||||
|
||||
throw Error(`Thunk handler has not been registered for ${type}`);
|
||||
|
@ -101,6 +101,7 @@
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-text-truncate": "0.14.1",
|
||||
"@types/react-window": "1.8.5",
|
||||
"@types/redux-actions": "2.6.2",
|
||||
"@types/webpack-livereload-plugin": "^2.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
|
@ -1,7 +1,9 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Download.Aggregation;
|
||||
using NzbDrone.Core.Parser;
|
||||
using Sonarr.Api.V3.CustomFormats;
|
||||
using Sonarr.Api.V3.Episodes;
|
||||
using Sonarr.Api.V3.Series;
|
||||
using Sonarr.Http;
|
||||
@ -13,12 +15,15 @@ public class ParseController : Controller
|
||||
{
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly IRemoteEpisodeAggregationService _aggregationService;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
|
||||
public ParseController(IParsingService parsingService,
|
||||
IRemoteEpisodeAggregationService aggregationService)
|
||||
IRemoteEpisodeAggregationService aggregationService,
|
||||
ICustomFormatCalculationService formatCalculator)
|
||||
{
|
||||
_parsingService = parsingService;
|
||||
_aggregationService = aggregationService;
|
||||
_formatCalculator = formatCalculator;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -42,16 +47,22 @@ public ParseResource Parse(string title, string path)
|
||||
|
||||
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0);
|
||||
|
||||
_aggregationService.Augment(remoteEpisode);
|
||||
|
||||
if (remoteEpisode != null)
|
||||
{
|
||||
_aggregationService.Augment(remoteEpisode);
|
||||
|
||||
remoteEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(remoteEpisode, 0);
|
||||
remoteEpisode.CustomFormatScore = remoteEpisode?.Series?.QualityProfile?.Value.CalculateCustomFormatScore(remoteEpisode.CustomFormats) ?? 0;
|
||||
|
||||
return new ParseResource
|
||||
{
|
||||
Title = title,
|
||||
ParsedEpisodeInfo = remoteEpisode.ParsedEpisodeInfo,
|
||||
Series = remoteEpisode.Series.ToResource(),
|
||||
Episodes = remoteEpisode.Episodes.ToResource()
|
||||
Episodes = remoteEpisode.Episodes.ToResource(),
|
||||
Languages = remoteEpisode.Languages,
|
||||
CustomFormats = remoteEpisode.CustomFormats?.ToResource(false),
|
||||
CustomFormatScore = remoteEpisode.CustomFormatScore
|
||||
};
|
||||
}
|
||||
else
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using Sonarr.Api.V3.CustomFormats;
|
||||
using Sonarr.Api.V3.Episodes;
|
||||
using Sonarr.Api.V3.Series;
|
||||
using Sonarr.Http.REST;
|
||||
@ -12,5 +14,8 @@ public class ParseResource : RestResource
|
||||
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
|
||||
public SeriesResource Series { get; set; }
|
||||
public List<EpisodeResource> Episodes { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||
public int CustomFormatScore { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1478,6 +1478,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/redux-actions@2.6.2":
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.2.tgz#5956d9e7b9a644358e2c0610f47b1fa3060edc21"
|
||||
integrity sha512-TvcINy8rWFANcpc3EiEQX9Yv3owM3d3KIrqr2ryUIOhYIYzXA/bhDZeGSSSuai62iVR2qMZUgz9tQ5kr0Kl+Tg==
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||
|
Loading…
Reference in New Issue
Block a user