New: Added UI for parsing release names

(cherry picked from commit 85e285598106346099ceae676599c5cb4b789c92)
pull/8842/head
Mark McDowall 1 year ago committed by Bogdan
parent 5f70581a59
commit faaef80a80

@ -13,7 +13,7 @@ import Switch from 'Components/Router/Switch';
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector'; import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector'; import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
import MovieIndex from 'Movie/Index/MovieIndex'; import MovieIndex from 'Movie/Index/MovieIndex';
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector'; import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector'; import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
@ -148,7 +148,7 @@ function AppRoutes(props) {
<Route <Route
path="/settings/customformats" path="/settings/customformats"
component={CustomFormatSettingsConnector} component={CustomFormatSettingsPage}
/> />
<Route <Route

@ -1,6 +1,7 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState'; import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import MovieFilesAppState from './MovieFilesAppState'; import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState'; import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
import ParseAppState from './ParseAppState';
import QueueAppState from './QueueAppState'; import QueueAppState from './QueueAppState';
import SettingsAppState from './SettingsAppState'; import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState'; import TagsAppState from './TagsAppState';
@ -41,6 +42,7 @@ interface AppState {
movieFiles: MovieFilesAppState; movieFiles: MovieFilesAppState;
interactiveImport: InteractiveImportAppState; interactiveImport: InteractiveImportAppState;
movieIndex: MovieIndexAppState; movieIndex: MovieIndexAppState;
parse: ParseAppState;
settings: SettingsAppState; settings: SettingsAppState;
movies: MoviesAppState; movies: MoviesAppState;
tags: TagsAppState; tags: TagsAppState;

@ -0,0 +1,34 @@
import ModelBase from 'App/ModelBase';
import { AppSectionItemState } from 'App/State/AppSectionState';
import Language from 'Language/Language';
import Movie from 'Movie/Movie';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
export interface ParsedMovieInfo {
releaseTitle: string;
originalTitle: string;
movieTitle: string;
movieTitles: string[];
year: number;
quality: QualityModel;
languages: Language[];
releaseHash: string;
releaseGroup?: string;
edition?: string;
tmdbId?: number;
imdbId?: string;
}
export interface ParseModel extends ModelBase {
title: string;
parsedMovieInfo: ParsedMovieInfo;
movie?: Movie;
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'; import styles from './PageContentBody.css';
interface PageContentBodyProps { interface PageContentBodyProps {
className: string; className?: string;
innerClassName: string; innerClassName?: string;
children: ReactNode; children: ReactNode;
initialScrollTop?: number; initialScrollTop?: number;
onScroll?: (payload: OnScroll) => void; onScroll?: (payload: OnScroll) => void;

@ -34,6 +34,7 @@ import {
faBug as fasBug, faBug as fasBug,
faBuilding as fasBuilding, faBuilding as fasBuilding,
faBullhorn as fasBullhorn, faBullhorn as fasBullhorn,
faCalculator as fasCalculator,
faCalendarAlt as fasCalendarAlt, faCalendarAlt as fasCalendarAlt,
faCaretDown as fasCaretDown, faCaretDown as fasCaretDown,
faCheck as fasCheck, faCheck as fasCheck,
@ -189,6 +190,7 @@ export const PAGE_PREVIOUS = fasBackward;
export const PAGE_NEXT = fasForward; export const PAGE_NEXT = fasForward;
export const PAGE_LAST = fasFastForward; export const PAGE_LAST = fasFastForward;
export const PARENT = fasLevelUpAlt; export const PARENT = fasLevelUpAlt;
export const PARSE = fasCalculator;
export const PAUSED = fasPause; export const PAUSED = fasPause;
export const PENDING = farClock; export const PENDING = farClock;
export const PLAY = fasPlay; export const PLAY = fasPlay;

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

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

@ -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(clear());
}, [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. Movie.Title.2020.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.parsedMovieInfo ? (
<div className={styles.message}>
Unable to parse the provided title, please try again.
</div>
) : null}
{!isFetching && !error && item.parsedMovieInfo ? (
<ParseResult item={item} />
) : null}
{title ? null : (
<div className={styles.message}>
<div className={styles.helpText}>
Enter a release title in the input above
</div>
<div>
Radarr will attempt to parse the title and show you details about
it
</div>
</div>
)}
</PageContentBody>
</PageContent>
);
}
export default Parse;

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

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

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

@ -0,0 +1,125 @@
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 translate from 'Utilities/String/translate';
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(clear());
}, [setTitle, dispatch]);
useEffect(
() => {
return () => {
dispatch(clear());
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('TestParsing')}</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. Movie.Title.2020.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.parsedMovieInfo ? (
<div className={styles.message}>
Unable to parse the provided title, please try again.
</div>
) : null}
{!isFetching && !error && item.parsedMovieInfo ? (
<ParseResult item={item} />
) : null}
{title ? null : (
<div className={styles.message}>
<div className={styles.helpText}>
Enter a release title in the input above
</div>
<div>
Radarr will attempt to parse the title and show you details about
it
</div>
</div>
)}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default ParseModalContent;

@ -0,0 +1,8 @@
.container {
display: flex;
flex-wrap: wrap;
}
.column {
flex: 0 0 50%;
}

@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'column': string;
'container': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -0,0 +1,161 @@
import React from 'react';
import { ParseModel } from 'App/State/ParseAppState';
import FieldSet from 'Components/FieldSet';
import MovieFormats from 'Movie/MovieFormats';
import MovieTitleLink from 'Movie/MovieTitleLink';
import translate from 'Utilities/String/translate';
import ParseResultItem from './ParseResultItem';
import styles from './ParseResult.css';
interface ParseResultProps {
item: ParseModel;
}
function ParseResult(props: ParseResultProps) {
const { item } = props;
const {
customFormats,
customFormatScore,
languages,
parsedMovieInfo,
movie,
} = item;
const {
releaseTitle,
movieTitle,
movieTitles,
year,
edition,
releaseGroup,
releaseHash,
quality,
tmdbId,
imdbId,
} = parsedMovieInfo;
const finalLanguages = languages ?? parsedMovieInfo.languages;
return (
<div>
<FieldSet legend={translate('Release')}>
<ParseResultItem
title={translate('ReleaseTitle')}
data={releaseTitle}
/>
<ParseResultItem title={translate('MovieTitle')} data={movieTitle} />
<ParseResultItem
title={translate('Year')}
data={year > 0 ? year : '-'}
/>
<ParseResultItem
title={translate('Edition')}
data={edition ? edition : '-'}
/>
<ParseResultItem
title={translate('AllTitles')}
data={movieTitles?.length > 0 ? movieTitles.join(', ') : '-'}
/>
<ParseResultItem
title={translate('ReleaseGroup')}
data={releaseGroup ?? '-'}
/>
<ParseResultItem
title={translate('ReleaseHash')}
data={releaseHash ? releaseHash : '-'}
/>
{tmdbId ? (
<ParseResultItem title={translate('TmdbId')} data={tmdbId} />
) : null}
{imdbId ? (
<ParseResultItem title={translate('ImdbId')} data={imdbId} />
) : null}
</FieldSet>
<FieldSet legend={translate('Quality')}>
<div className={styles.container}>
<div className={styles.column}>
<ParseResultItem
title={translate('Quality')}
data={quality.quality.name}
/>
<ParseResultItem
title={translate('Proper')}
data={
quality.revision.version > 1 && !quality.revision.isRepack
? 'True'
: '-'
}
/>
<ParseResultItem
title={translate('Repack')}
data={quality.revision.isRepack ? 'True' : '-'}
/>
</div>
<div className={styles.column}>
<ParseResultItem
title={translate('Version')}
data={
quality.revision.version > 1 ? quality.revision.version : '-'
}
/>
<ParseResultItem
title={translate('Real')}
data={quality.revision.real ? 'True' : '-'}
/>
</div>
</div>
</FieldSet>
<FieldSet legend={translate('Languages')}>
<ParseResultItem
title={translate('Languages')}
data={finalLanguages.map((l) => l.name).join(', ')}
/>
</FieldSet>
<FieldSet legend={translate('Details')}>
<ParseResultItem
title={translate('MatchedToMovie')}
data={
movie ? (
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
) : (
'-'
)
}
/>
{movie && movie.originalLanguage ? (
<ParseResultItem
title={translate('OriginalLanguage')}
data={movie.originalLanguage.name}
/>
) : null}
<ParseResultItem
title={translate('CustomFormats')}
data={<MovieFormats formats={customFormats} />}
/>
<ParseResultItem
title={translate('CustomFormatScore')}
data={customFormatScore}
/>
</FieldSet>
</div>
);
}
export default ParseResult;

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

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

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

@ -0,0 +1,31 @@
import React, { Fragment, useCallback, useState } from 'react';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import ParseModal from 'Parse/ParseModal';
import translate from 'Utilities/String/translate';
function ParseToolbarButton() {
const [isParseModalOpen, setIsParseModalOpen] = useState(false);
const onOpenParseModalPress = useCallback(() => {
setIsParseModalOpen(true);
}, [setIsParseModalOpen]);
const onParseModalClose = useCallback(() => {
setIsParseModalOpen(false);
}, [setIsParseModalOpen]);
return (
<Fragment>
<PageToolbarButton
label={translate('TestParsing')}
iconName={icons.PARSE}
onPress={onOpenParseModalPress}
/>
<ParseModal isOpen={isParseModalOpen} onModalClose={onParseModalClose} />
</Fragment>
);
}
export default ParseToolbarButton;

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

@ -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 = { SettingsToolbarConnector.propTypes = {
showSave: PropTypes.bool,
hasPendingChanges: PropTypes.bool.isRequired, hasPendingChanges: PropTypes.bool.isRequired,
history: PropTypes.shape(historyShape).isRequired, history: PropTypes.shape(historyShape).isRequired,
onSavePress: PropTypes.func, onSavePress: PropTypes.func,

@ -19,6 +19,7 @@ import * as movieHistory from './movieHistoryActions';
import * as movieIndex from './movieIndexActions'; import * as movieIndex from './movieIndexActions';
import * as oAuth from './oAuthActions'; import * as oAuth from './oAuthActions';
import * as organizePreview from './organizePreviewActions'; import * as organizePreview from './organizePreviewActions';
import * as parse from './parseActions';
import * as paths from './pathActions'; import * as paths from './pathActions';
import * as providerOptions from './providerOptionActions'; import * as providerOptions from './providerOptionActions';
import * as queue from './queueActions'; import * as queue from './queueActions';
@ -44,6 +45,7 @@ export default [
interactiveImportActions, interactiveImportActions,
oAuth, oAuth,
organizePreview, organizePreview,
parse,
paths, paths,
providerOptions, providerOptions,
queue, queue,

@ -0,0 +1,111 @@
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 () => {
dispatch(set({ section, isFetching: true }));
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
);

@ -0,0 +1,39 @@
import { Dispatch } from 'redux';
import AppState from 'App/State/AppState';
type GetState = () => AppState;
type Thunk = (
getState: GetState,
identityFn: never,
dispatch: Dispatch
) => unknown;
const thunks: Record<string, Thunk> = {};
function identity<T, TResult>(payload: T): TResult {
return payload as unknown as TResult;
}
export function createThunk(type: string, identityFunction = identity) {
return function <T>(payload?: T) {
return function (dispatch: Dispatch, getState: GetState) {
const thunk = thunks[type];
if (thunk) {
const finalPayload = payload ?? {};
return thunk(getState, identityFunction(finalPayload), dispatch);
}
throw Error(`Thunk handler has not been registered for ${type}`);
};
};
}
export function handleThunks(handlers: Record<string, Thunk>) {
const types = Object.keys(handlers);
types.forEach((type) => {
thunks[type] = handlers[type];
});
}

@ -102,6 +102,7 @@
"@types/react-router-dom": "5.3.3", "@types/react-router-dom": "5.3.3",
"@types/react-text-truncate": "0.14.1", "@types/react-text-truncate": "0.14.1",
"@types/react-window": "1.8.5", "@types/react-window": "1.8.5",
"@types/redux-actions": "2.6.2",
"@types/webpack-livereload-plugin": "2.3.3", "@types/webpack-livereload-plugin": "2.3.3",
"@typescript-eslint/eslint-plugin": "5.59.5", "@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5", "@typescript-eslint/parser": "5.59.5",

@ -37,6 +37,7 @@
"AllMoviesHiddenDueToFilter": "All movies are hidden due to applied filter.", "AllMoviesHiddenDueToFilter": "All movies are hidden due to applied filter.",
"AllMoviesInPathHaveBeenImported": "All movies in {0} have been imported", "AllMoviesInPathHaveBeenImported": "All movies in {0} have been imported",
"AllResultsHiddenFilter": "All results are hidden by the applied filter", "AllResultsHiddenFilter": "All results are hidden by the applied filter",
"AllTitles": "All Titles",
"AllowHardcodedSubs": "Allow Hardcoded Subs", "AllowHardcodedSubs": "Allow Hardcoded Subs",
"AllowHardcodedSubsHelpText": "Detected hardcoded subs will be automatically downloaded", "AllowHardcodedSubsHelpText": "Detected hardcoded subs will be automatically downloaded",
"AlreadyInYourLibrary": "Already in your library", "AlreadyInYourLibrary": "Already in your library",
@ -545,6 +546,7 @@
"MarkAsFailed": "Mark as Failed", "MarkAsFailed": "Mark as Failed",
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?", "MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?",
"MassMovieSearch": "Mass Movie Search", "MassMovieSearch": "Mass Movie Search",
"MatchedToMovie": "Matched to Movie",
"Max": "Max", "Max": "Max",
"MaximumLimits": "Maximum Limits", "MaximumLimits": "Maximum Limits",
"MaximumSize": "Maximum Size", "MaximumSize": "Maximum Size",
@ -832,6 +834,7 @@
"ReleaseBranchCheckOfficialBranchMessage": "Branch {0} is not a valid Radarr release branch, you will not receive updates", "ReleaseBranchCheckOfficialBranchMessage": "Branch {0} is not a valid Radarr release branch, you will not receive updates",
"ReleaseDates": "Release Dates", "ReleaseDates": "Release Dates",
"ReleaseGroup": "Release Group", "ReleaseGroup": "Release Group",
"ReleaseHash": "Release Hash",
"ReleaseRejected": "Release Rejected", "ReleaseRejected": "Release Rejected",
"ReleaseStatus": "Release Status", "ReleaseStatus": "Release Status",
"ReleaseTitle": "Release Title", "ReleaseTitle": "Release Title",
@ -1073,6 +1076,7 @@
"TestAllClients": "Test All Clients", "TestAllClients": "Test All Clients",
"TestAllIndexers": "Test All Indexers", "TestAllIndexers": "Test All Indexers",
"TestAllLists": "Test All Lists", "TestAllLists": "Test All Lists",
"TestParsing": "Test Parsing",
"TheLogLevelDefault": "The log level defaults to 'Info' and can be changed in", "TheLogLevelDefault": "The log level defaults to 'Info' and can be changed in",
"ThereWasAnErrorLoadingThisItem": "There was an error loading this item", "ThereWasAnErrorLoadingThisItem": "There was an error loading this item",
"ThereWasAnErrorLoadingThisPage": "There was an error loading this page", "ThereWasAnErrorLoadingThisPage": "There was an error loading this page",

@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Download.Aggregation; using NzbDrone.Core.Download.Aggregation;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using Radarr.Api.V3.CustomFormats;
using Radarr.Api.V3.Movies; using Radarr.Api.V3.Movies;
using Radarr.Http; using Radarr.Http;
@ -14,14 +16,17 @@ namespace Radarr.Api.V3.Parse
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly IRemoteMovieAggregationService _aggregationService; private readonly IRemoteMovieAggregationService _aggregationService;
private readonly ICustomFormatCalculationService _formatCalculator;
public ParseController(IParsingService parsingService, public ParseController(IParsingService parsingService,
IConfigService configService, IConfigService configService,
IRemoteMovieAggregationService aggregationService) IRemoteMovieAggregationService aggregationService,
ICustomFormatCalculationService formatCalculator)
{ {
_parsingService = parsingService; _parsingService = parsingService;
_configService = configService; _configService = configService;
_aggregationService = aggregationService; _aggregationService = aggregationService;
_formatCalculator = formatCalculator;
} }
[HttpGet] [HttpGet]
@ -44,15 +49,21 @@ namespace Radarr.Api.V3.Parse
var remoteMovie = _parsingService.Map(parsedMovieInfo, "", 0); var remoteMovie = _parsingService.Map(parsedMovieInfo, "", 0);
_aggregationService.Augment(remoteMovie);
if (remoteMovie != null) if (remoteMovie != null)
{ {
_aggregationService.Augment(remoteMovie);
remoteMovie.CustomFormats = _formatCalculator.ParseCustomFormat(remoteMovie, 0);
remoteMovie.CustomFormatScore = remoteMovie.Movie?.Profile?.CalculateCustomFormatScore(remoteMovie.CustomFormats) ?? 0;
return new ParseResource return new ParseResource
{ {
Title = title, Title = title,
ParsedMovieInfo = remoteMovie.ParsedMovieInfo, ParsedMovieInfo = remoteMovie.ParsedMovieInfo,
Movie = remoteMovie.Movie.ToResource(_configService.AvailabilityDelay) Movie = remoteMovie.Movie.ToResource(_configService.AvailabilityDelay),
Languages = remoteMovie.Languages,
CustomFormats = remoteMovie.CustomFormats?.ToResource(false),
CustomFormatScore = remoteMovie.CustomFormatScore
}; };
} }
else else

@ -1,4 +1,7 @@
using System.Collections.Generic;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using Radarr.Api.V3.CustomFormats;
using Radarr.Api.V3.Movies; using Radarr.Api.V3.Movies;
using Radarr.Http.REST; using Radarr.Http.REST;
@ -9,5 +12,8 @@ namespace Radarr.Api.V3.Parse
public string Title { get; set; } public string Title { get; set; }
public ParsedMovieInfo ParsedMovieInfo { get; set; } public ParsedMovieInfo ParsedMovieInfo { get; set; }
public MovieResource Movie { get; set; } public MovieResource Movie { get; set; }
public List<Language> Languages { get; set; }
public List<CustomFormatResource> CustomFormats { get; set; }
public int CustomFormatScore { get; set; }
} }
} }

@ -1471,6 +1471,11 @@
dependencies: dependencies:
"@types/node" "*" "@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@*": "@types/scheduler@*":
version "0.16.3" version "0.16.3"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"

Loading…
Cancel
Save