1
0
Fork 0
mirror of https://github.com/lidarr/Lidarr synced 2024-12-21 23:32:27 +00:00

New: Added UI for parsing release names

(cherry picked from commit 85e285598106346099ceae676599c5cb4b789c92)
This commit is contained in:
Mark McDowall 2023-05-07 18:55:00 -07:00 committed by Bogdan
parent bcfabacbd4
commit 2ad7396f6d
27 changed files with 847 additions and 40 deletions

View file

@ -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) {
<Route
path="/settings/customformats"
component={CustomFormatSettingsConnector}
component={CustomFormatSettingsPage}
/>
<Route

View file

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

View file

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

View file

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

View file

@ -0,0 +1,45 @@
.inputContainer {
display: flex;
margin-bottom: 10px;
}
.inputIconContainer {
width: 58px;
height: 46px;
border: 1px solid var(--inputBorderColor);
border-right: none;
border-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
background-color: var(--inputIconContainerBackgroundColor);
text-align: center;
line-height: 46px;
}
.input {
composes: input from '~Components/Form/TextInput.css';
height: 46px;
border-radius: 0;
font-size: 18px;
}
.clearButton {
border: 1px solid var(--inputBorderColor);
border-left: none;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.message {
margin-top: 30px;
text-align: center;
font-weight: 300;
font-size: $largeFontSize;
}
.helpText {
margin-bottom: 10px;
font-size: 24px;
}

12
frontend/src/Parse/Parse.css.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'clearButton': string;
'helpText': string;
'input': string;
'inputContainer': string;
'inputIconContainer': string;
'message': string;
}
export const cssExports: CssExports;
export default cssExports;

View file

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

View file

@ -0,0 +1,20 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ParseModalContent from './ParseModalContent';
interface ParseModalProps {
isOpen: boolean;
onModalClose: () => void;
}
function ParseModal(props: ParseModalProps) {
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ParseModalContent onModalClose={onModalClose} />
</Modal>
);
}
export default ParseModal;

View file

@ -0,0 +1,45 @@
.inputContainer {
display: flex;
margin-bottom: 10px;
}
.inputIconContainer {
width: 58px;
height: 46px;
border: 1px solid var(--inputBorderColor);
border-right: none;
border-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
background-color: var(--inputIconContainerBackgroundColor);
text-align: center;
line-height: 46px;
}
.input {
composes: input from '~Components/Form/TextInput.css';
height: 46px;
border-radius: 0;
font-size: 18px;
}
.clearButton {
border: 1px solid var(--inputBorderColor);
border-left: none;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.message {
margin-top: 30px;
text-align: center;
font-weight: 300;
font-size: $largeFontSize;
}
.helpText {
margin-bottom: 10px;
font-size: 24px;
}

View file

@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'clearButton': string;
'helpText': string;
'input': string;
'inputContainer': string;
'inputIconContainer': string;
'message': string;
}
export const cssExports: CssExports;
export default cssExports;

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,21 @@
.item {
display: flex;
}
.title {
margin-right: 20px;
width: 250px;
text-align: right;
font-weight: bold;
}
@media (max-width: $breakpointSmall) {
.item {
display: block;
margin-bottom: 10px;
}
.title {
text-align: left;
}
}

View file

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

View file

@ -0,0 +1,20 @@
import React, { ReactNode } from 'react';
import styles from './ParseResultItem.css';
interface ParseResultItemProps {
title: string;
data: string | number | ReactNode;
}
function ParseResultItem(props: ParseResultItemProps) {
const { title, data } = props;
return (
<div className={styles.item}>
<div className={styles.title}>{title}</div>
<div>{data}</div>
</div>
);
}
export default ParseResultItem;

View file

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

View file

@ -0,0 +1,12 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import ParseAppState from 'App/State/ParseAppState';
export default function parseStateSelector() {
return createSelector(
(state: AppState) => state.parse,
(parse: ParseAppState) => {
return parse;
}
);
}

View file

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

View file

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

View file

@ -134,6 +134,7 @@ const historyShape = {
};
SettingsToolbarConnector.propTypes = {
showSave: PropTypes.bool,
hasPendingChanges: PropTypes.bool.isRequired,
history: PropTypes.shape(historyShape).isRequired,
onSavePress: PropTypes.func,

View file

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

View file

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

View file

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

View file

@ -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",

View file

@ -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 @@ public class ParsedAlbumInfo
public string ReleaseGroup { get; set; }
public string ReleaseHash { get; set; }
public string ReleaseVersion { get; set; }
public string ReleaseTitle { get; set; }
[JsonIgnore]
public Dictionary<string, object> ExtraInfo { get; set; } = new Dictionary<string, object>();