mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-16 10:03:51 -07:00
[UI Work] Settings Naming Page, Other Settings
This commit is contained in:
parent
22d9c5e666
commit
456ead09da
14 changed files with 281 additions and 393 deletions
|
@ -74,7 +74,7 @@ class OrganizePreviewModalContent extends Component {
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
renameEpisodes,
|
renameTracks,
|
||||||
episodeFormat,
|
episodeFormat,
|
||||||
path,
|
path,
|
||||||
onModalClose
|
onModalClose
|
||||||
|
@ -109,7 +109,7 @@ class OrganizePreviewModalContent extends Component {
|
||||||
!isFetching && isPopulated && !items.length &&
|
!isFetching && isPopulated && !items.length &&
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
renameEpisodes ?
|
renameTracks ?
|
||||||
<div>Success! My work is done, no files to rename.</div> :
|
<div>Success! My work is done, no files to rename.</div> :
|
||||||
<div>Renaming is disabled, nothing to rename</div>
|
<div>Renaming is disabled, nothing to rename</div>
|
||||||
}
|
}
|
||||||
|
@ -191,7 +191,7 @@ OrganizePreviewModalContent.propTypes = {
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
renameEpisodes: PropTypes.bool,
|
renameTracks: PropTypes.bool,
|
||||||
episodeFormat: PropTypes.string,
|
episodeFormat: PropTypes.string,
|
||||||
onOrganizePress: PropTypes.func.isRequired,
|
onOrganizePress: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|
|
@ -19,7 +19,7 @@ function createMapStateToProps() {
|
||||||
props.isFetching = organizePreview.isFetching || naming.isFetching;
|
props.isFetching = organizePreview.isFetching || naming.isFetching;
|
||||||
props.isPopulated = organizePreview.isPopulated && naming.isPopulated;
|
props.isPopulated = organizePreview.isPopulated && naming.isPopulated;
|
||||||
props.error = organizePreview.error || naming.error;
|
props.error = organizePreview.error || naming.error;
|
||||||
props.renameEpisodes = naming.item.renameEpisodes;
|
props.renameTracks = naming.item.renameTracks;
|
||||||
props.episodeFormat = naming.item[`${series.seriesType}EpisodeFormat`];
|
props.episodeFormat = naming.item[`${series.seriesType}EpisodeFormat`];
|
||||||
props.path = series.path;
|
props.path = series.path;
|
||||||
|
|
||||||
|
|
|
@ -519,7 +519,7 @@ class GeneralSettings extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="analyticsEnabled"
|
name="analyticsEnabled"
|
||||||
helpText="Send anonymous usage and error information to Sonarr's servers. This includes information on your browser, which Sonarr WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes."
|
helpText="Send anonymous usage and error information to Lidarr's servers. This includes information on your browser, which Lidarr WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes."
|
||||||
helpTextWarning="Requires restart to take effect"
|
helpTextWarning="Requires restart to take effect"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...analyticsEnabled}
|
{...analyticsEnabled}
|
||||||
|
@ -541,7 +541,7 @@ class GeneralSettings extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.TEXT}
|
||||||
name="branch"
|
name="branch"
|
||||||
helpText="Branch to use to update Sonarr"
|
helpText="Branch to use to update Lidarr"
|
||||||
helpLink="https://github.com/Sonarr/Sonarr/wiki/Release-Branches"
|
helpLink="https://github.com/Sonarr/Sonarr/wiki/Release-Branches"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...branch}
|
{...branch}
|
||||||
|
@ -622,8 +622,8 @@ class GeneralSettings extends Component {
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isRestartRequiredModalOpen}
|
isOpen={isRestartRequiredModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title="Restart Sonarr"
|
title="Restart Lidarr"
|
||||||
message="Sonarr requires a restart to apply changes, do you want to restart now?"
|
message="Lidarr requires a restart to apply changes, do you want to restart now?"
|
||||||
cancelLabel="I'll restart later"
|
cancelLabel="I'll restart later"
|
||||||
confirmLabel="Restart Now"
|
confirmLabel="Restart Now"
|
||||||
onConfirm={this.onConfirmRestart}
|
onConfirm={this.onConfirmRestart}
|
||||||
|
|
|
@ -73,14 +73,14 @@ class MediaManagement extends Component {
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
size={sizes.MEDIUM}
|
size={sizes.MEDIUM}
|
||||||
>
|
>
|
||||||
<FormLabel>Create empty series folders</FormLabel>
|
<FormLabel>Create empty artist folders</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="createEmptySeriesFolders"
|
name="createEmptyArtistFolders"
|
||||||
helpText="Create missing series folders during disk scan"
|
helpText="Create missing series folders during disk scan"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.createEmptySeriesFolders}
|
{...settings.createEmptyArtistFolders}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
@ -103,7 +103,7 @@ class MediaManagement extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="skipFreeSpaceCheckWhenImporting"
|
name="skipFreeSpaceCheckWhenImporting"
|
||||||
helpText="Use when Lidarr is unable to detect free space from your series root folder"
|
helpText="Use when Lidarr is unable to detect free space from your artist root folder"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||||
/>
|
/>
|
||||||
|
@ -133,7 +133,7 @@ class MediaManagement extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="importExtraFiles"
|
name="importExtraFiles"
|
||||||
helpText="Import matching extra files (subtitles, nfo, etc) after importing an episode file"
|
helpText="Import matching extra files (subtitles, nfo, etc) after importing an track file"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.importExtraFiles}
|
{...settings.importExtraFiles}
|
||||||
/>
|
/>
|
||||||
|
@ -163,14 +163,14 @@ class MediaManagement extends Component {
|
||||||
legend="File Management"
|
legend="File Management"
|
||||||
>
|
>
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>Ignore Deleted Episodes</FormLabel>
|
<FormLabel>Ignore Deleted Tracks</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="autoUnmonitorPreviouslyDownloadedEpisodes"
|
name="autoUnmonitorPreviouslyDownloadedTracks"
|
||||||
helpText="Episodes deleted from disk are automatically unmonitored in Lidarr"
|
helpText="Tracks deleted from disk are automatically unmonitored in Lidarr"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.autoUnmonitorPreviouslyDownloadedEpisodes}
|
{...settings.autoUnmonitorPreviouslyDownloadedTracks}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
@ -195,12 +195,12 @@ class MediaManagement extends Component {
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
size={sizes.MEDIUM}
|
size={sizes.MEDIUM}
|
||||||
>
|
>
|
||||||
<FormLabel>Analyse video files</FormLabel>
|
<FormLabel>Analyse audio files</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="enableMediaInfo"
|
name="enableMediaInfo"
|
||||||
helpText="Extract video information such as resolution, runtime and codec information from files. This requires Lidarr to read parts of the file which may cause high disk or network activity during scans."
|
helpText="Extract audio information such as bitrate, runtime and codec information from files. This requires Lidarr to read parts of the file which may cause high disk or network activity during scans."
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.enableMediaInfo}
|
{...settings.enableMediaInfo}
|
||||||
/>
|
/>
|
||||||
|
@ -231,7 +231,7 @@ class MediaManagement extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.PATH}
|
type={inputTypes.PATH}
|
||||||
name="recycleBin"
|
name="recycleBin"
|
||||||
helpText="Episode files will go here when deleted instead of being permanently deleted"
|
helpText="Track files will go here when deleted instead of being permanently deleted"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.recycleBin}
|
{...settings.recycleBin}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -32,55 +32,29 @@ class Naming extends Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
isNamingModalOpen: true,
|
isNamingModalOpen: true,
|
||||||
namingModalOptions: {
|
namingModalOptions: {
|
||||||
name: 'standardEpisodeFormat',
|
name: 'standardTrackFormat',
|
||||||
season: true,
|
album: true,
|
||||||
episode: true,
|
track: true,
|
||||||
additional: true
|
additional: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onDailyNamingModalOpenClick = () => {
|
onArtistFolderNamingModalOpenClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isNamingModalOpen: true,
|
isNamingModalOpen: true,
|
||||||
namingModalOptions: {
|
namingModalOptions: {
|
||||||
name: 'dailyEpisodeFormat',
|
name: 'artistFolderFormat'
|
||||||
season: true,
|
|
||||||
episode: true,
|
|
||||||
daily: true,
|
|
||||||
additional: true
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onAnimeNamingModalOpenClick = () => {
|
onAlbumFolderNamingModalOpenClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isNamingModalOpen: true,
|
isNamingModalOpen: true,
|
||||||
namingModalOptions: {
|
namingModalOptions: {
|
||||||
name: 'animeEpisodeFormat',
|
name: 'albumFolderFormat',
|
||||||
season: true,
|
album: true
|
||||||
episode: true,
|
|
||||||
anime: true,
|
|
||||||
additional: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSeriesFolderNamingModalOpenClick = () => {
|
|
||||||
this.setState({
|
|
||||||
isNamingModalOpen: true,
|
|
||||||
namingModalOptions: {
|
|
||||||
name: 'seriesFolderFormat'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSeasonFolderNamingModalOpenClick = () => {
|
|
||||||
this.setState({
|
|
||||||
isNamingModalOpen: true,
|
|
||||||
namingModalOptions: {
|
|
||||||
name: 'seasonFolderFormat',
|
|
||||||
season: true
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -109,69 +83,56 @@ class Naming extends Component {
|
||||||
namingModalOptions
|
namingModalOptions
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const renameEpisodes = hasSettings && settings.renameEpisodes.value;
|
const renameTracks = hasSettings && settings.renameTracks.value;
|
||||||
|
|
||||||
const multiEpisodeStyleOptions = [
|
const standardTrackFormatHelpTexts = [];
|
||||||
{ key: 0, value: 'Extend' },
|
const standardTrackFormatErrors = [];
|
||||||
{ key: 1, value: 'Duplicate' },
|
const artistFolderFormatHelpTexts = [];
|
||||||
{ key: 2, value: 'Repeat' },
|
const artistFolderFormatErrors = [];
|
||||||
{ key: 3, value: 'Scene' },
|
const albumFolderFormatHelpTexts = [];
|
||||||
{ key: 4, value: 'Range' },
|
const albumFolderFormatErrors = [];
|
||||||
{ key: 5, value: 'Prefixed Range' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const standardEpisodeFormatHelpTexts = [];
|
|
||||||
const standardEpisodeFormatErrors = [];
|
|
||||||
const dailyEpisodeFormatHelpTexts = [];
|
|
||||||
const dailyEpisodeFormatErrors = [];
|
|
||||||
const animeEpisodeFormatHelpTexts = [];
|
|
||||||
const animeEpisodeFormatErrors = [];
|
|
||||||
const seriesFolderFormatHelpTexts = [];
|
|
||||||
const seriesFolderFormatErrors = [];
|
|
||||||
const seasonFolderFormatHelpTexts = [];
|
|
||||||
const seasonFolderFormatErrors = [];
|
|
||||||
|
|
||||||
if (examplesPopulated) {
|
if (examplesPopulated) {
|
||||||
if (examples.singleEpisodeExample) {
|
if (examples.singleTrackExample) {
|
||||||
standardEpisodeFormatHelpTexts.push(`Single Episode: ${examples.singleEpisodeExample}`);
|
standardTrackFormatHelpTexts.push(`Single Track: ${examples.singleTrackExample}`);
|
||||||
} else {
|
} else {
|
||||||
standardEpisodeFormatErrors.push('Single Episode: Invalid Format');
|
standardTrackFormatErrors.push('Single Track: Invalid Format');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (examples.multiEpisodeExample) {
|
// if (examples.multiEpisodeExample) {
|
||||||
standardEpisodeFormatHelpTexts.push(`Multi Episode: ${examples.multiEpisodeExample}`);
|
// standardTrackFormatHelpTexts.push(`Multi Episode: ${examples.multiEpisodeExample}`);
|
||||||
|
// } else {
|
||||||
|
// standardTrackFormatErrors.push('Multi Episode: Invalid Format');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (examples.dailyEpisodeExample) {
|
||||||
|
// dailyEpisodeFormatHelpTexts.push(`Example: ${examples.dailyEpisodeExample}`);
|
||||||
|
// } else {
|
||||||
|
// dailyEpisodeFormatErrors.push('Invalid Format');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (examples.animeEpisodeExample) {
|
||||||
|
// animeEpisodeFormatHelpTexts.push(`Single Episode: ${examples.animeEpisodeExample}`);
|
||||||
|
// } else {
|
||||||
|
// animeEpisodeFormatErrors.push('Single Episode: Invalid Format');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (examples.animeMultiEpisodeExample) {
|
||||||
|
// animeEpisodeFormatHelpTexts.push(`Multi Episode: ${examples.animeMultiEpisodeExample}`);
|
||||||
|
// } else {
|
||||||
|
// animeEpisodeFormatErrors.push('Multi Episode: Invalid Format');
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (examples.artistFolderExample) {
|
||||||
|
artistFolderFormatHelpTexts.push(`Example: ${examples.artistFolderExample}`);
|
||||||
} else {
|
} else {
|
||||||
standardEpisodeFormatErrors.push('Multi Episode: Invalid Format');
|
artistFolderFormatErrors.push('Invalid Format');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (examples.dailyEpisodeExample) {
|
if (examples.albumFolderExample) {
|
||||||
dailyEpisodeFormatHelpTexts.push(`Example: ${examples.dailyEpisodeExample}`);
|
albumFolderFormatHelpTexts.push(`Example: ${examples.albumFolderExample}`);
|
||||||
} else {
|
} else {
|
||||||
dailyEpisodeFormatErrors.push('Invalid Format');
|
albumFolderFormatErrors.push('Invalid Format');
|
||||||
}
|
|
||||||
|
|
||||||
if (examples.animeEpisodeExample) {
|
|
||||||
animeEpisodeFormatHelpTexts.push(`Single Episode: ${examples.animeEpisodeExample}`);
|
|
||||||
} else {
|
|
||||||
animeEpisodeFormatErrors.push('Single Episode: Invalid Format');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (examples.animeMultiEpisodeExample) {
|
|
||||||
animeEpisodeFormatHelpTexts.push(`Multi Episode: ${examples.animeMultiEpisodeExample}`);
|
|
||||||
} else {
|
|
||||||
animeEpisodeFormatErrors.push('Multi Episode: Invalid Format');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (examples.seriesFolderExample) {
|
|
||||||
seriesFolderFormatHelpTexts.push(`Example: ${examples.seriesFolderExample}`);
|
|
||||||
} else {
|
|
||||||
seriesFolderFormatErrors.push('Invalid Format');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (examples.seasonFolderExample) {
|
|
||||||
seasonFolderFormatHelpTexts.push(`Example: ${examples.seasonFolderExample}`);
|
|
||||||
} else {
|
|
||||||
seasonFolderFormatErrors.push('Invalid Format');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,52 +178,23 @@ class Naming extends Component {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
renameEpisodes &&
|
renameTracks &&
|
||||||
<div>
|
<div>
|
||||||
<FormGroup size={sizes.LARGE}>
|
<FormGroup size={sizes.LARGE}>
|
||||||
<FormLabel>Standard Episode Format</FormLabel>
|
<FormLabel>Standard Track Format</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
inputClassName={styles.namingInput}
|
inputClassName={styles.namingInput}
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.TEXT}
|
||||||
name="standardEpisodeFormat"
|
name="standardTrackFormat"
|
||||||
buttons={<FormInputButton onPress={this.onStandardNamingModalOpenClick}>?</FormInputButton>}
|
buttons={<FormInputButton onPress={this.onStandardNamingModalOpenClick}>?</FormInputButton>}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.standardEpisodeFormat}
|
{...settings.standardTrackFormat}
|
||||||
helpTexts={standardEpisodeFormatHelpTexts}
|
helpTexts={standardTrackFormatHelpTexts}
|
||||||
errors={[...standardEpisodeFormatErrors, ...settings.standardEpisodeFormat.errors]}
|
errors={[...standardTrackFormatErrors, ...settings.standardTrackFormat.errors]}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.LARGE}>
|
|
||||||
<FormLabel>Daily Episode Format</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
inputClassName={styles.namingInput}
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="dailyEpisodeFormat"
|
|
||||||
buttons={<FormInputButton onPress={this.onDailyNamingModalOpenClick}>?</FormInputButton>}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.dailyEpisodeFormat}
|
|
||||||
helpTexts={dailyEpisodeFormatHelpTexts}
|
|
||||||
errors={[...dailyEpisodeFormatErrors, ...settings.dailyEpisodeFormat.errors]}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup size={sizes.LARGE}>
|
|
||||||
<FormLabel>Anime Episode Format</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
inputClassName={styles.namingInput}
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="animeEpisodeFormat"
|
|
||||||
buttons={<FormInputButton onPress={this.onAnimeNamingModalOpenClick}>?</FormInputButton>}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.animeEpisodeFormat}
|
|
||||||
helpTexts={animeEpisodeFormatHelpTexts}
|
|
||||||
errors={[...animeEpisodeFormatErrors, ...settings.animeEpisodeFormat.errors]}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,45 +202,32 @@ class Naming extends Component {
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
>
|
>
|
||||||
<FormLabel>Series Folder Format</FormLabel>
|
<FormLabel>Artist Folder Format</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
inputClassName={styles.namingInput}
|
inputClassName={styles.namingInput}
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.TEXT}
|
||||||
name="seriesFolderFormat"
|
name="artistFolderFormat"
|
||||||
buttons={<FormInputButton onPress={this.onSeriesFolderNamingModalOpenClick}>?</FormInputButton>}
|
buttons={<FormInputButton onPress={this.onArtistFolderNamingModalOpenClick}>?</FormInputButton>}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.seriesFolderFormat}
|
{...settings.artistFolderFormat}
|
||||||
helpTexts={['Only used when adding a new series', ...seriesFolderFormatHelpTexts]}
|
helpTexts={['Only used when adding a new artist', ...artistFolderFormatHelpTexts]}
|
||||||
errors={[...seriesFolderFormatErrors, ...settings.seriesFolderFormat.errors]}
|
errors={[...artistFolderFormatErrors, ...settings.artistFolderFormat.errors]}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Season Folder Format</FormLabel>
|
<FormLabel>Album Folder Format</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
inputClassName={styles.namingInput}
|
inputClassName={styles.namingInput}
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.TEXT}
|
||||||
name="seasonFolderFormat"
|
name="albumFolderFormat"
|
||||||
buttons={<FormInputButton onPress={this.onSeasonFolderNamingModalOpenClick}>?</FormInputButton>}
|
buttons={<FormInputButton onPress={this.onAlbumFolderNamingModalOpenClick}>?</FormInputButton>}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.seasonFolderFormat}
|
{...settings.albumFolderFormat}
|
||||||
helpTexts={seasonFolderFormatHelpTexts}
|
helpTexts={albumFolderFormatHelpTexts}
|
||||||
errors={[...seasonFolderFormatErrors, ...settings.seasonFolderFormat.errors]}
|
errors={[...albumFolderFormatErrors, ...settings.albumFolderFormat.errors]}
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>Multi-Episode Style</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="multiEpisodeStyle"
|
|
||||||
helpText="Change file date on import/rescan"
|
|
||||||
values={multiEpisodeStyleOptions}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.multiEpisodeStyle}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
|
|
@ -42,10 +42,8 @@ class NamingModal extends Component {
|
||||||
value,
|
value,
|
||||||
isOpen,
|
isOpen,
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
season,
|
album,
|
||||||
episode,
|
track,
|
||||||
daily,
|
|
||||||
anime,
|
|
||||||
additional,
|
additional,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
onModalClose
|
onModalClose
|
||||||
|
@ -59,61 +57,55 @@ class NamingModal extends Component {
|
||||||
|
|
||||||
const fileNameTokens = [
|
const fileNameTokens = [
|
||||||
{
|
{
|
||||||
token: '{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}',
|
token: '{Artist Name} - {Album Title} - {track:00} - {Track Title} {Quality Full}',
|
||||||
example: 'Series Title (2010) - S01E01 - Episode Title HDTV-720p Proper'
|
example: 'Artist Name - Album Title - 01 - Track Title MP3-320 Proper'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: '{Series Title} - {season:0}x{episode:00} - {Episode Title} {Quality Full}',
|
token: '{Artist.Name}.{Album.Title}.{track:00}.{TrackClean.Title}.{Quality.Full}',
|
||||||
example: 'Series Title (2010) - 1x01 - Episode Title HDTV-720p Proper'
|
example: 'Artist.Name.Album.Title.01.Track.Title.MP3-320'
|
||||||
},
|
|
||||||
{
|
|
||||||
token: '{Series.Title}.S{season:00}E{episode:00}.{EpisodeClean.Title}.{Quality.Full}',
|
|
||||||
example: 'Series.Title.(2010).S01E01.Episode.Title.HDTV-720p'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const seriesTokens = [
|
const artistTokens = [
|
||||||
{ token: '{Series Title}', example: 'Series Title (2010)' },
|
{ token: '{Artist Name}', example: 'Artist Name' },
|
||||||
{ token: '{Series.Title}', example: 'Series.Title.(2010)' },
|
{ token: '{Artist.Name}', example: 'Artist.Name' },
|
||||||
{ token: '{Series_Title}', example: 'Series_Title_(2010)' },
|
{ token: '{Artist_Name}', example: 'Artist_Name' },
|
||||||
|
|
||||||
{ token: '{Series TitleThe}', example: 'Series Title, The (2010)' },
|
{ token: '{Artist NameThe}', example: 'Artist Name, The' },
|
||||||
|
|
||||||
{ token: '{Series CleanTitle}', example: 'Series Title 2010' },
|
{ token: '{Artist CleanName}', example: 'Artist Name' },
|
||||||
{ token: '{Series.CleanTitle}', example: 'Series.Title.2010' },
|
{ token: '{Artist.CleanName}', example: 'Artist.Name' },
|
||||||
{ token: '{Series_CleanTitle}', example: 'Series_Title_2010' }
|
{ token: '{Artist_CleanName}', example: 'Artist_Name' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const seasonTokens = [
|
const albumTokens = [
|
||||||
{ token: '{season:0}', example: '1' },
|
{ token: '{Album Title}', example: 'Album Title' },
|
||||||
{ token: '{season:00}', example: '01' }
|
{ token: '{Album.Title}', example: 'Album.Title' },
|
||||||
|
{ token: '{Album_Name}', example: 'Album_Name' },
|
||||||
|
|
||||||
|
{ token: '{Album TitleThe}', example: 'Album Title, The' },
|
||||||
|
|
||||||
|
{ token: '{Album CleanTitle}', example: 'Album Title' },
|
||||||
|
{ token: '{Album.CleanTitle}', example: 'Album.Title' },
|
||||||
|
{ token: '{Album_CleanTitle}', example: 'Album_Title' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const episodeTokens = [
|
const trackTokens = [
|
||||||
{ token: '{episode:0}', example: '1' },
|
{ token: '{track:0}', example: '1' },
|
||||||
{ token: '{episode:00}', example: '01' }
|
{ token: '{track:00}', example: '01' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const airDateTokens = [
|
const releaseDateTokens = [
|
||||||
{ token: '{Air-Date}', example: '2016-03-20' },
|
{ token: '{Release Year}', example: '2016' }
|
||||||
{ token: '{Air Date}', example: '2016 03 20' },
|
|
||||||
{ token: '{Air.Date}', example: '2016.03.20' },
|
|
||||||
{ token: '{Air_Date}', example: '2016_03_20' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const absoluteTokens = [
|
const trackTitleTokens = [
|
||||||
{ token: '{absolute:0}', example: '1' },
|
{ token: '{Track Title}', example: 'Track Title' },
|
||||||
{ token: '{absolute:00}', example: '01' },
|
{ token: '{Track.Title}', example: 'Track.Title' },
|
||||||
{ token: '{absolute:000}', example: '001' }
|
{ token: '{Track_Title}', example: 'Track_Title' },
|
||||||
];
|
{ token: '{Track CleanTitle}', example: 'Track Title' },
|
||||||
|
{ token: '{Track.CleanTitle}', example: 'Track.Title' },
|
||||||
const episodeTitleTokens = [
|
{ token: '{Track_CleanTitle}', example: 'Track_Title' }
|
||||||
{ token: '{Episode Title}', example: 'Episode Title' },
|
|
||||||
{ token: '{Episode.Title}', example: 'Episode.Title' },
|
|
||||||
{ token: '{Episode_Title}', example: 'Episode_Title' },
|
|
||||||
{ token: '{Episode CleanTitle}', example: 'Episode Title' },
|
|
||||||
{ token: '{Episode.CleanTitle}', example: 'Episode.Title' },
|
|
||||||
{ token: '{Episode_CleanTitle}', example: 'Episode_Title' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const qualityTokens = [
|
const qualityTokens = [
|
||||||
|
@ -146,8 +138,8 @@ class NamingModal extends Component {
|
||||||
];
|
];
|
||||||
|
|
||||||
const originalTokens = [
|
const originalTokens = [
|
||||||
{ token: '{Original Title}', example: 'Series.Title.S01E01.HDTV.x264-EVOLVE' },
|
{ token: '{Original Title}', example: 'Artist.Name.S01E01.HDTV.x264-EVOLVE' },
|
||||||
{ token: '{Original Filename}', example: 'series.title.s01e01.hdtv.x264-EVOLVE' }
|
{ token: '{Original Filename}', example: 'artist.name.s01e01.hdtv.x264-EVOLVE' }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -197,10 +189,10 @@ class NamingModal extends Component {
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
}
|
}
|
||||||
|
|
||||||
<FieldSet legend="Series">
|
<FieldSet legend="Artist">
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
seriesTokens.map(({ token, example }) => {
|
artistTokens.map(({ token, example }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
|
@ -219,11 +211,12 @@ class NamingModal extends Component {
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
{
|
{
|
||||||
season &&
|
album &&
|
||||||
<FieldSet legend="Season">
|
<div>
|
||||||
<div className={styles.groups}>
|
<FieldSet legend="Album">
|
||||||
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
seasonTokens.map(({ token, example }) => {
|
albumTokens.map(({ token, example }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
|
@ -238,17 +231,39 @@ class NamingModal extends Component {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend="Release Date">
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{
|
||||||
|
releaseDateTokens.map(({ token, example }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
tokenCase={this.state.case}
|
||||||
|
onInputChange={onInputChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
episode &&
|
track &&
|
||||||
<div>
|
<div>
|
||||||
<FieldSet legend="Episode">
|
<FieldSet legend="Track">
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
episodeTokens.map(({ token, example }) => {
|
trackTokens.map(({ token, example }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
|
@ -266,63 +281,16 @@ class NamingModal extends Component {
|
||||||
</div>
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
{
|
|
||||||
daily &&
|
|
||||||
<FieldSet legend="Air-Date">
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
airDateTokens.map(({ token, example }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
tokenCase={this.state.case}
|
|
||||||
onInputChange={onInputChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
anime &&
|
|
||||||
<FieldSet legend="Absolute Episode Number">
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
absoluteTokens.map(({ token, example }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
tokenCase={this.state.case}
|
|
||||||
onInputChange={onInputChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
additional &&
|
additional &&
|
||||||
<div>
|
<div>
|
||||||
<FieldSet legend="Episode Title">
|
<FieldSet legend="Track Title">
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
episodeTitleTokens.map(({ token, example }) => {
|
trackTitleTokens.map(({ token, example }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
|
@ -449,20 +417,16 @@ NamingModal.propTypes = {
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
advancedSettings: PropTypes.bool.isRequired,
|
||||||
season: PropTypes.bool.isRequired,
|
album: PropTypes.bool.isRequired,
|
||||||
episode: PropTypes.bool.isRequired,
|
track: PropTypes.bool.isRequired,
|
||||||
daily: PropTypes.bool.isRequired,
|
|
||||||
anime: PropTypes.bool.isRequired,
|
|
||||||
additional: PropTypes.bool.isRequired,
|
additional: PropTypes.bool.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired,
|
onInputChange: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
NamingModal.defaultProps = {
|
NamingModal.defaultProps = {
|
||||||
season: false,
|
album: false,
|
||||||
episode: false,
|
track: false,
|
||||||
daily: false,
|
|
||||||
anime: false,
|
|
||||||
additional: false
|
additional: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ class QualityDefinitions extends Component {
|
||||||
|
|
||||||
<div className={styles.sizeLimitHelpTextContainer}>
|
<div className={styles.sizeLimitHelpTextContainer}>
|
||||||
<div className={styles.sizeLimitHelpText}>
|
<div className={styles.sizeLimitHelpText}>
|
||||||
Limits are automatically adjusted for the series runtime and number of episodes in the file.
|
Limits are automatically adjusted for the album duration.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageSectionContent>
|
</PageSectionContent>
|
||||||
|
|
|
@ -36,9 +36,9 @@ namespace Lidarr.Api.V3.Config
|
||||||
Get["/examples"] = x => GetExamples(this.Bind<NamingConfigResource>());
|
Get["/examples"] = x => GetExamples(this.Bind<NamingConfigResource>());
|
||||||
|
|
||||||
|
|
||||||
SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidEpisodeFormat();
|
SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidTrackFormat();
|
||||||
SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidSeriesFolderFormat();
|
SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat();
|
||||||
SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidSeasonFolderFormat();
|
SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidAlbumFolderFormat();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateNamingConfig(NamingConfigResource resource)
|
private void UpdateNamingConfig(NamingConfigResource resource)
|
||||||
|
@ -99,7 +99,7 @@ namespace Lidarr.Api.V3.Config
|
||||||
{
|
{
|
||||||
var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec);
|
var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec);
|
||||||
|
|
||||||
var singleTrackValidationResult = _filenameValidationService.ValidateStandardFilename(singleTrackSampleResult);
|
var singleTrackValidationResult = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult);
|
||||||
|
|
||||||
var validationFailures = new List<ValidationFailure>();
|
var validationFailures = new List<ValidationFailure>();
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,6 @@ namespace Lidarr.Api.V3.Config
|
||||||
public class NamingExampleResource
|
public class NamingExampleResource
|
||||||
{
|
{
|
||||||
public string SingleTrackExample { get; set; }
|
public string SingleTrackExample { get; set; }
|
||||||
public string MultiEpisodeExample { get; set; }
|
|
||||||
public string DailyEpisodeExample { get; set; }
|
|
||||||
public string AnimeEpisodeExample { get; set; }
|
|
||||||
public string AnimeMultiEpisodeExample { get; set; }
|
|
||||||
public string ArtistFolderExample { get; set; }
|
public string ArtistFolderExample { get; set; }
|
||||||
public string AlbumFolderExample { get; set; }
|
public string AlbumFolderExample { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -301,6 +301,7 @@
|
||||||
<Compile Include="NotificationTests\NotificationBaseFixture.cs" />
|
<Compile Include="NotificationTests\NotificationBaseFixture.cs" />
|
||||||
<Compile Include="NotificationTests\SynologyIndexerFixture.cs" />
|
<Compile Include="NotificationTests\SynologyIndexerFixture.cs" />
|
||||||
<Compile Include="OrganizerTests\FileNameBuilderTests\CleanTitleFixture.cs" />
|
<Compile Include="OrganizerTests\FileNameBuilderTests\CleanTitleFixture.cs" />
|
||||||
|
<Compile Include="OrganizerTests\FileNameBuilderTests\TitleTheFixture.cs" />
|
||||||
<Compile Include="ParserTests\MiniSeriesEpisodeParserFixture.cs" />
|
<Compile Include="ParserTests\MiniSeriesEpisodeParserFixture.cs" />
|
||||||
<Compile Include="ParserTests\MusicParserFixture.cs" />
|
<Compile Include="ParserTests\MusicParserFixture.cs" />
|
||||||
<Compile Include="Qualities\RevisionComparableFixture.cs" />
|
<Compile Include="Qualities\RevisionComparableFixture.cs" />
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
using NzbDrone.Core.Organizer;
|
||||||
|
using NzbDrone.Core.Qualities;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.Music;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TitleTheFixture : CoreTest<FileNameBuilder>
|
||||||
|
{
|
||||||
|
private Artist _artist;
|
||||||
|
private Album _album;
|
||||||
|
private Track _track;
|
||||||
|
private TrackFile _trackFile;
|
||||||
|
private NamingConfig _namingConfig;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_artist = Builder<Artist>
|
||||||
|
.CreateNew()
|
||||||
|
.With(s => s.Name = "Alien Ant Farm")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_album = Builder<Album>
|
||||||
|
.CreateNew()
|
||||||
|
.With(s => s.Title = "Anthology")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_track = Builder<Track>.CreateNew()
|
||||||
|
.With(e => e.Title = "City Sushi")
|
||||||
|
.With(e => e.TrackNumber = 6)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_trackFile = new TrackFile { Quality = new QualityModel(Quality.MP3_320), ReleaseGroup = "LidarrTest" };
|
||||||
|
|
||||||
|
_namingConfig = NamingConfig.Default;
|
||||||
|
_namingConfig.RenameTracks = true;
|
||||||
|
|
||||||
|
Mocker.GetMock<INamingConfigService>()
|
||||||
|
.Setup(c => c.GetConfig()).Returns(_namingConfig);
|
||||||
|
|
||||||
|
Mocker.GetMock<IQualityDefinitionService>()
|
||||||
|
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||||
|
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("The Mist", "Mist, The")]
|
||||||
|
[TestCase("A Place to Call Home", "Place to Call Home, A")]
|
||||||
|
[TestCase("An Adventure in Space and Time", "Adventure in Space and Time, An")]
|
||||||
|
[TestCase("The Flash (2010)", "Flash, The (2010)")]
|
||||||
|
[TestCase("A League Of Their Own (AU)", "League Of Their Own, A (AU)")]
|
||||||
|
[TestCase("The Fixer (ZH) (2015)", "Fixer, The (ZH) (2015)")]
|
||||||
|
[TestCase("The Sixth Sense 2 (Thai)", "Sixth Sense 2, The (Thai)")]
|
||||||
|
[TestCase("The Amazing Race (Latin America)", "Amazing Race, The (Latin America)")]
|
||||||
|
[TestCase("The Rat Pack (A&E)", "Rat Pack, The (A&E)")]
|
||||||
|
[TestCase("The Climax: I (Almost) Got Away With It (2016)", "Climax- I (Almost) Got Away With It, The (2016)")]
|
||||||
|
//[TestCase("", "")]
|
||||||
|
public void should_get_expected_title_back(string name, string expected)
|
||||||
|
{
|
||||||
|
_artist.Name = name;
|
||||||
|
_namingConfig.StandardTrackFormat = "{Artist NameThe}";
|
||||||
|
|
||||||
|
Subject.BuildTrackFileName(new List<Track> { _track }, _artist, _album, _trackFile)
|
||||||
|
.Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("A")]
|
||||||
|
[TestCase("Anne")]
|
||||||
|
[TestCase("Theodore")]
|
||||||
|
[TestCase("3%")]
|
||||||
|
public void should_not_change_title(string name)
|
||||||
|
{
|
||||||
|
_artist.Name = name;
|
||||||
|
_namingConfig.StandardTrackFormat = "{Artist NameThe}";
|
||||||
|
|
||||||
|
Subject.BuildTrackFileName(new List<Track> { _track }, _artist, _album, _trackFile)
|
||||||
|
.Should().Be(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,10 +60,10 @@ namespace NzbDrone.Core.Organizer
|
||||||
public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>[- ._])(Clean)?Title\})",
|
public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>[- ._])(Clean)?Title\})",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
public static readonly Regex ArtistNameRegex = new Regex(@"(?<token>\{(?:Artist)(?<separator>[- ._])(Clean)?Name\})",
|
public static readonly Regex ArtistNameRegex = new Regex(@"(?<token>\{(?:Artist)(?<separator>[- ._])(Clean)?Name(The)\})",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
public static readonly Regex AlbumTitleRegex = new Regex(@"(?<token>\{(?:Album)(?<separator>[- ._])(Clean)?Title\})",
|
public static readonly Regex AlbumTitleRegex = new Regex(@"(?<token>\{(?:Album)(?<separator>[- ._])(Clean)?Title(The)\})",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled);
|
private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled);
|
||||||
|
@ -77,6 +77,8 @@ namespace NzbDrone.Core.Organizer
|
||||||
|
|
||||||
private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' };
|
private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' };
|
||||||
|
|
||||||
|
private static readonly Regex TitlePrefixRegex = new Regex(@"^(The|An|A) (.*?)((?: *\([^)]+\))*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
public FileNameBuilder(INamingConfigService namingConfigService,
|
public FileNameBuilder(INamingConfigService namingConfigService,
|
||||||
IQualityDefinitionService qualityDefinitionService,
|
IQualityDefinitionService qualityDefinitionService,
|
||||||
ICacheManager cacheManager,
|
ICacheManager cacheManager,
|
||||||
|
@ -110,10 +112,10 @@ namespace NzbDrone.Core.Organizer
|
||||||
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
|
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
|
||||||
|
|
||||||
tracks = tracks.OrderBy(e => e.AlbumId).ThenBy(e => e.TrackNumber).ToList();
|
tracks = tracks.OrderBy(e => e.AlbumId).ThenBy(e => e.TrackNumber).ToList();
|
||||||
|
|
||||||
pattern = FormatTrackNumberTokens(pattern, "", tracks);
|
pattern = FormatTrackNumberTokens(pattern, "", tracks);
|
||||||
//pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig);
|
//pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig);
|
||||||
|
|
||||||
AddArtistTokens(tokenHandlers, artist);
|
AddArtistTokens(tokenHandlers, artist);
|
||||||
AddAlbumTokens(tokenHandlers, album);
|
AddAlbumTokens(tokenHandlers, album);
|
||||||
AddTrackTokens(tokenHandlers, tracks);
|
AddTrackTokens(tokenHandlers, tracks);
|
||||||
|
@ -143,13 +145,13 @@ namespace NzbDrone.Core.Organizer
|
||||||
|
|
||||||
if (artist.AlbumFolder)
|
if (artist.AlbumFolder)
|
||||||
{
|
{
|
||||||
|
|
||||||
var albumFolder = GetAlbumFolder(artist, album);
|
var albumFolder = GetAlbumFolder(artist, album);
|
||||||
|
|
||||||
albumFolder = CleanFileName(albumFolder);
|
albumFolder = CleanFileName(albumFolder);
|
||||||
|
|
||||||
path = Path.Combine(path, albumFolder);
|
path = Path.Combine(path, albumFolder);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
|
@ -165,9 +167,9 @@ namespace NzbDrone.Core.Organizer
|
||||||
}
|
}
|
||||||
|
|
||||||
var basicNamingConfig = new BasicNamingConfig
|
var basicNamingConfig = new BasicNamingConfig
|
||||||
{
|
{
|
||||||
Separator = trackFormat.Separator
|
Separator = trackFormat.Separator
|
||||||
};
|
};
|
||||||
|
|
||||||
var titleTokens = TitleRegex.Matches(nameSpec.StandardTrackFormat);
|
var titleTokens = TitleRegex.Matches(nameSpec.StandardTrackFormat);
|
||||||
|
|
||||||
|
@ -238,6 +240,11 @@ namespace NzbDrone.Core.Organizer
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string TitleThe(string title)
|
||||||
|
{
|
||||||
|
return TitlePrefixRegex.Replace(title, "$2, $1$3");
|
||||||
|
}
|
||||||
|
|
||||||
public static string CleanFileName(string name, bool replace = true)
|
public static string CleanFileName(string name, bool replace = true)
|
||||||
{
|
{
|
||||||
string result = name;
|
string result = name;
|
||||||
|
@ -262,12 +269,14 @@ namespace NzbDrone.Core.Organizer
|
||||||
{
|
{
|
||||||
tokenHandlers["{Artist Name}"] = m => artist.Name;
|
tokenHandlers["{Artist Name}"] = m => artist.Name;
|
||||||
tokenHandlers["{Artist CleanName}"] = m => CleanTitle(artist.Name);
|
tokenHandlers["{Artist CleanName}"] = m => CleanTitle(artist.Name);
|
||||||
|
tokenHandlers["{Artist NameThe}"] = m => TitleThe(artist.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddAlbumTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Album album)
|
private void AddAlbumTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Album album)
|
||||||
{
|
{
|
||||||
tokenHandlers["{Album Title}"] = m => album.Title;
|
tokenHandlers["{Album Title}"] = m => album.Title;
|
||||||
tokenHandlers["{Album CleanTitle}"] = m => CleanTitle(album.Title);
|
tokenHandlers["{Album CleanTitle}"] = m => CleanTitle(album.Title);
|
||||||
|
tokenHandlers["{Album TitleThe}"] = m => TitleThe(album.Title);
|
||||||
if (album.ReleaseDate.HasValue)
|
if (album.ReleaseDate.HasValue)
|
||||||
{
|
{
|
||||||
tokenHandlers["{Release Year}"] = m => album.ReleaseDate.Value.Year.ToString();
|
tokenHandlers["{Release Year}"] = m => album.ReleaseDate.Value.Year.ToString();
|
||||||
|
@ -321,7 +330,7 @@ namespace NzbDrone.Core.Organizer
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var audioCodec = MediaInfoFormatter.FormatAudioCodec(trackFile.MediaInfo);
|
var audioCodec = MediaInfoFormatter.FormatAudioCodec(trackFile.MediaInfo);
|
||||||
var audioChannels = MediaInfoFormatter.FormatAudioChannels(trackFile.MediaInfo);
|
var audioChannels = MediaInfoFormatter.FormatAudioChannels(trackFile.MediaInfo);
|
||||||
|
|
||||||
|
@ -468,7 +477,7 @@ namespace NzbDrone.Core.Organizer
|
||||||
|
|
||||||
private AbsoluteTrackFormat[] GetAbsoluteFormat(string pattern)
|
private AbsoluteTrackFormat[] GetAbsoluteFormat(string pattern)
|
||||||
{
|
{
|
||||||
return _absoluteTrackFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType<Match>()
|
return _absoluteTrackFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType<Match>()
|
||||||
.Select(match => new AbsoluteTrackFormat
|
.Select(match => new AbsoluteTrackFormat
|
||||||
{
|
{
|
||||||
Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-",
|
Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.Music;
|
using NzbDrone.Core.Music;
|
||||||
|
@ -30,12 +30,12 @@ namespace NzbDrone.Core.Organizer
|
||||||
|
|
||||||
_standardArtist = new Artist
|
_standardArtist = new Artist
|
||||||
{
|
{
|
||||||
Name = "Artist Name"
|
Name = "The Artist Name"
|
||||||
};
|
};
|
||||||
|
|
||||||
_standardAlbum = new Album
|
_standardAlbum = new Album
|
||||||
{
|
{
|
||||||
Title = "Album Title",
|
Title = "The Album Title",
|
||||||
ReleaseDate = System.DateTime.Today
|
ReleaseDate = System.DateTime.Today
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,41 +1,19 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Tv;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Organizer
|
namespace NzbDrone.Core.Organizer
|
||||||
{
|
{
|
||||||
public interface IFilenameValidationService
|
public interface IFilenameValidationService
|
||||||
{
|
{
|
||||||
ValidationFailure ValidateStandardFilename(SampleResult sampleResult);
|
|
||||||
ValidationFailure ValidateTrackFilename(SampleResult sampleResult);
|
ValidationFailure ValidateTrackFilename(SampleResult sampleResult);
|
||||||
ValidationFailure ValidateDailyFilename(SampleResult sampleResult);
|
|
||||||
ValidationFailure ValidateAnimeFilename(SampleResult sampleResult);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FileNameValidationService : IFilenameValidationService
|
public class FileNameValidationService : IFilenameValidationService
|
||||||
{
|
{
|
||||||
private const string ERROR_MESSAGE = "Produces invalid file names";
|
private const string ERROR_MESSAGE = "Produces invalid file names";
|
||||||
|
|
||||||
public ValidationFailure ValidateStandardFilename(SampleResult sampleResult)
|
|
||||||
{
|
|
||||||
var validationFailure = new ValidationFailure("StandardEpisodeFormat", ERROR_MESSAGE);
|
|
||||||
var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName);
|
|
||||||
|
|
||||||
if (parsedEpisodeInfo == null)
|
|
||||||
{
|
|
||||||
return validationFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo))
|
|
||||||
{
|
|
||||||
return validationFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValidationFailure ValidateTrackFilename(SampleResult sampleResult)
|
public ValidationFailure ValidateTrackFilename(SampleResult sampleResult)
|
||||||
{
|
{
|
||||||
var validationFailure = new ValidationFailure("StandardTrackFormat", ERROR_MESSAGE);
|
var validationFailure = new ValidationFailure("StandardTrackFormat", ERROR_MESSAGE);
|
||||||
|
@ -57,71 +35,5 @@ namespace NzbDrone.Core.Organizer
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidationFailure ValidateDailyFilename(SampleResult sampleResult)
|
|
||||||
{
|
|
||||||
var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE);
|
|
||||||
var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName);
|
|
||||||
|
|
||||||
if (parsedEpisodeInfo == null)
|
|
||||||
{
|
|
||||||
return validationFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedEpisodeInfo.IsDaily)
|
|
||||||
{
|
|
||||||
if (!parsedEpisodeInfo.AirDate.Equals(sampleResult.Episodes.Single().AirDate))
|
|
||||||
{
|
|
||||||
return validationFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo))
|
|
||||||
{
|
|
||||||
return validationFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValidationFailure ValidateAnimeFilename(SampleResult sampleResult)
|
|
||||||
{
|
|
||||||
var validationFailure = new ValidationFailure("AnimeEpisodeFormat", ERROR_MESSAGE);
|
|
||||||
var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName);
|
|
||||||
|
|
||||||
if (parsedEpisodeInfo == null)
|
|
||||||
{
|
|
||||||
return validationFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedEpisodeInfo.AbsoluteEpisodeNumbers.Any())
|
|
||||||
{
|
|
||||||
if (!parsedEpisodeInfo.AbsoluteEpisodeNumbers.First().Equals(sampleResult.Episodes.First().AbsoluteEpisodeNumber))
|
|
||||||
{
|
|
||||||
return validationFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo))
|
|
||||||
{
|
|
||||||
return validationFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateSeasonAndEpisodeNumbers(List<Episode> episodes, ParsedEpisodeInfo parsedEpisodeInfo)
|
|
||||||
{
|
|
||||||
if (parsedEpisodeInfo.SeasonNumber != episodes.First().SeasonNumber ||
|
|
||||||
!parsedEpisodeInfo.EpisodeNumbers.OrderBy(e => e).SequenceEqual(episodes.Select(e => e.EpisodeNumber).OrderBy(e => e)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue