diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index c77633230..e6f8f4c20 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -13,7 +13,7 @@ import Switch from 'Components/Router/Switch'; import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector'; import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector'; 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 GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector'; @@ -148,7 +148,7 @@ function AppRoutes(props) { ; + +export default ParseAppState; diff --git a/frontend/src/Components/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx index a36010749..ce9b0e7e4 100644 --- a/frontend/src/Components/Page/PageContentBody.tsx +++ b/frontend/src/Components/Page/PageContentBody.tsx @@ -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; diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 55c607023..7263286ab 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -34,6 +34,7 @@ import { faBug as fasBug, faBuilding as fasBuilding, faBullhorn as fasBullhorn, + faCalculator as fasCalculator, faCalendarAlt as fasCalendarAlt, faCaretDown as fasCaretDown, faCheck as fasCheck, @@ -189,6 +190,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 PLAY = fasPlay; diff --git a/frontend/src/Parse/Parse.css b/frontend/src/Parse/Parse.css new file mode 100644 index 000000000..43536452c --- /dev/null +++ b/frontend/src/Parse/Parse.css @@ -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; +} diff --git a/frontend/src/Parse/Parse.css.d.ts b/frontend/src/Parse/Parse.css.d.ts new file mode 100644 index 000000000..4a4def577 --- /dev/null +++ b/frontend/src/Parse/Parse.css.d.ts @@ -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; diff --git a/frontend/src/Parse/Parse.tsx b/frontend/src/Parse/Parse.tsx new file mode 100644 index 000000000..f86f445c4 --- /dev/null +++ b/frontend/src/Parse/Parse.tsx @@ -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 ( + + +
+
+ +
+ + + + +
+ + {isFetching ? : null} + + {!isFetching && !!error ? ( +
+
+ Error parsing, please try again. +
+
{getErrorMessage(error)}
+
+ ) : null} + + {!isFetching && title && !error && !item.parsedMovieInfo ? ( +
+ Unable to parse the provided title, please try again. +
+ ) : null} + + {!isFetching && !error && item.parsedMovieInfo ? ( + + ) : null} + + {title ? null : ( +
+
+ Enter a release title in the input above +
+
+ Radarr will attempt to parse the title and show you details about + it +
+
+ )} +
+
+ ); +} + +export default Parse; diff --git a/frontend/src/Parse/ParseModal.tsx b/frontend/src/Parse/ParseModal.tsx new file mode 100644 index 000000000..0ee455bf0 --- /dev/null +++ b/frontend/src/Parse/ParseModal.tsx @@ -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 ( + + + + ); +} + +export default ParseModal; diff --git a/frontend/src/Parse/ParseModalContent.css b/frontend/src/Parse/ParseModalContent.css new file mode 100644 index 000000000..43536452c --- /dev/null +++ b/frontend/src/Parse/ParseModalContent.css @@ -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; +} diff --git a/frontend/src/Parse/ParseModalContent.css.d.ts b/frontend/src/Parse/ParseModalContent.css.d.ts new file mode 100644 index 000000000..4a4def577 --- /dev/null +++ b/frontend/src/Parse/ParseModalContent.css.d.ts @@ -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; diff --git a/frontend/src/Parse/ParseModalContent.tsx b/frontend/src/Parse/ParseModalContent.tsx new file mode 100644 index 000000000..5138f7e2b --- /dev/null +++ b/frontend/src/Parse/ParseModalContent.tsx @@ -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 ( + + {translate('TestParsing')} + + +
+
+ +
+ + + + +
+ + {isFetching ? : null} + + {!isFetching && !!error ? ( +
+
+ Error parsing, please try again. +
+
{getErrorMessage(error)}
+
+ ) : null} + + {!isFetching && title && !error && !item.parsedMovieInfo ? ( +
+ Unable to parse the provided title, please try again. +
+ ) : null} + + {!isFetching && !error && item.parsedMovieInfo ? ( + + ) : null} + + {title ? null : ( +
+
+ Enter a release title in the input above +
+
+ Radarr will attempt to parse the title and show you details about + it +
+
+ )} +
+ + + + +
+ ); +} + +export default ParseModalContent; diff --git a/frontend/src/Parse/ParseResult.css b/frontend/src/Parse/ParseResult.css new file mode 100644 index 000000000..c49c4e3fa --- /dev/null +++ b/frontend/src/Parse/ParseResult.css @@ -0,0 +1,8 @@ +.container { + display: flex; + flex-wrap: wrap; +} + +.column { + flex: 0 0 50%; +} diff --git a/frontend/src/Parse/ParseResult.css.d.ts b/frontend/src/Parse/ParseResult.css.d.ts new file mode 100644 index 000000000..653368e06 --- /dev/null +++ b/frontend/src/Parse/ParseResult.css.d.ts @@ -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; diff --git a/frontend/src/Parse/ParseResult.tsx b/frontend/src/Parse/ParseResult.tsx new file mode 100644 index 000000000..909e3f062 --- /dev/null +++ b/frontend/src/Parse/ParseResult.tsx @@ -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 ( +
+
+ + + + + 0 ? year : '-'} + /> + + + + 0 ? movieTitles.join(', ') : '-'} + /> + + + + + + {tmdbId ? ( + + ) : null} + + {imdbId ? ( + + ) : null} +
+ +
+
+
+ + 1 && !quality.revision.isRepack + ? 'True' + : '-' + } + /> + + +
+ +
+ 1 ? quality.revision.version : '-' + } + /> + + +
+
+
+ +
+ l.name).join(', ')} + /> +
+ +
+ + ) : ( + '-' + ) + } + /> + + {movie && movie.originalLanguage ? ( + + ) : null} + + } + /> + + +
+
+ ); +} + +export default ParseResult; diff --git a/frontend/src/Parse/ParseResultItem.css b/frontend/src/Parse/ParseResultItem.css new file mode 100644 index 000000000..275fe7e1f --- /dev/null +++ b/frontend/src/Parse/ParseResultItem.css @@ -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; + } +} diff --git a/frontend/src/Parse/ParseResultItem.css.d.ts b/frontend/src/Parse/ParseResultItem.css.d.ts new file mode 100644 index 000000000..bcf268e50 --- /dev/null +++ b/frontend/src/Parse/ParseResultItem.css.d.ts @@ -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; diff --git a/frontend/src/Parse/ParseResultItem.tsx b/frontend/src/Parse/ParseResultItem.tsx new file mode 100644 index 000000000..661af448d --- /dev/null +++ b/frontend/src/Parse/ParseResultItem.tsx @@ -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 ( +
+
{title}
+
{data}
+
+ ); +} + +export default ParseResultItem; diff --git a/frontend/src/Parse/ParseToolbarButton.tsx b/frontend/src/Parse/ParseToolbarButton.tsx new file mode 100644 index 000000000..43b8b959f --- /dev/null +++ b/frontend/src/Parse/ParseToolbarButton.tsx @@ -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 ( + + + + + + ); +} + +export default ParseToolbarButton; diff --git a/frontend/src/Parse/parseStateSelector.ts b/frontend/src/Parse/parseStateSelector.ts new file mode 100644 index 000000000..7abcfeca1 --- /dev/null +++ b/frontend/src/Parse/parseStateSelector.ts @@ -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; + } + ); +} diff --git a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx new file mode 100644 index 000000000..cb86066b1 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx @@ -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 ( + + + + + + + } + /> + + + {/* TODO: Upgrade react-dnd to get typings, we're 2 major versions behind */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + + + + + ); +} + +export default CustomFormatSettingsPage; diff --git a/frontend/src/Settings/SettingsToolbarConnector.js b/frontend/src/Settings/SettingsToolbarConnector.js index 1e6f7a589..65d937ab8 100644 --- a/frontend/src/Settings/SettingsToolbarConnector.js +++ b/frontend/src/Settings/SettingsToolbarConnector.js @@ -134,6 +134,7 @@ const historyShape = { }; SettingsToolbarConnector.propTypes = { + showSave: PropTypes.bool, hasPendingChanges: PropTypes.bool.isRequired, history: PropTypes.shape(historyShape).isRequired, onSavePress: PropTypes.func, diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index 09c373494..394fcd964 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -19,6 +19,7 @@ import * as movieHistory from './movieHistoryActions'; import * as movieIndex from './movieIndexActions'; 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, diff --git a/frontend/src/Store/Actions/parseActions.ts b/frontend/src/Store/Actions/parseActions.ts new file mode 100644 index 000000000..d4b6e9bcb --- /dev/null +++ b/frontend/src/Store/Actions/parseActions.ts @@ -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 +); diff --git a/frontend/src/Store/thunks.ts b/frontend/src/Store/thunks.ts new file mode 100644 index 000000000..fd277211e --- /dev/null +++ b/frontend/src/Store/thunks.ts @@ -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 = {}; + +function identity(payload: T): TResult { + return payload as unknown as TResult; +} + +export function createThunk(type: string, identityFunction = identity) { + return function (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) { + const types = Object.keys(handlers); + + types.forEach((type) => { + thunks[type] = handlers[type]; + }); +} diff --git a/package.json b/package.json index 59ed05857..c7f3b623e 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,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", diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 8e7990683..b1fb3be9e 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -37,6 +37,7 @@ "AllMoviesHiddenDueToFilter": "All movies are hidden due to applied filter.", "AllMoviesInPathHaveBeenImported": "All movies in {0} have been imported", "AllResultsHiddenFilter": "All results are hidden by the applied filter", + "AllTitles": "All Titles", "AllowHardcodedSubs": "Allow Hardcoded Subs", "AllowHardcodedSubsHelpText": "Detected hardcoded subs will be automatically downloaded", "AlreadyInYourLibrary": "Already in your library", @@ -545,6 +546,7 @@ "MarkAsFailed": "Mark as Failed", "MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?", "MassMovieSearch": "Mass Movie Search", + "MatchedToMovie": "Matched to Movie", "Max": "Max", "MaximumLimits": "Maximum Limits", "MaximumSize": "Maximum Size", @@ -832,6 +834,7 @@ "ReleaseBranchCheckOfficialBranchMessage": "Branch {0} is not a valid Radarr release branch, you will not receive updates", "ReleaseDates": "Release Dates", "ReleaseGroup": "Release Group", + "ReleaseHash": "Release Hash", "ReleaseRejected": "Release Rejected", "ReleaseStatus": "Release Status", "ReleaseTitle": "Release Title", @@ -1073,6 +1076,7 @@ "TestAllClients": "Test All Clients", "TestAllIndexers": "Test All Indexers", "TestAllLists": "Test All Lists", + "TestParsing": "Test Parsing", "TheLogLevelDefault": "The log level defaults to 'Info' and can be changed in", "ThereWasAnErrorLoadingThisItem": "There was an error loading this item", "ThereWasAnErrorLoadingThisPage": "There was an error loading this page", diff --git a/src/Radarr.Api.V3/Parse/ParseController.cs b/src/Radarr.Api.V3/Parse/ParseController.cs index 46a0ab6c3..28d5eb878 100644 --- a/src/Radarr.Api.V3/Parse/ParseController.cs +++ b/src/Radarr.Api.V3/Parse/ParseController.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Download.Aggregation; using NzbDrone.Core.Parser; +using Radarr.Api.V3.CustomFormats; using Radarr.Api.V3.Movies; using Radarr.Http; @@ -14,14 +16,17 @@ namespace Radarr.Api.V3.Parse private readonly IParsingService _parsingService; private readonly IConfigService _configService; private readonly IRemoteMovieAggregationService _aggregationService; + private readonly ICustomFormatCalculationService _formatCalculator; public ParseController(IParsingService parsingService, IConfigService configService, - IRemoteMovieAggregationService aggregationService) + IRemoteMovieAggregationService aggregationService, + ICustomFormatCalculationService formatCalculator) { _parsingService = parsingService; _configService = configService; _aggregationService = aggregationService; + _formatCalculator = formatCalculator; } [HttpGet] @@ -44,15 +49,21 @@ namespace Radarr.Api.V3.Parse var remoteMovie = _parsingService.Map(parsedMovieInfo, "", 0); - _aggregationService.Augment(remoteMovie); - if (remoteMovie != null) { + _aggregationService.Augment(remoteMovie); + + remoteMovie.CustomFormats = _formatCalculator.ParseCustomFormat(remoteMovie, 0); + remoteMovie.CustomFormatScore = remoteMovie.Movie?.Profile?.CalculateCustomFormatScore(remoteMovie.CustomFormats) ?? 0; + return new ParseResource { Title = title, 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 diff --git a/src/Radarr.Api.V3/Parse/ParseResource.cs b/src/Radarr.Api.V3/Parse/ParseResource.cs index 10bd14617..7b637dd02 100644 --- a/src/Radarr.Api.V3/Parse/ParseResource.cs +++ b/src/Radarr.Api.V3/Parse/ParseResource.cs @@ -1,4 +1,7 @@ +using System.Collections.Generic; +using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; +using Radarr.Api.V3.CustomFormats; using Radarr.Api.V3.Movies; using Radarr.Http.REST; @@ -9,5 +12,8 @@ namespace Radarr.Api.V3.Parse public string Title { get; set; } public ParsedMovieInfo ParsedMovieInfo { get; set; } public MovieResource Movie { get; set; } + public List Languages { get; set; } + public List CustomFormats { get; set; } + public int CustomFormatScore { get; set; } } } diff --git a/yarn.lock b/yarn.lock index df4a66669..ce9222262 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1471,6 +1471,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"