diff --git a/frontend/src/Organize/OrganizePreviewModalContent.js b/frontend/src/Organize/OrganizePreviewModalContent.js index 5e9124db6..061d623bd 100644 --- a/frontend/src/Organize/OrganizePreviewModalContent.js +++ b/frontend/src/Organize/OrganizePreviewModalContent.js @@ -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 &&
{ - renameEpisodes ? + renameTracks ?
Success! My work is done, no files to rename.
:
Renaming is disabled, nothing to rename
} @@ -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 diff --git a/frontend/src/Organize/OrganizePreviewModalContentConnector.js b/frontend/src/Organize/OrganizePreviewModalContentConnector.js index 6fbd2b9eb..4efa06089 100644 --- a/frontend/src/Organize/OrganizePreviewModalContentConnector.js +++ b/frontend/src/Organize/OrganizePreviewModalContentConnector.js @@ -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; diff --git a/frontend/src/Settings/General/GeneralSettings.js b/frontend/src/Settings/General/GeneralSettings.js index 3ab73148f..2bd2f789a 100644 --- a/frontend/src/Settings/General/GeneralSettings.js +++ b/frontend/src/Settings/General/GeneralSettings.js @@ -519,7 +519,7 @@ class GeneralSettings extends Component { - Create empty series folders + Create empty artist folders @@ -103,7 +103,7 @@ class MediaManagement extends Component { @@ -133,7 +133,7 @@ class MediaManagement extends Component { @@ -163,14 +163,14 @@ class MediaManagement extends Component { legend="File Management" > - Ignore Deleted Episodes + Ignore Deleted Tracks @@ -195,12 +195,12 @@ class MediaManagement extends Component { isAdvanced={true} size={sizes.MEDIUM} > - Analyse video files + Analyse audio files @@ -231,7 +231,7 @@ class MediaManagement extends Component { diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js index 03f45e908..8e060aea0 100644 --- a/frontend/src/Settings/MediaManagement/Naming/Naming.js +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js @@ -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 { { - renameEpisodes && + renameTracks &&
- Standard Episode Format + Standard Track Format ?} onChange={onInputChange} - {...settings.standardEpisodeFormat} - helpTexts={standardEpisodeFormatHelpTexts} - errors={[...standardEpisodeFormatErrors, ...settings.standardEpisodeFormat.errors]} + {...settings.standardTrackFormat} + helpTexts={standardTrackFormatHelpTexts} + errors={[...standardTrackFormatErrors, ...settings.standardTrackFormat.errors]} /> - - Daily Episode Format - - ?} - onChange={onInputChange} - {...settings.dailyEpisodeFormat} - helpTexts={dailyEpisodeFormatHelpTexts} - errors={[...dailyEpisodeFormatErrors, ...settings.dailyEpisodeFormat.errors]} - /> - - - - Anime Episode Format - - ?} - onChange={onInputChange} - {...settings.animeEpisodeFormat} - helpTexts={animeEpisodeFormatHelpTexts} - errors={[...animeEpisodeFormatErrors, ...settings.animeEpisodeFormat.errors]} - /> -
} @@ -270,45 +202,32 @@ class Naming extends Component { advancedSettings={advancedSettings} isAdvanced={true} > - Series Folder Format + Artist Folder Format ?} + name="artistFolderFormat" + buttons={?} 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]} /> - Season Folder Format + Album Folder Format ?} - onChange={onInputChange} - {...settings.seasonFolderFormat} - helpTexts={seasonFolderFormatHelpTexts} - errors={[...seasonFolderFormatErrors, ...settings.seasonFolderFormat.errors]} - /> - - - - Multi-Episode Style - - ?} onChange={onInputChange} - {...settings.multiEpisodeStyle} + {...settings.albumFolderFormat} + helpTexts={albumFolderFormatHelpTexts} + errors={[...albumFolderFormatErrors, ...settings.albumFolderFormat.errors]} /> diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index 59800e52a..45869d06f 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -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 { } -
+
{ - seriesTokens.map(({ token, example }) => { + artistTokens.map(({ token, example }) => { return ( { - season && -
-
+ album && +
+
+
{ - seasonTokens.map(({ token, example }) => { + albumTokens.map(({ token, example }) => { return ( -
+
+
+ +
+
+ { + releaseDateTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+
} { - episode && + track &&
-
+
{ - episodeTokens.map(({ token, example }) => { + trackTokens.map(({ token, example }) => { return (
- { - daily && -
-
- { - airDateTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- } - - { - anime && -
-
- { - absoluteTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- }
} { additional &&
-
+
{ - episodeTitleTokens.map(({ token, example }) => { + trackTitleTokens.map(({ token, example }) => { return (
- Limits are automatically adjusted for the series runtime and number of episodes in the file. + Limits are automatically adjusted for the album duration.
diff --git a/src/Lidarr.Api.V3/Config/NamingConfigModule.cs b/src/Lidarr.Api.V3/Config/NamingConfigModule.cs index 8dd3c9c74..4484f0455 100644 --- a/src/Lidarr.Api.V3/Config/NamingConfigModule.cs +++ b/src/Lidarr.Api.V3/Config/NamingConfigModule.cs @@ -36,9 +36,9 @@ namespace Lidarr.Api.V3.Config Get["/examples"] = x => GetExamples(this.Bind()); - 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(); diff --git a/src/Lidarr.Api.V3/Config/NamingExampleResource.cs b/src/Lidarr.Api.V3/Config/NamingExampleResource.cs index b086a3f06..d516dc8e1 100644 --- a/src/Lidarr.Api.V3/Config/NamingExampleResource.cs +++ b/src/Lidarr.Api.V3/Config/NamingExampleResource.cs @@ -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; } } diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 10ee75fc3..407c6ca10 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -301,6 +301,7 @@ + diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs new file mode 100644 index 000000000..ee407d3e0 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.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 + { + private Artist _artist; + private Album _album; + private Track _track; + private TrackFile _trackFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _artist = Builder + .CreateNew() + .With(s => s.Name = "Alien Ant Farm") + .Build(); + + _album = Builder + .CreateNew() + .With(s => s.Title = "Anthology") + .Build(); + + _track = Builder.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() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(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 }, _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 }, _artist, _album, _trackFile) + .Should().Be(name); + } + } +} diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index b8374f866..fb916f4e5 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -60,10 +60,10 @@ namespace NzbDrone.Core.Organizer public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly Regex ArtistNameRegex = new Regex(@"(?\{(?:Artist)(?[- ._])(Clean)?Name\})", + public static readonly Regex ArtistNameRegex = new Regex(@"(?\{(?:Artist)(?[- ._])(Clean)?Name(The)\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly Regex AlbumTitleRegex = new Regex(@"(?\{(?:Album)(?[- ._])(Clean)?Title\})", + public static readonly Regex AlbumTitleRegex = new Regex(@"(?\{(?:Album)(?[- ._])(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>(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> 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() + return _absoluteTrackFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() .Select(match => new AbsoluteTrackFormat { Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-", diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 1ef6af4c9..13dc05942 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -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 }; diff --git a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs index a26b619c8..324940453 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs @@ -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 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; - } } }