From 2ad7396f6db5c2dfcb9e5b584f92650567cabb43 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 7 May 2023 18:55:00 -0700 Subject: [PATCH] New: Added UI for parsing release names (cherry picked from commit 85e285598106346099ceae676599c5cb4b789c92) --- frontend/src/App/AppRoutes.js | 4 +- frontend/src/App/State/AppState.ts | 2 + frontend/src/App/State/ParseAppState.ts | 35 ++++ frontend/src/Helpers/Props/icons.js | 2 + frontend/src/Parse/Parse.css | 45 +++++ frontend/src/Parse/Parse.css.d.ts | 12 ++ frontend/src/Parse/Parse.tsx | 109 ++++++++++++ frontend/src/Parse/ParseModal.tsx | 20 +++ frontend/src/Parse/ParseModalContent.css | 45 +++++ frontend/src/Parse/ParseModalContent.css.d.ts | 12 ++ frontend/src/Parse/ParseModalContent.tsx | 122 +++++++++++++ frontend/src/Parse/ParseResult.css | 8 + frontend/src/Parse/ParseResult.css.d.ts | 8 + frontend/src/Parse/ParseResult.tsx | 160 ++++++++++++++++++ frontend/src/Parse/ParseResultItem.css | 21 +++ frontend/src/Parse/ParseResultItem.css.d.ts | 8 + frontend/src/Parse/ParseResultItem.tsx | 20 +++ frontend/src/Parse/ParseToolbarButton.tsx | 31 ++++ frontend/src/Parse/parseStateSelector.ts | 12 ++ .../CustomFormatSettingsConnector.js | 33 ---- .../CustomFormatSettingsPage.tsx | 42 +++++ .../src/Settings/SettingsToolbarConnector.js | 1 + frontend/src/Store/Actions/index.js | 2 + frontend/src/Store/Actions/parseActions.ts | 111 ++++++++++++ src/Lidarr.Api.V1/openapi.json | 8 +- src/NzbDrone.Core/Localization/Core/en.json | 12 ++ .../Parser/Model/ParsedAlbumInfo.cs | 2 +- 27 files changed, 847 insertions(+), 40 deletions(-) create mode 100644 frontend/src/App/State/ParseAppState.ts create mode 100644 frontend/src/Parse/Parse.css create mode 100644 frontend/src/Parse/Parse.css.d.ts create mode 100644 frontend/src/Parse/Parse.tsx create mode 100644 frontend/src/Parse/ParseModal.tsx create mode 100644 frontend/src/Parse/ParseModalContent.css create mode 100644 frontend/src/Parse/ParseModalContent.css.d.ts create mode 100644 frontend/src/Parse/ParseModalContent.tsx create mode 100644 frontend/src/Parse/ParseResult.css create mode 100644 frontend/src/Parse/ParseResult.css.d.ts create mode 100644 frontend/src/Parse/ParseResult.tsx create mode 100644 frontend/src/Parse/ParseResultItem.css create mode 100644 frontend/src/Parse/ParseResultItem.css.d.ts create mode 100644 frontend/src/Parse/ParseResultItem.tsx create mode 100644 frontend/src/Parse/ParseToolbarButton.tsx create mode 100644 frontend/src/Parse/parseStateSelector.ts delete mode 100644 frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx create mode 100644 frontend/src/Store/Actions/parseActions.ts diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index 0af990f43..d12f3c4b0 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -11,7 +11,7 @@ import CalendarPageConnector from 'Calendar/CalendarPageConnector'; import NotFound from 'Components/NotFound'; import Switch from 'Components/Router/Switch'; import AddNewItemConnector from 'Search/AddNewItemConnector'; -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'; @@ -184,7 +184,7 @@ function AppRoutes(props) { ; + +export default ParseAppState; diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index ccb8b90e9..aa9c23145 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -32,6 +32,7 @@ import { faBookReader as fasBookReader, faBroadcastTower as fasBroadcastTower, faBug as fasBug, + faCalculator as fasCalculator, faCalendarAlt as fasCalendarAlt, faCaretDown as fasCaretDown, faCheck as fasCheck, @@ -187,6 +188,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; 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..15a0deb47 --- /dev/null +++ b/frontend/src/Parse/Parse.tsx @@ -0,0 +1,109 @@ +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 translate from 'Utilities/String/translate'; +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 ? ( +
+
+ {translate('ParseModalErrorParsing')} +
+
{getErrorMessage(error)}
+
+ ) : null} + + {!isFetching && title && !error && !item.parsedAlbumInfo ? ( +
+ {translate('ParseModalUnableToParse')} +
+ ) : null} + + {!isFetching && !error && item.parsedAlbumInfo ? ( + + ) : null} + + {title ? null : ( +
+
+ {translate('ParseModalHelpText')} +
+
{translate('ParseModalHelpTextDetails')}
+
+ )} +
+
+ ); +} + +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..d5ae93759 --- /dev/null +++ b/frontend/src/Parse/ParseModalContent.tsx @@ -0,0 +1,122 @@ +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 ? ( +
+
+ {translate('ParseModalErrorParsing')} +
+
{getErrorMessage(error)}
+
+ ) : null} + + {!isFetching && title && !error && !item.parsedAlbumInfo ? ( +
+ {translate('ParseModalUnableToParse')} +
+ ) : null} + + {!isFetching && !error && item.parsedAlbumInfo ? ( + + ) : null} + + {title ? null : ( +
+
+ {translate('ParseModalHelpText')} +
+
{translate('ParseModalHelpTextDetails')}
+
+ )} +
+ + + + +
+ ); +} + +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..7e6c40d92 --- /dev/null +++ b/frontend/src/Parse/ParseResult.tsx @@ -0,0 +1,160 @@ +import _ from 'lodash'; +import moment from 'moment'; +import React from 'react'; +import AlbumFormats from 'Album/AlbumFormats'; +import AlbumTitleLink from 'Album/AlbumTitleLink'; +import { ParseModel } from 'App/State/ParseAppState'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import FieldSet from 'Components/FieldSet'; +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, albums, parsedAlbumInfo, artist } = + item; + + const { + releaseTitle, + artistName, + albumTitle, + releaseGroup, + discography, + quality, + } = parsedAlbumInfo; + + const sortedAlbums = _.sortBy(albums, (item) => + moment(item.releaseDate).unix() + ); + + return ( +
+
+ + + + + + + +
+ +
+
+
+ +
+
+
+ +
+
+
+ + 1 && !quality.revision.isRepack + ? translate('True') + : '-' + } + /> + + +
+ +
+ 1 ? quality.revision.version : '-' + } + /> + + +
+
+
+ +
+ + ) : ( + '-' + ) + } + /> + + + {sortedAlbums.map((album) => { + return ( +
+ +
+ ); + })} +
+ ) : ( + '-' + ) + } + /> + + + ) : ( + '-' + ) + } + /> + + + + + ); +} + +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/CustomFormatSettingsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js deleted file mode 100644 index 342df29d2..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js +++ /dev/null @@ -1,33 +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 translate from 'Utilities/String/translate'; -import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector'; - -class CustomFormatSettingsConnector extends Component { - - // - // Render - - render() { - return ( - - - - - - - - - - ); - } -} - -export default CustomFormatSettingsConnector; - diff --git a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx new file mode 100644 index 000000000..fee176554 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx @@ -0,0 +1,42 @@ +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 translate from 'Utilities/String/translate'; +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 95b02d089..85bcc2645 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -13,6 +13,7 @@ import * as history from './historyActions'; 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'; @@ -41,6 +42,7 @@ export default [ oAuth, organizePreview, retagPreview, + 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/src/Lidarr.Api.V1/openapi.json b/src/Lidarr.Api.V1/openapi.json index 67f10c69f..d3d5e381c 100644 --- a/src/Lidarr.Api.V1/openapi.json +++ b/src/Lidarr.Api.V1/openapi.json @@ -11263,6 +11263,10 @@ "ParsedAlbumInfo": { "type": "object", "properties": { + "releaseTitle": { + "type": "string", + "nullable": true + }, "albumTitle": { "type": "string", "nullable": true @@ -11307,10 +11311,6 @@ "releaseVersion": { "type": "string", "nullable": true - }, - "releaseTitle": { - "type": "string", - "nullable": true } }, "additionalProperties": false diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 0a2ebd03c..d279bfadd 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -53,6 +53,7 @@ "AlbumCount": "Album Count", "AlbumDetails": "Album Details", "AlbumHasNotAired": "Album has not aired", + "AlbumInfo": "Album Info", "AlbumIsDownloading": "Album is downloading", "AlbumIsDownloadingInterp": "Album is downloading - {0}% {1}", "AlbumIsNotMonitored": "Album is not monitored", @@ -467,6 +468,7 @@ "FailedDownloadHandling": "Failed Download Handling", "FailedLoadingSearchResults": "Failed to load search results, please try again.", "FailedToLoadQueue": "Failed to load Queue", + "False": "False", "FileDateHelpText": "Change file date on import/rescan", "FileManagement": "File Management", "FileNameTokens": "File Name Tokens", @@ -677,6 +679,8 @@ "MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?", "MassAlbumsCutoffUnmetWarning": "Are you sure you want to search for all '{0}' Cutoff Unmet albums?", "MassSearchCancelWarning": "This cannot be cancelled once started without restarting {appName} or disabling all of your indexers.", + "MatchedToAlbums": "Matched to Albums", + "MatchedToArtist": "Matched to Artist", "MaximumLimits": "Maximum Limits", "MaximumSize": "Maximum Size", "MaximumSizeHelpText": "Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited.", @@ -836,6 +840,11 @@ "PackageVersion": "Package Version", "PageSize": "Page Size", "PageSizeHelpText": "Number of items to show on each page", + "Parse": "Parse", + "ParseModalErrorParsing": "Error parsing, please try again.", + "ParseModalHelpText": "Enter a release title in the input above", + "ParseModalHelpTextDetails": "{appName} will attempt to parse the title and show you details about it", + "ParseModalUnableToParse": "Unable to parse the provided title, please try again.", "Password": "Password", "PasswordConfirmation": "Password Confirmation", "PastDays": "Past Days", @@ -982,6 +991,7 @@ "RenameTracksHelpText": "{appName} will use the existing file name if renaming is disabled", "Renamed": "Renamed", "Reorder": "Reorder", + "Repack": "Repack", "Replace": "Replace", "ReplaceExistingFiles": "Replace Existing Files", "ReplaceIllegalCharacters": "Replace Illegal Characters", @@ -1154,6 +1164,7 @@ "TestAllClients": "Test All Clients", "TestAllIndexers": "Test All Indexers", "TestAllLists": "Test All Lists", + "TestParsing": "Test Parsing", "TheAlbumsFilesWillBeDeleted": "The album's files will be deleted.", "TheArtistFolderStrongpathstrongAndAllOfItsContentWillBeDeleted": "The artist folder '{0}' and all of its content will be deleted.", "Theme": "Theme", @@ -1193,6 +1204,7 @@ "TrackStatus": "Track status", "TrackTitle": "Track Title", "Tracks": "Tracks", + "True": "True", "Type": "Type", "URLBase": "URL Base", "Ui": "UI", diff --git a/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs index be30e257f..1fd80a6f9 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs @@ -6,6 +6,7 @@ namespace NzbDrone.Core.Parser.Model { public class ParsedAlbumInfo { + public string ReleaseTitle { get; set; } public string AlbumTitle { get; set; } public string ArtistName { get; set; } public string AlbumType { get; set; } @@ -18,7 +19,6 @@ namespace NzbDrone.Core.Parser.Model public string ReleaseGroup { get; set; } public string ReleaseHash { get; set; } public string ReleaseVersion { get; set; } - public string ReleaseTitle { get; set; } [JsonIgnore] public Dictionary ExtraInfo { get; set; } = new Dictionary();