diff --git a/src/NzbDrone.Api/Config/NamingConfigModule.cs b/src/NzbDrone.Api/Config/NamingConfigModule.cs index 0b72e0b0c..d2f05539a 100644 --- a/src/NzbDrone.Api/Config/NamingConfigModule.cs +++ b/src/NzbDrone.Api/Config/NamingConfigModule.cs @@ -35,10 +35,13 @@ namespace NzbDrone.Api.Config SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 5); SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); + SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidTrackFormat(); SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat(); SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat(); SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat(); SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat(); + SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat(); + SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidAlbumFolderFormat(); } private void UpdateNamingConfig(NamingConfigResource resource) @@ -74,6 +77,7 @@ namespace NzbDrone.Api.Config var sampleResource = new NamingSampleResource(); var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec); var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec); @@ -83,6 +87,10 @@ namespace NzbDrone.Api.Config ? "Invalid format" : singleEpisodeSampleResult.FileName; + sampleResource.SingleTrackExample = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult) != null + ? "Invalid format" + : singleTrackSampleResult.FileName; + sampleResource.MultiEpisodeExample = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult) != null ? "Invalid format" : multiEpisodeSampleResult.FileName; @@ -107,18 +115,28 @@ namespace NzbDrone.Api.Config ? "Invalid format" : _filenameSampleService.GetSeasonFolderSample(nameSpec); + sampleResource.ArtistFolderExample = nameSpec.ArtistFolderFormat.IsNullOrWhiteSpace() + ? "Invalid format" + : _filenameSampleService.GetArtistFolderSample(nameSpec); + + sampleResource.AlbumFolderExample = nameSpec.AlbumFolderFormat.IsNullOrWhiteSpace() + ? "Invalid format" + : _filenameSampleService.GetAlbumFolderSample(nameSpec); + return sampleResource.AsResponse(); } private void ValidateFormatResult(NamingConfig nameSpec) { var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec); var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec); var animeMultiEpisodeSampleResult = _filenameSampleService.GetAnimeMultiEpisodeSample(nameSpec); var singleEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult); + var singleTrackValidationResult = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult); var multiEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult); var dailyEpisodeValidationResult = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult); var animeEpisodeValidationResult = _filenameValidationService.ValidateAnimeFilename(animeEpisodeSampleResult); @@ -127,6 +145,7 @@ namespace NzbDrone.Api.Config var validationFailures = new List(); validationFailures.AddIfNotNull(singleEpisodeValidationResult); + validationFailures.AddIfNotNull(singleTrackValidationResult); validationFailures.AddIfNotNull(multiEpisodeValidationResult); validationFailures.AddIfNotNull(dailyEpisodeValidationResult); validationFailures.AddIfNotNull(animeEpisodeValidationResult); diff --git a/src/NzbDrone.Api/Config/NamingConfigResource.cs b/src/NzbDrone.Api/Config/NamingConfigResource.cs index 39147b993..d49c39936 100644 --- a/src/NzbDrone.Api/Config/NamingConfigResource.cs +++ b/src/NzbDrone.Api/Config/NamingConfigResource.cs @@ -6,13 +6,17 @@ namespace NzbDrone.Api.Config public class NamingConfigResource : RestResource { public bool RenameEpisodes { get; set; } + public bool RenameTracks { get; set; } public bool ReplaceIllegalCharacters { get; set; } public int MultiEpisodeStyle { get; set; } public string StandardEpisodeFormat { get; set; } + public string StandardTrackFormat { get; set; } public string DailyEpisodeFormat { get; set; } public string AnimeEpisodeFormat { get; set; } public string SeriesFolderFormat { get; set; } public string SeasonFolderFormat { get; set; } + public string ArtistFolderFormat { get; set; } + public string AlbumFolderFormat { get; set; } public bool IncludeSeriesTitle { get; set; } public bool IncludeEpisodeTitle { get; set; } public bool IncludeQuality { get; set; } @@ -30,13 +34,17 @@ namespace NzbDrone.Api.Config Id = model.Id, RenameEpisodes = model.RenameEpisodes, + RenameTracks = model.RenameTracks, ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, MultiEpisodeStyle = model.MultiEpisodeStyle, StandardEpisodeFormat = model.StandardEpisodeFormat, + StandardTrackFormat = model.StandardTrackFormat, DailyEpisodeFormat = model.DailyEpisodeFormat, AnimeEpisodeFormat = model.AnimeEpisodeFormat, SeriesFolderFormat = model.SeriesFolderFormat, - SeasonFolderFormat = model.SeasonFolderFormat + SeasonFolderFormat = model.SeasonFolderFormat, + ArtistFolderFormat = model.ArtistFolderFormat, + AlbumFolderFormat = model.AlbumFolderFormat //IncludeSeriesTitle //IncludeEpisodeTitle //IncludeQuality @@ -63,13 +71,17 @@ namespace NzbDrone.Api.Config Id = resource.Id, RenameEpisodes = resource.RenameEpisodes, + RenameTracks = resource.RenameTracks, ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, MultiEpisodeStyle = resource.MultiEpisodeStyle, StandardEpisodeFormat = resource.StandardEpisodeFormat, + StandardTrackFormat = resource.StandardTrackFormat, DailyEpisodeFormat = resource.DailyEpisodeFormat, AnimeEpisodeFormat = resource.AnimeEpisodeFormat, SeriesFolderFormat = resource.SeriesFolderFormat, - SeasonFolderFormat = resource.SeasonFolderFormat + SeasonFolderFormat = resource.SeasonFolderFormat, + ArtistFolderFormat = resource.ArtistFolderFormat, + AlbumFolderFormat = resource.AlbumFolderFormat }; } } diff --git a/src/NzbDrone.Api/Config/NamingSampleResource.cs b/src/NzbDrone.Api/Config/NamingSampleResource.cs index 1f9c7f066..f6d6d15b3 100644 --- a/src/NzbDrone.Api/Config/NamingSampleResource.cs +++ b/src/NzbDrone.Api/Config/NamingSampleResource.cs @@ -3,11 +3,14 @@ public class NamingSampleResource { public string SingleEpisodeExample { 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 SeriesFolderExample { get; set; } public string SeasonFolderExample { get; set; } + public string ArtistFolderExample { get; set; } + public string AlbumFolderExample { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs index d845291d8..117412062 100644 --- a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -97,6 +97,8 @@ namespace NzbDrone.Core.Datastore.Migration Alter.Table("NamingConfig") .AddColumn("ArtistFolderFormat").AsString().Nullable() + .AddColumn("RenameTracks").AsBoolean().Nullable() + .AddColumn("StandardTrackFormat").AsString().Nullable() .AddColumn("AlbumFolderFormat").AsString().Nullable(); } diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index d485da076..f5d3c3c24 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -18,12 +18,13 @@ namespace NzbDrone.Core.Organizer public interface IBuildFileNames { string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); + string BuildTrackFileName(List tracks, Artist artist, Album album, TrackFile trackFile, NamingConfig namingConfig = null); string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); string BuildSeasonPath(Series series, int seasonNumber); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); string GetSeriesFolder(Series series, NamingConfig namingConfig = null); string GetArtistFolder(Artist artist, NamingConfig namingConfig = null); - string GetAlbumFolder(Album album, NamingConfig namingConfig = null); + string GetAlbumFolder(Artist artist, Album album, NamingConfig namingConfig = null); string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); // TODO: Implement Music functions @@ -44,6 +45,9 @@ namespace NzbDrone.Core.Organizer private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex TrackRegex = new Regex(@"(?\{track(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -61,6 +65,12 @@ 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\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex AlbumTitleRegex = new Regex(@"(?\{(?:Album)(?[- ._])(Clean)?Title\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); @@ -142,6 +152,47 @@ namespace NzbDrone.Core.Organizer return fileName; } + public string BuildTrackFileName(List tracks, Artist artist, Album album, TrackFile trackFile, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + if (!namingConfig.RenameTracks) + { + return GetOriginalTitle(trackFile); + } + + if (namingConfig.StandardTrackFormat.IsNullOrWhiteSpace()) + { + throw new NamingFormatException("Standard track format cannot be empty"); + } + + var pattern = namingConfig.StandardTrackFormat; + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + tracks = tracks.OrderBy(e => e.AlbumId).ThenBy(e => e.TrackNumber).ToList(); + + //pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); + + pattern = FormatTrackNumberTokens(pattern, "", tracks); + //pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); + + AddArtistTokens(tokenHandlers, artist); + AddAlbumTokens(tokenHandlers, album); + AddTrackTokens(tokenHandlers, tracks); + AddTrackFileTokens(tokenHandlers, trackFile); + AddQualityTokens(tokenHandlers, artist, trackFile); + //AddMediaInfoTokens(tokenHandlers, trackFile); TODO ReWork MediaInfo for Tracks + + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + + return fileName; + } + public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) { Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); @@ -263,7 +314,7 @@ namespace NzbDrone.Core.Organizer return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); } - public string GetAlbumFolder(Album album, NamingConfig namingConfig = null) + public string GetAlbumFolder(Artist artist, Album album, NamingConfig namingConfig = null) { if (namingConfig == null) { @@ -273,6 +324,7 @@ namespace NzbDrone.Core.Organizer var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); AddAlbumTokens(tokenHandlers, album); + AddArtistTokens(tokenHandlers, artist); return CleanFolderName(ReplaceTokens(namingConfig.AlbumFolderFormat, tokenHandlers, namingConfig)); } @@ -322,7 +374,7 @@ namespace NzbDrone.Core.Organizer { tokenHandlers["{Album Title}"] = m => album.Title; tokenHandlers["{Album CleanTitle}"] = m => CleanTitle(album.Title); - tokenHandlers["{Album Year}"] = m => album.ReleaseDate.Year.ToString(); + tokenHandlers["{Release Year}"] = m => album.ReleaseDate.Year.ToString(); } private string AddSeasonEpisodeNumberingTokens(string pattern, Dictionary> tokenHandlers, List episodes, NamingConfig namingConfig) @@ -469,6 +521,12 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Episode CleanTitle}"] = m => CleanTitle(GetEpisodeTitle(episodes, "and")); } + private void AddTrackTokens(Dictionary> tokenHandlers, List tracks) + { + tokenHandlers["{Track Title}"] = m => GetTrackTitle(tracks, "+"); + tokenHandlers["{Track CleanTitle}"] = m => CleanTitle(GetTrackTitle(tracks, "and")); + } + private void AddEpisodeFileTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) { tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); @@ -476,6 +534,13 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Lidarr"); } + private void AddTrackFileTokens(Dictionary> tokenHandlers, TrackFile trackFile) + { + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(trackFile); + tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(trackFile); + tokenHandlers["{Release Group}"] = m => trackFile.ReleaseGroup ?? m.DefaultValue("Lidarr"); + } + private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) { var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title; @@ -488,6 +553,18 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Quality Real}"] = m => qualityReal; } + private void AddQualityTokens(Dictionary> tokenHandlers, Artist artist, TrackFile trackFile) + { + var qualityTitle = _qualityDefinitionService.Get(trackFile.Quality.Quality).Title; + //var qualityProper = GetQualityProper(artist, trackFile.Quality); + //var qualityReal = GetQualityReal(artist, trackFile.Quality); + + tokenHandlers["{Quality Full}"] = m => String.Format("{0}", qualityTitle); + tokenHandlers["{Quality Title}"] = m => qualityTitle; + //tokenHandlers["{Quality Proper}"] = m => qualityProper; + //tokenHandlers["{Quality Real}"] = m => qualityReal; + } + private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) { if (episodeFile.MediaInfo == null) return; @@ -683,6 +760,20 @@ namespace NzbDrone.Core.Organizer return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); } + private string FormatTrackNumberTokens(string basePattern, string formatPattern, List tracks) + { + var pattern = string.Empty; + + for (int i = 0; i < tracks.Count; i++) + { + var patternToReplace = i == 0 ? basePattern : formatPattern; + + pattern += TrackRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["track"].Value, tracks[i].TrackNumber)); + } + + return pattern; + } + private string FormatAbsoluteNumberTokens(string basePattern, string formatPattern, List episodes) { var pattern = string.Empty; @@ -765,6 +856,30 @@ namespace NzbDrone.Core.Organizer return string.Join(separator, titles); } + private string GetTrackTitle(List tracks, string separator) + { + separator = string.Format(" {0} ", separator.Trim()); + + if (tracks.Count == 1) + { + return tracks.First().Title.TrimEnd(EpisodeTitleTrimCharacters); + } + + var titles = tracks.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Select(CleanupEpisodeTitle) + .Distinct() + .ToList(); + + if (titles.All(t => t.IsNullOrWhiteSpace())) + { + titles = tracks.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Distinct() + .ToList(); + } + + return string.Join(separator, titles); + } + private string CleanupEpisodeTitle(string title) { //this will remove (1),(2) from the end of multi part episodes. @@ -806,6 +921,16 @@ namespace NzbDrone.Core.Organizer return episodeFile.SceneName; } + private string GetOriginalTitle(TrackFile trackFile) + { + if (trackFile.SceneName.IsNullOrWhiteSpace()) + { + return GetOriginalFileName(trackFile); + } + + return trackFile.SceneName; + } + private string GetOriginalFileName(EpisodeFile episodeFile) { if (episodeFile.RelativePath.IsNullOrWhiteSpace()) @@ -816,35 +941,16 @@ namespace NzbDrone.Core.Organizer return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); } - //public string GetArtistFolder(Artist artist, NamingConfig namingConfig = null) - //{ - // if (namingConfig == null) - // { - // namingConfig = _namingConfigService.GetConfig(); - // } + private string GetOriginalFileName(TrackFile trackFile) + { + if (trackFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(trackFile.Path); + } - // var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + return Path.GetFileNameWithoutExtension(trackFile.RelativePath); + } - // AddArtistTokens(tokenHandlers, artist); - - // return CleanFolderName(ReplaceTokens("{Artist Name}", tokenHandlers, namingConfig)); //namingConfig.ArtistFolderFormat, - //} - - //public string GetAlbumFolder(Artist artist, string albumName, NamingConfig namingConfig = null) - //{ - // throw new NotImplementedException(); - // //if (namingConfig == null) - // //{ - // // namingConfig = _namingConfigService.GetConfig(); - // //} - - // //var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - // //AddSeriesTokens(tokenHandlers, artist); - // //AddSeasonTokens(tokenHandlers, seasonNumber); - - // //return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); - //} } internal sealed class TokenMatch diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 7f92fe180..324e6e524 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -2,6 +2,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Core.MediaFiles.MediaInfo; namespace NzbDrone.Core.Organizer @@ -9,26 +10,34 @@ namespace NzbDrone.Core.Organizer public interface IFilenameSampleService { SampleResult GetStandardSample(NamingConfig nameSpec); + SampleResult GetStandardTrackSample(NamingConfig nameSpec); SampleResult GetMultiEpisodeSample(NamingConfig nameSpec); SampleResult GetDailySample(NamingConfig nameSpec); SampleResult GetAnimeSample(NamingConfig nameSpec); SampleResult GetAnimeMultiEpisodeSample(NamingConfig nameSpec); string GetSeriesFolderSample(NamingConfig nameSpec); string GetSeasonFolderSample(NamingConfig nameSpec); + string GetArtistFolderSample(NamingConfig nameSpec); + string GetAlbumFolderSample(NamingConfig nameSpec); } public class FileNameSampleService : IFilenameSampleService { private readonly IBuildFileNames _buildFileNames; private static Series _standardSeries; + private static Artist _standardArtist; + private static Album _standardAlbum; + private static Track _track1; private static Series _dailySeries; private static Series _animeSeries; private static Episode _episode1; private static Episode _episode2; private static Episode _episode3; private static List _singleEpisode; + private static List _singleTrack; private static List _multiEpisodes; private static EpisodeFile _singleEpisodeFile; + private static TrackFile _singleTrackFile; private static EpisodeFile _multiEpisodeFile; private static EpisodeFile _dailyEpisodeFile; private static EpisodeFile _animeEpisodeFile; @@ -44,6 +53,17 @@ namespace NzbDrone.Core.Organizer Title = "Series Title (2010)" }; + _standardArtist = new Artist + { + Name = "Artist Name" + }; + + _standardAlbum = new Album + { + Title = "Album Title", + ReleaseDate = System.DateTime.Today + }; + _dailySeries = new Series { SeriesType = SeriesTypes.Daily, @@ -56,6 +76,14 @@ namespace NzbDrone.Core.Organizer Title = "Series Title (2010)" }; + _track1 = new Track + { + TrackNumber = 3, + + Title = "Track Title (1)", + + }; + _episode1 = new Episode { SeasonNumber = 1, @@ -82,6 +110,7 @@ namespace NzbDrone.Core.Organizer }; _singleEpisode = new List { _episode1 }; + _singleTrack = new List { _track1 }; _multiEpisodes = new List { _episode1, _episode2, _episode3 }; var mediaInfo = new MediaInfoModel() @@ -115,6 +144,15 @@ namespace NzbDrone.Core.Organizer MediaInfo = mediaInfo }; + _singleTrackFile = new TrackFile + { + Quality = new QualityModel(Quality.MP3256, new Revision(2)), + RelativePath = "Artist.Name.Album.Name.TrackNum.Track.Title.MP3256.mp3", + SceneName = "Artist.Name.Album.Name.TrackNum.Track.Title.MP3256", + ReleaseGroup = "RlsGrp", + MediaInfo = mediaInfo + }; + _multiEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.MP3256, new Revision(2)), @@ -165,6 +203,20 @@ namespace NzbDrone.Core.Organizer return result; } + public SampleResult GetStandardTrackSample(NamingConfig nameSpec) + { + var result = new SampleResult + { + FileName = BuildTrackSample(_singleTrack, _standardArtist, _standardAlbum, _singleTrackFile, nameSpec), + Artist = _standardArtist, + Album = _standardAlbum, + Tracks = _singleTrack, + TrackFile = _singleTrackFile + }; + + return result; + } + public SampleResult GetMultiEpisodeSample(NamingConfig nameSpec) { var result = new SampleResult @@ -227,6 +279,16 @@ namespace NzbDrone.Core.Organizer return _buildFileNames.GetSeasonFolder(_standardSeries, _episode1.SeasonNumber, nameSpec); } + public string GetArtistFolderSample(NamingConfig nameSpec) + { + return _buildFileNames.GetArtistFolder(_standardArtist, nameSpec); + } + + public string GetAlbumFolderSample(NamingConfig nameSpec) + { + return _buildFileNames.GetAlbumFolder(_standardArtist, _standardAlbum, nameSpec); + } + private string BuildSample(List episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec) { try @@ -238,5 +300,17 @@ namespace NzbDrone.Core.Organizer return string.Empty; } } + + private string BuildTrackSample(List tracks, Artist artist, Album album, TrackFile trackFile, NamingConfig nameSpec) + { + try + { + return _buildFileNames.BuildTrackFileName(tracks, artist, album, trackFile, nameSpec); + } + catch (NamingFormatException) + { + return string.Empty; + } + } } } diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index 930b8a044..5231f1fe6 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -18,6 +18,12 @@ namespace NzbDrone.Core.Organizer return ruleBuilder.SetValidator(new ValidStandardEpisodeFormatValidator()); } + public static IRuleBuilderOptions ValidTrackFormat(this IRuleBuilder ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new ValidStandardTrackFormatValidator()); + } + public static IRuleBuilderOptions ValidDailyEpisodeFormat(this IRuleBuilder ruleBuilder) { ruleBuilder.SetValidator(new NotEmptyValidator(null)); @@ -41,6 +47,17 @@ namespace NzbDrone.Core.Organizer ruleBuilder.SetValidator(new NotEmptyValidator(null)); return ruleBuilder.SetValidator(new RegularExpressionValidator(SeasonFolderRegex)).WithMessage("Must contain season number"); } + public static IRuleBuilderOptions ValidArtistFolderFormat(this IRuleBuilder ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.ArtistNameRegex)).WithMessage("Must contain Artist name"); + } + + public static IRuleBuilderOptions ValidAlbumFolderFormat(this IRuleBuilder ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.AlbumTitleRegex)).WithMessage("Must contain Album name"); + } } public class ValidStandardEpisodeFormatValidator : PropertyValidator @@ -65,6 +82,21 @@ namespace NzbDrone.Core.Organizer } } + public class ValidStandardTrackFormatValidator : PropertyValidator + { + public ValidStandardTrackFormatValidator() + : base("Must contain Album Title and Track numbers OR Original Title") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + + return true; //TODO Add Logic here + } + } + public class ValidDailyEpisodeFormatValidator : PropertyValidator { public ValidDailyEpisodeFormatValidator() diff --git a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs index 9367c11d8..a26b619c8 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.Organizer public interface IFilenameValidationService { ValidationFailure ValidateStandardFilename(SampleResult sampleResult); + ValidationFailure ValidateTrackFilename(SampleResult sampleResult); ValidationFailure ValidateDailyFilename(SampleResult sampleResult); ValidationFailure ValidateAnimeFilename(SampleResult sampleResult); } @@ -35,6 +36,27 @@ namespace NzbDrone.Core.Organizer return null; } + public ValidationFailure ValidateTrackFilename(SampleResult sampleResult) + { + var validationFailure = new ValidationFailure("StandardTrackFormat", ERROR_MESSAGE); + + //TODO Add Validation for TrackFilename + //var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + + + //if (parsedEpisodeInfo == null) + //{ + // return validationFailure; + //} + + //if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo)) + //{ + // return validationFailure; + //} + + return null; + } + public ValidationFailure ValidateDailyFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE); diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index 637cd15cd..b0a4d811a 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Organizer { @@ -7,21 +7,25 @@ namespace NzbDrone.Core.Organizer public static NamingConfig Default => new NamingConfig { RenameEpisodes = false, + RenameTracks = false, ReplaceIllegalCharacters = true, MultiEpisodeStyle = 0, StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", + StandardTrackFormat = "{Artist Name} - {track:00} - {Album Title} - {Track Title}", DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}", AnimeEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", SeriesFolderFormat = "{Series Title}", SeasonFolderFormat = "Season {season}", ArtistFolderFormat = "{Artist Name}", - AlbumFolderFormat = "{Album Name} ({Year})" + AlbumFolderFormat = "{Album Title} ({Release Year})" }; public bool RenameEpisodes { get; set; } + public bool RenameTracks { get; set; } public bool ReplaceIllegalCharacters { get; set; } public int MultiEpisodeStyle { get; set; } public string StandardEpisodeFormat { get; set; } + public string StandardTrackFormat { get; set; } public string DailyEpisodeFormat { get; set; } public string AnimeEpisodeFormat { get; set; } public string SeriesFolderFormat { get; set; } diff --git a/src/NzbDrone.Core/Organizer/SampleResult.cs b/src/NzbDrone.Core/Organizer/SampleResult.cs index 0f3885a1b..3075032ce 100644 --- a/src/NzbDrone.Core/Organizer/SampleResult.cs +++ b/src/NzbDrone.Core/Organizer/SampleResult.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Organizer { @@ -8,7 +9,11 @@ namespace NzbDrone.Core.Organizer { public string FileName { get; set; } public Series Series { get; set; } + public Artist Artist { get; set; } + public Album Album { get; set; } public List Episodes { get; set; } public EpisodeFile EpisodeFile { get; set; } + public List Tracks { get; set; } + public TrackFile TrackFile { get; set; } } } diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index 71e4df4f8..55dbb860f 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -10,26 +10,20 @@ module.exports = (function() { template : 'Settings/MediaManagement/Naming/NamingViewTemplate', ui : { namingOptions : '.x-naming-options', - renameEpisodesCheckbox : '.x-rename-episodes', - singleEpisodeExample : '.x-single-episode-example', - multiEpisodeExample : '.x-multi-episode-example', - dailyEpisodeExample : '.x-daily-episode-example', - animeEpisodeExample : '.x-anime-episode-example', - animeMultiEpisodeExample : '.x-anime-multi-episode-example', + renameTracksCheckbox : '.x-rename-tracks', + singleTrackExample : '.x-single-track-example', namingTokenHelper : '.x-naming-token-helper', - multiEpisodeStyle : '.x-multi-episode-style', - seriesFolderExample : '.x-series-folder-example', - seasonFolderExample : '.x-season-folder-example' + artistFolderExample : '.x-artist-folder-example', + albumFolderExample : '.x-album-folder-example' }, events : { - "change .x-rename-episodes" : '_setFailedDownloadOptionsVisibility', + "change .x-rename-tracks" : '_setFailedDownloadOptionsVisibility', "click .x-show-wizard" : '_showWizard', - "click .x-naming-token-helper a" : '_addToken', - "change .x-multi-episode-style" : '_multiEpisodeFomatChanged' + "click .x-naming-token-helper a" : '_addToken' }, regions : { basicNamingRegion : '.x-basic-naming' }, onRender : function() { - if (!this.model.get('renameEpisodes')) { + if (!this.model.get('renameTracks')) { this.ui.namingOptions.hide(); } var basicNamingView = new BasicNamingView({ model : this.model }); @@ -40,7 +34,7 @@ module.exports = (function() { this._updateSamples(); }, _setFailedDownloadOptionsVisibility : function() { - var checked = this.ui.renameEpisodesCheckbox.prop('checked'); + var checked = this.ui.renameTracksCheckbox.prop('checked'); if (checked) { this.ui.namingOptions.slideDown(); } else { @@ -51,13 +45,9 @@ module.exports = (function() { this.namingSampleModel.fetch({ data : this.model.toJSON() }); }, _showSamples : function() { - this.ui.singleEpisodeExample.html(this.namingSampleModel.get('singleEpisodeExample')); - this.ui.multiEpisodeExample.html(this.namingSampleModel.get('multiEpisodeExample')); - this.ui.dailyEpisodeExample.html(this.namingSampleModel.get('dailyEpisodeExample')); - this.ui.animeEpisodeExample.html(this.namingSampleModel.get('animeEpisodeExample')); - this.ui.animeMultiEpisodeExample.html(this.namingSampleModel.get('animeMultiEpisodeExample')); - this.ui.seriesFolderExample.html(this.namingSampleModel.get('seriesFolderExample')); - this.ui.seasonFolderExample.html(this.namingSampleModel.get('seasonFolderExample')); + this.ui.singleTrackExample.html(this.namingSampleModel.get('singleTrackExample')); + this.ui.artistFolderExample.html(this.namingSampleModel.get('artistFolderExample')); + this.ui.albumFolderExample.html(this.namingSampleModel.get('albumFolderExample')); }, _addToken : function(e) { e.preventDefault(); @@ -75,9 +65,6 @@ module.exports = (function() { this.ui.namingTokenHelper.removeClass('open'); input.focus(); }, - multiEpisodeFormatChanged : function() { - this.model.set('multiEpisodeStyle', this.ui.multiEpisodeStyle.val()); - } }); AsModelBoundView.call(view); AsValidatedView.call(view); diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs index 39ddcf63f..6b46ace90 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs @@ -1,13 +1,13 @@
- Episode Naming + Track Naming
- +
diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs new file mode 100644 index 000000000..9e3da7a54 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs @@ -0,0 +1,11 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs new file mode 100644 index 000000000..c951bd123 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs @@ -0,0 +1,11 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs new file mode 100644 index 000000000..0a4153d66 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs @@ -0,0 +1 @@ +
  • Release Year
  • \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs new file mode 100644 index 000000000..68ddc8ff5 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs @@ -0,0 +1,7 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs new file mode 100644 index 000000000..591c57098 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs @@ -0,0 +1,11 @@ +