Bogdan 2 months ago committed by GitHub
commit 2d1783825e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -11,7 +11,7 @@ import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import NotFound from 'Components/NotFound'; import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch'; import Switch from 'Components/Router/Switch';
import AddNewItemConnector from 'Search/AddNewItemConnector'; import AddNewItemConnector from 'Search/AddNewItemConnector';
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';
@ -184,7 +184,7 @@ function AppRoutes(props) {
<Route <Route
path="/settings/customformats" path="/settings/customformats"
component={CustomFormatSettingsConnector} component={CustomFormatSettingsPage}
/> />
<Route <Route

@ -1,3 +1,4 @@
import ParseAppState from 'App/State/ParseAppState';
import AlbumAppState from './AlbumAppState'; import AlbumAppState from './AlbumAppState';
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState'; import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
import CalendarAppState from './CalendarAppState'; import CalendarAppState from './CalendarAppState';
@ -57,6 +58,7 @@ interface AppState {
calendar: CalendarAppState; calendar: CalendarAppState;
commands: CommandAppState; commands: CommandAppState;
history: HistoryAppState; history: HistoryAppState;
parse: ParseAppState;
queue: QueueAppState; queue: QueueAppState;
settings: SettingsAppState; settings: SettingsAppState;
tags: TagsAppState; tags: TagsAppState;

@ -0,0 +1,35 @@
import Album from 'Album/Album';
import ModelBase from 'App/ModelBase';
import { AppSectionItemState } from 'App/State/AppSectionState';
import Artist from 'Artist/Artist';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
export interface ArtistTitleInfo {
title: string;
}
export interface ParsedAlbumInfo {
albumTitle: string;
artistName: string;
artistTitleInfo: ArtistTitleInfo;
discography: boolean;
quality: QualityModel;
releaseGroup?: string;
releaseHash: string;
releaseTitle: string;
releaseTokens: string;
}
export interface ParseModel extends ModelBase {
title: string;
parsedAlbumInfo: ParsedAlbumInfo;
artist?: Artist;
albums: Album[];
customFormats?: CustomFormat[];
customFormatScore?: number;
}
type ParseAppState = AppSectionItemState<ParseModel>;
export default ParseAppState;

@ -164,6 +164,10 @@ const links = [
{ {
title: () => translate('LogFiles'), title: () => translate('LogFiles'),
to: '/system/logs/files' to: '/system/logs/files'
},
{
title: 'Parse Testing',
to: '/system/parse'
} }
] ]
} }

@ -32,6 +32,7 @@ import {
faBookReader as fasBookReader, faBookReader as fasBookReader,
faBroadcastTower as fasBroadcastTower, faBroadcastTower as fasBroadcastTower,
faBug as fasBug, faBug as fasBug,
faCalculator as fasCalculator,
faCalendarAlt as fasCalendarAlt, faCalendarAlt as fasCalendarAlt,
faCaretDown as fasCaretDown, faCaretDown as fasCaretDown,
faCheck as fasCheck, faCheck as fasCheck,
@ -187,6 +188,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 PROFILE = fasUser; export const PROFILE = fasUser;

@ -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,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 (
<PageContent title={translate('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. Artist.Name-Album.Title[FLAC]-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}>
{translate('ParseModalErrorParsing')}
</div>
<div>{getErrorMessage(error)}</div>
</div>
) : null}
{!isFetching && title && !error && !item.parsedAlbumInfo ? (
<div className={styles.message}>
{translate('ParseModalUnableToParse')}
</div>
) : null}
{!isFetching && !error && item.parsedAlbumInfo ? (
<ParseResult item={item} />
) : null}
{title ? null : (
<div className={styles.message}>
<div className={styles.helpText}>
{translate('ParseModalHelpText')}
</div>
<div>{translate('ParseModalHelpTextDetails')}</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,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 (
<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. Artist.Name-Album.Title[FLAC]-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}>
{translate('ParseModalErrorParsing')}
</div>
<div>{getErrorMessage(error)}</div>
</div>
) : null}
{!isFetching && title && !error && !item.parsedAlbumInfo ? (
<div className={styles.message}>
{translate('ParseModalUnableToParse')}
</div>
) : null}
{!isFetching && !error && item.parsedAlbumInfo ? (
<ParseResult item={item} />
) : null}
{title ? null : (
<div className={styles.message}>
<div className={styles.helpText}>
{translate('ParseModalHelpText')}
</div>
<div>{translate('ParseModalHelpTextDetails')}</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,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 (
<div>
<FieldSet legend={translate('Release')}>
<ParseResultItem
title={translate('ReleaseTitle')}
data={releaseTitle}
/>
<ParseResultItem title={translate('ArtistName')} data={artistName} />
<ParseResultItem title={translate('AlbumTitle')} data={albumTitle} />
<ParseResultItem
title={translate('ReleaseGroup')}
data={releaseGroup ?? '-'}
/>
</FieldSet>
<FieldSet legend={translate('AlbumInfo')}>
<div className={styles.container}>
<div className={styles.column}>
<ParseResultItem
title={translate('Discography')}
data={discography ? translate('True') : translate('False')}
/>
</div>
</div>
</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
? translate('True')
: '-'
}
/>
<ParseResultItem
title={translate('Repack')}
data={quality.revision.isRepack ? translate('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 ? translate('True') : '-'}
/>
</div>
</div>
</FieldSet>
<FieldSet legend={translate('Details')}>
<ParseResultItem
title={translate('MatchedToArtist')}
data={
artist ? (
<ArtistNameLink
foreignArtistId={artist.foreignArtistId}
artistName={artist.artistName}
/>
) : (
'-'
)
}
/>
<ParseResultItem
title={translate('MatchedToAlbums')}
data={
sortedAlbums.length ? (
<div>
{sortedAlbums.map((album) => {
return (
<div key={album.id}>
<AlbumTitleLink
foreignAlbumId={album.foreignAlbumId}
title={album.title}
disambiguation={album.disambiguation}
/>
</div>
);
})}
</div>
) : (
'-'
)
}
/>
<ParseResultItem
title={translate('CustomFormats')}
data={
customFormats?.length ? (
<AlbumFormats 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;
}
);
}

@ -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 (
<PageContent title={translate('CustomFormatSettings')}>
<SettingsToolbarConnector
showSave={false}
/>
<PageContentBody>
<DndProvider backend={HTML5Backend}>
<CustomFormatsConnector />
</DndProvider>
</PageContentBody>
</PageContent>
);
}
}
export default CustomFormatSettingsConnector;

@ -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 (
<PageContent title={translate('CustomFormatsSettings')}>
<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,

@ -13,6 +13,7 @@ import * as history from './historyActions';
import * as interactiveImportActions from './interactiveImportActions'; import * as interactiveImportActions from './interactiveImportActions';
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';
@ -41,6 +42,7 @@ export default [
oAuth, oAuth,
organizePreview, organizePreview,
retagPreview, retagPreview,
parse,
paths, paths,
providerOptions, providerOptions,
queue, queue,

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

@ -53,6 +53,7 @@
"AlbumCount": "Album Count", "AlbumCount": "Album Count",
"AlbumDetails": "Album Details", "AlbumDetails": "Album Details",
"AlbumHasNotAired": "Album has not aired", "AlbumHasNotAired": "Album has not aired",
"AlbumInfo": "Album Info",
"AlbumIsDownloading": "Album is downloading", "AlbumIsDownloading": "Album is downloading",
"AlbumIsDownloadingInterp": "Album is downloading - {0}% {1}", "AlbumIsDownloadingInterp": "Album is downloading - {0}% {1}",
"AlbumIsNotMonitored": "Album is not monitored", "AlbumIsNotMonitored": "Album is not monitored",
@ -465,6 +466,7 @@
"FailedDownloadHandling": "Failed Download Handling", "FailedDownloadHandling": "Failed Download Handling",
"FailedLoadingSearchResults": "Failed to load search results, please try again.", "FailedLoadingSearchResults": "Failed to load search results, please try again.",
"FailedToLoadQueue": "Failed to load Queue", "FailedToLoadQueue": "Failed to load Queue",
"False": "False",
"FileDateHelpText": "Change file date on import/rescan", "FileDateHelpText": "Change file date on import/rescan",
"FileManagement": "File Management", "FileManagement": "File Management",
"FileNameTokens": "File Name Tokens", "FileNameTokens": "File Name Tokens",
@ -668,6 +670,8 @@
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?", "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?", "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.", "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", "MaximumLimits": "Maximum Limits",
"MaximumSize": "Maximum Size", "MaximumSize": "Maximum Size",
"MaximumSizeHelpText": "Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited.", "MaximumSizeHelpText": "Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited.",
@ -825,6 +829,11 @@
"PackageVersion": "Package Version", "PackageVersion": "Package Version",
"PageSize": "Page Size", "PageSize": "Page Size",
"PageSizeHelpText": "Number of items to show on each page", "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", "Password": "Password",
"PasswordConfirmation": "Password Confirmation", "PasswordConfirmation": "Password Confirmation",
"PastDays": "Past Days", "PastDays": "Past Days",
@ -971,6 +980,7 @@
"RenameTracksHelpText": "{appName} will use the existing file name if renaming is disabled", "RenameTracksHelpText": "{appName} will use the existing file name if renaming is disabled",
"Renamed": "Renamed", "Renamed": "Renamed",
"Reorder": "Reorder", "Reorder": "Reorder",
"Repack": "Repack",
"Replace": "Replace", "Replace": "Replace",
"ReplaceExistingFiles": "Replace Existing Files", "ReplaceExistingFiles": "Replace Existing Files",
"ReplaceIllegalCharacters": "Replace Illegal Characters", "ReplaceIllegalCharacters": "Replace Illegal Characters",
@ -1143,6 +1153,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",
"TheAlbumsFilesWillBeDeleted": "The album's files will be deleted.", "TheAlbumsFilesWillBeDeleted": "The album's files will be deleted.",
"TheArtistFolderStrongpathstrongAndAllOfItsContentWillBeDeleted": "The artist folder '{0}' and all of its content will be deleted.", "TheArtistFolderStrongpathstrongAndAllOfItsContentWillBeDeleted": "The artist folder '{0}' and all of its content will be deleted.",
"Theme": "Theme", "Theme": "Theme",
@ -1182,6 +1193,7 @@
"TrackStatus": "Track status", "TrackStatus": "Track status",
"TrackTitle": "Track Title", "TrackTitle": "Track Title",
"Tracks": "Tracks", "Tracks": "Tracks",
"True": "True",
"Type": "Type", "Type": "Type",
"URLBase": "URL Base", "URLBase": "URL Base",
"Ui": "UI", "Ui": "UI",

@ -6,6 +6,7 @@ namespace NzbDrone.Core.Parser.Model
{ {
public class ParsedAlbumInfo public class ParsedAlbumInfo
{ {
public string ReleaseTitle { get; set; }
public string AlbumTitle { get; set; } public string AlbumTitle { get; set; }
public string ArtistName { get; set; } public string ArtistName { get; set; }
public string AlbumType { get; set; } public string AlbumType { get; set; }
@ -18,7 +19,6 @@ namespace NzbDrone.Core.Parser.Model
public string ReleaseGroup { get; set; } public string ReleaseGroup { get; set; }
public string ReleaseHash { get; set; } public string ReleaseHash { get; set; }
public string ReleaseVersion { get; set; } public string ReleaseVersion { get; set; }
public string ReleaseTitle { get; set; }
[JsonIgnore] [JsonIgnore]
public Dictionary<string, object> ExtraInfo { get; set; } = new Dictionary<string, object>(); public Dictionary<string, object> ExtraInfo { get; set; } = new Dictionary<string, object>();

Loading…
Cancel
Save