[UI Work] Settings Naming Page, Other Settings

pull/6/head
Qstick 7 years ago
parent 22d9c5e666
commit 456ead09da

@ -74,7 +74,7 @@ class OrganizePreviewModalContent extends Component {
isPopulated,
error,
items,
renameEpisodes,
renameTracks,
episodeFormat,
path,
onModalClose
@ -109,7 +109,7 @@ class OrganizePreviewModalContent extends Component {
!isFetching && isPopulated && !items.length &&
<div>
{
renameEpisodes ?
renameTracks ?
<div>Success! My work is done, no files to rename.</div> :
<div>Renaming is disabled, nothing to rename</div>
}
@ -191,7 +191,7 @@ OrganizePreviewModalContent.propTypes = {
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
path: PropTypes.string.isRequired,
renameEpisodes: PropTypes.bool,
renameTracks: PropTypes.bool,
episodeFormat: PropTypes.string,
onOrganizePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired

@ -19,7 +19,7 @@ function createMapStateToProps() {
props.isFetching = organizePreview.isFetching || naming.isFetching;
props.isPopulated = organizePreview.isPopulated && naming.isPopulated;
props.error = organizePreview.error || naming.error;
props.renameEpisodes = naming.item.renameEpisodes;
props.renameTracks = naming.item.renameTracks;
props.episodeFormat = naming.item[`${series.seriesType}EpisodeFormat`];
props.path = series.path;

@ -519,7 +519,7 @@ class GeneralSettings extends Component {
<FormInputGroup
type={inputTypes.CHECK}
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"
onChange={onInputChange}
{...analyticsEnabled}
@ -541,7 +541,7 @@ class GeneralSettings extends Component {
<FormInputGroup
type={inputTypes.TEXT}
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"
onChange={onInputChange}
{...branch}
@ -622,8 +622,8 @@ class GeneralSettings extends Component {
<ConfirmModal
isOpen={isRestartRequiredModalOpen}
kind={kinds.DANGER}
title="Restart Sonarr"
message="Sonarr requires a restart to apply changes, do you want to restart now?"
title="Restart Lidarr"
message="Lidarr requires a restart to apply changes, do you want to restart now?"
cancelLabel="I'll restart later"
confirmLabel="Restart Now"
onConfirm={this.onConfirmRestart}

@ -73,14 +73,14 @@ class MediaManagement extends Component {
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>Create empty series folders</FormLabel>
<FormLabel>Create empty artist folders</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="createEmptySeriesFolders"
name="createEmptyArtistFolders"
helpText="Create missing series folders during disk scan"
onChange={onInputChange}
{...settings.createEmptySeriesFolders}
{...settings.createEmptyArtistFolders}
/>
</FormGroup>
</FieldSet>
@ -103,7 +103,7 @@ class MediaManagement extends Component {
<FormInputGroup
type={inputTypes.CHECK}
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}
{...settings.skipFreeSpaceCheckWhenImporting}
/>
@ -133,7 +133,7 @@ class MediaManagement extends Component {
<FormInputGroup
type={inputTypes.CHECK}
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}
{...settings.importExtraFiles}
/>
@ -163,14 +163,14 @@ class MediaManagement extends Component {
legend="File Management"
>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>Ignore Deleted Episodes</FormLabel>
<FormLabel>Ignore Deleted Tracks</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="autoUnmonitorPreviouslyDownloadedEpisodes"
helpText="Episodes deleted from disk are automatically unmonitored in Lidarr"
name="autoUnmonitorPreviouslyDownloadedTracks"
helpText="Tracks deleted from disk are automatically unmonitored in Lidarr"
onChange={onInputChange}
{...settings.autoUnmonitorPreviouslyDownloadedEpisodes}
{...settings.autoUnmonitorPreviouslyDownloadedTracks}
/>
</FormGroup>
@ -195,12 +195,12 @@ class MediaManagement extends Component {
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>Analyse video files</FormLabel>
<FormLabel>Analyse audio files</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
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}
{...settings.enableMediaInfo}
/>
@ -231,7 +231,7 @@ class MediaManagement extends Component {
<FormInputGroup
type={inputTypes.PATH}
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}
{...settings.recycleBin}
/>

@ -32,55 +32,29 @@ class Naming extends Component {
this.setState({
isNamingModalOpen: true,
namingModalOptions: {
name: 'standardEpisodeFormat',
season: true,
episode: true,
name: 'standardTrackFormat',
album: true,
track: true,
additional: true
}
});
}
onDailyNamingModalOpenClick = () => {
onArtistFolderNamingModalOpenClick = () => {
this.setState({
isNamingModalOpen: true,
namingModalOptions: {
name: 'dailyEpisodeFormat',
season: true,
episode: true,
daily: true,
additional: true
}
});
}
onAnimeNamingModalOpenClick = () => {
this.setState({
isNamingModalOpen: true,
namingModalOptions: {
name: 'animeEpisodeFormat',
season: true,
episode: true,
anime: true,
additional: true
name: 'artistFolderFormat'
}
});
}
onSeriesFolderNamingModalOpenClick = () => {
onAlbumFolderNamingModalOpenClick = () => {
this.setState({
isNamingModalOpen: true,
namingModalOptions: {
name: 'seriesFolderFormat'
}
});
}
onSeasonFolderNamingModalOpenClick = () => {
this.setState({
isNamingModalOpen: true,
namingModalOptions: {
name: 'seasonFolderFormat',
season: true
name: 'albumFolderFormat',
album: true
}
});
}
@ -109,69 +83,56 @@ class Naming extends Component {
namingModalOptions
} = this.state;
const renameEpisodes = hasSettings && settings.renameEpisodes.value;
const multiEpisodeStyleOptions = [
{ key: 0, value: 'Extend' },
{ key: 1, value: 'Duplicate' },
{ key: 2, value: 'Repeat' },
{ key: 3, value: 'Scene' },
{ key: 4, value: 'Range' },
{ 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 = [];
const renameTracks = hasSettings && settings.renameTracks.value;
if (examplesPopulated) {
if (examples.singleEpisodeExample) {
standardEpisodeFormatHelpTexts.push(`Single Episode: ${examples.singleEpisodeExample}`);
} else {
standardEpisodeFormatErrors.push('Single Episode: Invalid Format');
}
const standardTrackFormatHelpTexts = [];
const standardTrackFormatErrors = [];
const artistFolderFormatHelpTexts = [];
const artistFolderFormatErrors = [];
const albumFolderFormatHelpTexts = [];
const albumFolderFormatErrors = [];
if (examples.multiEpisodeExample) {
standardEpisodeFormatHelpTexts.push(`Multi Episode: ${examples.multiEpisodeExample}`);
} else {
standardEpisodeFormatErrors.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}`);
if (examplesPopulated) {
if (examples.singleTrackExample) {
standardTrackFormatHelpTexts.push(`Single Track: ${examples.singleTrackExample}`);
} else {
animeEpisodeFormatErrors.push('Multi Episode: Invalid Format');
standardTrackFormatErrors.push('Single Track: Invalid Format');
}
if (examples.seriesFolderExample) {
seriesFolderFormatHelpTexts.push(`Example: ${examples.seriesFolderExample}`);
// if (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 {
seriesFolderFormatErrors.push('Invalid Format');
artistFolderFormatErrors.push('Invalid Format');
}
if (examples.seasonFolderExample) {
seasonFolderFormatHelpTexts.push(`Example: ${examples.seasonFolderExample}`);
if (examples.albumFolderExample) {
albumFolderFormatHelpTexts.push(`Example: ${examples.albumFolderExample}`);
} else {
seasonFolderFormatErrors.push('Invalid Format');
albumFolderFormatErrors.push('Invalid Format');
}
}
@ -217,52 +178,23 @@ class Naming extends Component {
</FormGroup>
{
renameEpisodes &&
renameTracks &&
<div>
<FormGroup size={sizes.LARGE}>
<FormLabel>Standard Episode Format</FormLabel>
<FormLabel>Standard Track Format</FormLabel>
<FormInputGroup
inputClassName={styles.namingInput}
type={inputTypes.TEXT}
name="standardEpisodeFormat"
name="standardTrackFormat"
buttons={<FormInputButton onPress={this.onStandardNamingModalOpenClick}>?</FormInputButton>}
onChange={onInputChange}
{...settings.standardEpisodeFormat}
helpTexts={standardEpisodeFormatHelpTexts}
errors={[...standardEpisodeFormatErrors, ...settings.standardEpisodeFormat.errors]}
{...settings.standardTrackFormat}
helpTexts={standardTrackFormatHelpTexts}
errors={[...standardTrackFormatErrors, ...settings.standardTrackFormat.errors]}
/>
</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>
}
@ -270,45 +202,32 @@ class Naming extends Component {
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>Series Folder Format</FormLabel>
<FormLabel>Artist Folder Format</FormLabel>
<FormInputGroup
inputClassName={styles.namingInput}
type={inputTypes.TEXT}
name="seriesFolderFormat"
buttons={<FormInputButton onPress={this.onSeriesFolderNamingModalOpenClick}>?</FormInputButton>}
name="artistFolderFormat"
buttons={<FormInputButton onPress={this.onArtistFolderNamingModalOpenClick}>?</FormInputButton>}
onChange={onInputChange}
{...settings.seriesFolderFormat}
helpTexts={['Only used when adding a new series', ...seriesFolderFormatHelpTexts]}
errors={[...seriesFolderFormatErrors, ...settings.seriesFolderFormat.errors]}
{...settings.artistFolderFormat}
helpTexts={['Only used when adding a new artist', ...artistFolderFormatHelpTexts]}
errors={[...artistFolderFormatErrors, ...settings.artistFolderFormat.errors]}
/>
</FormGroup>
<FormGroup>
<FormLabel>Season Folder Format</FormLabel>
<FormLabel>Album Folder Format</FormLabel>
<FormInputGroup
inputClassName={styles.namingInput}
type={inputTypes.TEXT}
name="seasonFolderFormat"
buttons={<FormInputButton onPress={this.onSeasonFolderNamingModalOpenClick}>?</FormInputButton>}
onChange={onInputChange}
{...settings.seasonFolderFormat}
helpTexts={seasonFolderFormatHelpTexts}
errors={[...seasonFolderFormatErrors, ...settings.seasonFolderFormat.errors]}
/>
</FormGroup>
<FormGroup>
<FormLabel>Multi-Episode Style</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="multiEpisodeStyle"
helpText="Change file date on import/rescan"
values={multiEpisodeStyleOptions}
name="albumFolderFormat"
buttons={<FormInputButton onPress={this.onAlbumFolderNamingModalOpenClick}>?</FormInputButton>}
onChange={onInputChange}
{...settings.multiEpisodeStyle}
{...settings.albumFolderFormat}
helpTexts={albumFolderFormatHelpTexts}
errors={[...albumFolderFormatErrors, ...settings.albumFolderFormat.errors]}
/>
</FormGroup>

@ -42,10 +42,8 @@ class NamingModal extends Component {
value,
isOpen,
advancedSettings,
season,
episode,
daily,
anime,
album,
track,
additional,
onInputChange,
onModalClose
@ -59,61 +57,55 @@ class NamingModal extends Component {
const fileNameTokens = [
{
token: '{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}',
example: 'Series Title (2010) - S01E01 - Episode Title HDTV-720p Proper'
token: '{Artist Name} - {Album Title} - {track:00} - {Track Title} {Quality Full}',
example: 'Artist Name - Album Title - 01 - Track Title MP3-320 Proper'
},
{
token: '{Series Title} - {season:0}x{episode:00} - {Episode Title} {Quality Full}',
example: 'Series Title (2010) - 1x01 - Episode Title HDTV-720p Proper'
},
{
token: '{Series.Title}.S{season:00}E{episode:00}.{EpisodeClean.Title}.{Quality.Full}',
example: 'Series.Title.(2010).S01E01.Episode.Title.HDTV-720p'
token: '{Artist.Name}.{Album.Title}.{track:00}.{TrackClean.Title}.{Quality.Full}',
example: 'Artist.Name.Album.Title.01.Track.Title.MP3-320'
}
];
const seriesTokens = [
{ token: '{Series Title}', example: 'Series Title (2010)' },
{ token: '{Series.Title}', example: 'Series.Title.(2010)' },
{ token: '{Series_Title}', example: 'Series_Title_(2010)' },
const artistTokens = [
{ token: '{Artist Name}', example: 'Artist Name' },
{ token: '{Artist.Name}', example: 'Artist.Name' },
{ 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: '{Series.CleanTitle}', example: 'Series.Title.2010' },
{ token: '{Series_CleanTitle}', example: 'Series_Title_2010' }
{ token: '{Artist CleanName}', example: 'Artist Name' },
{ token: '{Artist.CleanName}', example: 'Artist.Name' },
{ token: '{Artist_CleanName}', example: 'Artist_Name' }
];
const seasonTokens = [
{ token: '{season:0}', example: '1' },
{ token: '{season:00}', example: '01' }
];
const albumTokens = [
{ token: '{Album Title}', example: 'Album Title' },
{ token: '{Album.Title}', example: 'Album.Title' },
{ token: '{Album_Name}', example: 'Album_Name' },
const episodeTokens = [
{ token: '{episode:0}', example: '1' },
{ token: '{episode:00}', example: '01' }
{ 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 airDateTokens = [
{ token: '{Air-Date}', example: '2016-03-20' },
{ token: '{Air Date}', example: '2016 03 20' },
{ token: '{Air.Date}', example: '2016.03.20' },
{ token: '{Air_Date}', example: '2016_03_20' }
const trackTokens = [
{ token: '{track:0}', example: '1' },
{ token: '{track:00}', example: '01' }
];
const absoluteTokens = [
{ token: '{absolute:0}', example: '1' },
{ token: '{absolute:00}', example: '01' },
{ token: '{absolute:000}', example: '001' }
const releaseDateTokens = [
{ token: '{Release Year}', example: '2016' }
];
const episodeTitleTokens = [
{ 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 trackTitleTokens = [
{ token: '{Track Title}', example: 'Track Title' },
{ token: '{Track.Title}', example: 'Track.Title' },
{ token: '{Track_Title}', example: 'Track_Title' },
{ token: '{Track CleanTitle}', example: 'Track Title' },
{ token: '{Track.CleanTitle}', example: 'Track.Title' },
{ token: '{Track_CleanTitle}', example: 'Track_Title' }
];
const qualityTokens = [
@ -146,8 +138,8 @@ class NamingModal extends Component {
];
const originalTokens = [
{ token: '{Original Title}', example: 'Series.Title.S01E01.HDTV.x264-EVOLVE' },
{ token: '{Original Filename}', example: 'series.title.s01e01.hdtv.x264-EVOLVE' }
{ token: '{Original Title}', example: 'Artist.Name.S01E01.HDTV.x264-EVOLVE' },
{ token: '{Original Filename}', example: 'artist.name.s01e01.hdtv.x264-EVOLVE' }
];
return (
@ -197,10 +189,10 @@ class NamingModal extends Component {
</FieldSet>
}
<FieldSet legend="Series">
<FieldSet legend="Artist">
<div className={styles.groups}>
{
seriesTokens.map(({ token, example }) => {
artistTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
@ -219,11 +211,12 @@ class NamingModal extends Component {
</FieldSet>
{
season &&
<FieldSet legend="Season">
<div className={styles.groups}>
album &&
<div>
<FieldSet legend="Album">
<div className={styles.groups}>
{
seasonTokens.map(({ token, example }) => {
albumTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
@ -238,17 +231,39 @@ class NamingModal extends Component {
}
)
}
</div>
</FieldSet>
</div>
</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>
<FieldSet legend="Episode">
<FieldSet legend="Track">
<div className={styles.groups}>
{
episodeTokens.map(({ token, example }) => {
trackTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
@ -266,63 +281,16 @@ class NamingModal extends Component {
</div>
</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>
}
{
additional &&
<div>
<FieldSet legend="Episode Title">
<FieldSet legend="Track Title">
<div className={styles.groups}>
{
episodeTitleTokens.map(({ token, example }) => {
trackTitleTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
@ -449,20 +417,16 @@ NamingModal.propTypes = {
value: PropTypes.string.isRequired,
isOpen: PropTypes.bool.isRequired,
advancedSettings: PropTypes.bool.isRequired,
season: PropTypes.bool.isRequired,
episode: PropTypes.bool.isRequired,
daily: PropTypes.bool.isRequired,
anime: PropTypes.bool.isRequired,
album: PropTypes.bool.isRequired,
track: PropTypes.bool.isRequired,
additional: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
NamingModal.defaultProps = {
season: false,
episode: false,
daily: false,
anime: false,
album: false,
track: false,
additional: false
};

@ -46,7 +46,7 @@ class QualityDefinitions extends Component {
<div className={styles.sizeLimitHelpTextContainer}>
<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>
</PageSectionContent>

@ -36,9 +36,9 @@ namespace Lidarr.Api.V3.Config
Get["/examples"] = x => GetExamples(this.Bind<NamingConfigResource>());
SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidEpisodeFormat();
SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidSeriesFolderFormat();
SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidSeasonFolderFormat();
SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidTrackFormat();
SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat();
SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidAlbumFolderFormat();
}
private void UpdateNamingConfig(NamingConfigResource resource)
@ -99,7 +99,7 @@ namespace Lidarr.Api.V3.Config
{
var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec);
var singleTrackValidationResult = _filenameValidationService.ValidateStandardFilename(singleTrackSampleResult);
var singleTrackValidationResult = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult);
var validationFailures = new List<ValidationFailure>();

@ -5,10 +5,6 @@ namespace Lidarr.Api.V3.Config
public class NamingExampleResource
{
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 AlbumFolderExample { get; set; }
}

@ -301,6 +301,7 @@
<Compile Include="NotificationTests\NotificationBaseFixture.cs" />
<Compile Include="NotificationTests\SynologyIndexerFixture.cs" />
<Compile Include="OrganizerTests\FileNameBuilderTests\CleanTitleFixture.cs" />
<Compile Include="OrganizerTests\FileNameBuilderTests\TitleTheFixture.cs" />
<Compile Include="ParserTests\MiniSeriesEpisodeParserFixture.cs" />
<Compile Include="ParserTests\MusicParserFixture.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\})",
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);
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);
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 Regex TitlePrefixRegex = new Regex(@"^(The|An|A) (.*?)((?: *\([^)]+\))*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public FileNameBuilder(INamingConfigService namingConfigService,
IQualityDefinitionService qualityDefinitionService,
ICacheManager cacheManager,
@ -110,10 +112,10 @@ namespace NzbDrone.Core.Organizer
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
tracks = tracks.OrderBy(e => e.AlbumId).ThenBy(e => e.TrackNumber).ToList();
pattern = FormatTrackNumberTokens(pattern, "", tracks);
//pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig);
AddArtistTokens(tokenHandlers, artist);
AddAlbumTokens(tokenHandlers, album);
AddTrackTokens(tokenHandlers, tracks);
@ -143,13 +145,13 @@ namespace NzbDrone.Core.Organizer
if (artist.AlbumFolder)
{
var albumFolder = GetAlbumFolder(artist, album);
albumFolder = CleanFileName(albumFolder);
path = Path.Combine(path, albumFolder);
}
return path;
@ -165,9 +167,9 @@ namespace NzbDrone.Core.Organizer
}
var basicNamingConfig = new BasicNamingConfig
{
Separator = trackFormat.Separator
};
{
Separator = trackFormat.Separator
};
var titleTokens = TitleRegex.Matches(nameSpec.StandardTrackFormat);
@ -238,6 +240,11 @@ namespace NzbDrone.Core.Organizer
return title;
}
public static string TitleThe(string title)
{
return TitlePrefixRegex.Replace(title, "$2, $1$3");
}
public static string CleanFileName(string name, bool replace = true)
{
string result = name;
@ -262,12 +269,14 @@ namespace NzbDrone.Core.Organizer
{
tokenHandlers["{Artist Name}"] = m => 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)
{
tokenHandlers["{Album Title}"] = m => album.Title;
tokenHandlers["{Album CleanTitle}"] = m => CleanTitle(album.Title);
tokenHandlers["{Album TitleThe}"] = m => TitleThe(album.Title);
if (album.ReleaseDate.HasValue)
{
tokenHandlers["{Release Year}"] = m => album.ReleaseDate.Value.Year.ToString();
@ -321,7 +330,7 @@ namespace NzbDrone.Core.Organizer
{
return;
}
var audioCodec = MediaInfoFormatter.FormatAudioCodec(trackFile.MediaInfo);
var audioChannels = MediaInfoFormatter.FormatAudioChannels(trackFile.MediaInfo);
@ -468,7 +477,7 @@ namespace NzbDrone.Core.Organizer
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
{
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.Qualities;
using NzbDrone.Core.Music;
@ -30,12 +30,12 @@ namespace NzbDrone.Core.Organizer
_standardArtist = new Artist
{
Name = "Artist Name"
Name = "The Artist Name"
};
_standardAlbum = new Album
{
Title = "Album Title",
Title = "The Album Title",
ReleaseDate = System.DateTime.Today
};

@ -1,41 +1,19 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Organizer
{
public interface IFilenameValidationService
{
ValidationFailure ValidateStandardFilename(SampleResult sampleResult);
ValidationFailure ValidateTrackFilename(SampleResult sampleResult);
ValidationFailure ValidateDailyFilename(SampleResult sampleResult);
ValidationFailure ValidateAnimeFilename(SampleResult sampleResult);
}
public class FileNameValidationService : IFilenameValidationService
{
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)
{
var validationFailure = new ValidationFailure("StandardTrackFormat", ERROR_MESSAGE);
@ -57,71 +35,5 @@ namespace NzbDrone.Core.Organizer
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…
Cancel
Save