NamingConfig Refactor

Adds track NamingConfig, Gets naming section in settings working. Adds Release Year token and track number token
pull/6/head
Qstick 8 years ago
parent a8ac1f3adc
commit fe58f54ad4

@ -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<ValidationFailure>();
validationFailures.AddIfNotNull(singleEpisodeValidationResult);
validationFailures.AddIfNotNull(singleTrackValidationResult);
validationFailures.AddIfNotNull(multiEpisodeValidationResult);
validationFailures.AddIfNotNull(dailyEpisodeValidationResult);
validationFailures.AddIfNotNull(animeEpisodeValidationResult);

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

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

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

@ -18,12 +18,13 @@ namespace NzbDrone.Core.Organizer
public interface IBuildFileNames
{
string BuildFileName(List<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null);
string BuildTrackFileName(List<Track> 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>\{episode(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex TrackRegex = new Regex(@"(?<track>\{track(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
@ -61,6 +65,12 @@ 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\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex AlbumTitleRegex = new Regex(@"(?<token>\{(?:Album)(?<separator>[- ._])(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<Track> 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<string, Func<TokenMatch, string>>(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<string, Func<TokenMatch, string>>(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<string, Func<TokenMatch, string>> tokenHandlers, List<Episode> episodes, NamingConfig namingConfig)
@ -469,6 +521,12 @@ namespace NzbDrone.Core.Organizer
tokenHandlers["{Episode CleanTitle}"] = m => CleanTitle(GetEpisodeTitle(episodes, "and"));
}
private void AddTrackTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, List<Track> tracks)
{
tokenHandlers["{Track Title}"] = m => GetTrackTitle(tracks, "+");
tokenHandlers["{Track CleanTitle}"] = m => CleanTitle(GetTrackTitle(tracks, "and"));
}
private void AddEpisodeFileTokens(Dictionary<string, Func<TokenMatch, string>> 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<string, Func<TokenMatch, string>> 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<string, Func<TokenMatch, string>> 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<string, Func<TokenMatch, string>> 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<string, Func<TokenMatch, string>> 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<Track> 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<Episode> episodes)
{
var pattern = string.Empty;
@ -765,6 +856,30 @@ namespace NzbDrone.Core.Organizer
return string.Join(separator, titles);
}
private string GetTrackTitle(List<Track> 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();
// }
// var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
// 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<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
private string GetOriginalFileName(TrackFile trackFile)
{
if (trackFile.RelativePath.IsNullOrWhiteSpace())
{
return Path.GetFileNameWithoutExtension(trackFile.Path);
}
// //AddSeriesTokens(tokenHandlers, artist);
// //AddSeasonTokens(tokenHandlers, seasonNumber);
return Path.GetFileNameWithoutExtension(trackFile.RelativePath);
}
// //return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig));
//}
}
internal sealed class TokenMatch

@ -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<Episode> _singleEpisode;
private static List<Track> _singleTrack;
private static List<Episode> _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<Episode> { _episode1 };
_singleTrack = new List<Track> { _track1 };
_multiEpisodes = new List<Episode> { _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<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec)
{
try
@ -238,5 +300,17 @@ namespace NzbDrone.Core.Organizer
return string.Empty;
}
}
private string BuildTrackSample(List<Track> tracks, Artist artist, Album album, TrackFile trackFile, NamingConfig nameSpec)
{
try
{
return _buildFileNames.BuildTrackFileName(tracks, artist, album, trackFile, nameSpec);
}
catch (NamingFormatException)
{
return string.Empty;
}
}
}
}

@ -18,6 +18,12 @@ namespace NzbDrone.Core.Organizer
return ruleBuilder.SetValidator(new ValidStandardEpisodeFormatValidator());
}
public static IRuleBuilderOptions<T, string> ValidTrackFormat<T>(this IRuleBuilder<T, string> ruleBuilder)
{
ruleBuilder.SetValidator(new NotEmptyValidator(null));
return ruleBuilder.SetValidator(new ValidStandardTrackFormatValidator());
}
public static IRuleBuilderOptions<T, string> ValidDailyEpisodeFormat<T>(this IRuleBuilder<T, string> 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<T, string> ValidArtistFolderFormat<T>(this IRuleBuilder<T, string> ruleBuilder)
{
ruleBuilder.SetValidator(new NotEmptyValidator(null));
return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.ArtistNameRegex)).WithMessage("Must contain Artist name");
}
public static IRuleBuilderOptions<T, string> ValidAlbumFolderFormat<T>(this IRuleBuilder<T, string> 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()

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

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

@ -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<Episode> Episodes { get; set; }
public EpisodeFile EpisodeFile { get; set; }
public List<Track> Tracks { get; set; }
public TrackFile TrackFile { get; set; }
}
}

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

@ -1,13 +1,13 @@
<fieldset>
<legend>Episode Naming</legend>
<legend>Track Naming</legend>
<div class="form-group">
<label class="col-sm-3 control-label">Rename Episodes</label>
<label class="col-sm-3 control-label">Rename Tracks</label>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="renameEpisodes" class="x-rename-episodes"/>
<input type="checkbox" name="renameTracks" class="x-rename-tracks"/>
<p>
<span>Yes</span>
@ -51,7 +51,7 @@
<div class="basic-setting x-basic-naming"></div>
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Standard Episode Format</label>
<label class="col-sm-3 control-label">Standard Track Format</label>
<div class="col-sm-1 col-sm-push-8 help-inline">
<i class="icon-lidarr-form-info" title="" data-original-title="All caps or all lower-case can also be used"></i>
@ -60,16 +60,17 @@
<div class="col-sm-8 col-sm-pull-1">
<div class="input-group x-helper-input">
<input type="text" class="form-control naming-format" name="standardEpisodeFormat" data-onkeyup="true" />
<input type="text" class="form-control naming-format" name="standardTrackFormat" data-onkeyup="true" />
<div class="input-group-btn btn-group x-naming-token-helper">
<button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown">
<i class="icon-lidarr-add"></i>
</button>
<ul class="dropdown-menu">
{{> SeriesTitleNamingPartial}}
{{> SeasonNamingPartial}}
{{> EpisodeNamingPartial}}
{{> EpisodeTitleNamingPartial}}
{{> ArtistNameNamingPartial}}
{{> AlbumTitleNamingPartial}}
{{> ReleaseYearNamingPartial}}
{{> TrackNumNamingPartial}}
{{> TrackTitleNamingPartial}}
{{> QualityNamingPartial}}
{{> MediaInfoNamingPartial}}
{{> ReleaseGroupNamingPartial}}
@ -81,87 +82,24 @@
</div>
</div>
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Daily Episode Format</label>
<div class="col-sm-1 col-sm-push-8 help-inline">
<i class="icon-lidarr-form-info" title="" data-original-title="All caps or all lower-case can also be used"></i>
<a href="https://github.com/NzbDrone/NzbDrone/wiki/Sorting-and-Renaming" class="help-link" title="More information"><i class="icon-lidarr-form-info-link"/></a>
</div>
<div class="col-sm-8 col-sm-pull-1">
<div class="input-group x-helper-input">
<input type="text" class="form-control naming-format" name="dailyEpisodeFormat" data-onkeyup="true" />
<div class="input-group-btn btn-group x-naming-token-helper">
<button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown">
<i class="icon-lidarr-add"></i>
</button>
<ul class="dropdown-menu">
{{> SeriesTitleNamingPartial}}
{{> AirDateNamingPartial}}
{{> SeasonNamingPartial}}
{{> EpisodeNamingPartial}}
{{> EpisodeTitleNamingPartial}}
{{> QualityNamingPartial}}
{{> MediaInfoNamingPartial}}
{{> ReleaseGroupNamingPartial}}
{{> OriginalTitleNamingPartial}}
{{> SeparatorNamingPartial}}
</ul>
</div>
</div>
</div>
</div>
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Anime Episode Format</label>
<div class="col-sm-1 col-sm-push-8 help-inline">
<i class="icon-lidarr-form-info" title="" data-original-title="All caps or all lower-case can also be used"></i>
<a href="https://github.com/NzbDrone/NzbDrone/wiki/Sorting-and-Renaming" class="help-link" title="More information"><i class="icon-lidarr-form-info-link"/></a>
</div>
<div class="col-sm-8 col-sm-pull-1">
<div class="input-group x-helper-input">
<input type="text" class="form-control naming-format" name="animeEpisodeFormat" data-onkeyup="true" />
<div class="input-group-btn btn-group x-naming-token-helper">
<button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown">
<i class="icon-lidarr-add"></i>
</button>
<ul class="dropdown-menu">
{{> SeriesTitleNamingPartial}}
{{> AbsoluteEpisodeNamingPartial}}
{{> SeasonNamingPartial}}
{{> EpisodeNamingPartial}}
{{> EpisodeTitleNamingPartial}}
{{> QualityNamingPartial}}
{{> MediaInfoNamingPartial}}
{{> ReleaseGroupNamingPartial}}
{{> OriginalTitleNamingPartial}}
{{> SeparatorNamingPartial}}
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Series Folder Format</label>
<label class="col-sm-3 control-label">Artist Folder Format</label>
<div class="col-sm-1 col-sm-push-8 help-inline">
<i class="icon-lidarr-form-info" title="" data-original-title="All caps or all lower-case can also be used. Only used when adding a new series."></i>
<i class="icon-lidarr-form-info" title="" data-original-title="All caps or all lower-case can also be used. Only used when adding a new artist."></i>
</div>
<div class="col-sm-8 col-sm-pull-1">
<div class="input-group x-helper-input">
<input type="text" class="form-control naming-format" name="seriesFolderFormat" data-onkeyup="true"/>
<input type="text" class="form-control naming-format" name="artistFolderFormat" data-onkeyup="true"/>
<div class="input-group-btn btn-group x-naming-token-helper">
<button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown">
<i class="icon-lidarr-add"></i>
</button>
<ul class="dropdown-menu">
{{> SeriesTitleNamingPartial}}
{{> ArtistNameNamingPartial}}
</ul>
</div>
</div>
@ -169,18 +107,19 @@
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Season Folder Format</label>
<label class="col-sm-3 control-label">Album Folder Format</label>
<div class="col-sm-8">
<div class="input-group x-helper-input">
<input type="text" class="form-control naming-format" name="seasonFolderFormat" data-onkeyup="true"/>
<input type="text" class="form-control naming-format" name="albumFolderFormat" data-onkeyup="true"/>
<div class="input-group-btn btn-group x-naming-token-helper">
<button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown">
<i class="icon-lidarr-add"></i>
</button>
<ul class="dropdown-menu">
{{> SeriesTitleNamingPartial}}
{{> SeasonNamingPartial}}
{{> ArtistNameNamingPartial}}
{{> AlbumTitleNamingPartial}}
{{> ReleaseYearNamingPartial}}
{{> SeparatorNamingPartial}}
</ul>
</div>
@ -188,75 +127,27 @@
</div>
</div>
<div class="x-naming-options">
<div class="form-group">
<label class="col-sm-3 control-label">Multi-Episode Style</label>
<div class="col-sm-2">
<select class="form-control x-multi-episode-style" name="multiEpisodeStyle">
<option value="0">Extend</option>
<option value="1">Duplicate</option>
<option value="2">Repeat</option>
<option value="3">Scene</option>
<option value="4">Range</option>
<option value="5">Prefixed Range</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Single Episode Example</label>
<div class="col-sm-8">
<p class="form-control-static x-single-episode-example naming-example"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Multi-Episode Example</label>
<div class="col-sm-8">
<p class="form-control-static x-multi-episode-example naming-example"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Daily-Episode Example</label>
<div class="col-sm-8">
<p class="form-control-static x-daily-episode-example naming-example"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Anime Episode Example</label>
<div class="col-sm-8">
<p class="form-control-static x-anime-episode-example naming-example"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Anime Multi-Episode Example</label>
<label class="col-sm-3 control-label">Single Track Example</label>
<div class="col-sm-8">
<p class="form-control-static x-anime-multi-episode-example naming-example"></p>
<p class="form-control-static x-single-track-example naming-example"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Series Folder Example</label>
<label class="col-sm-3 control-label">Artist Folder Example</label>
<div class="col-sm-8">
<p class="form-control-static x-series-folder-example naming-example"></p>
<p class="form-control-static x-artist-folder-example naming-example"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Season Folder Example</label>
<label class="col-sm-3 control-label">Album Folder Example</label>
<div class="col-sm-8">
<p class="form-control-static x-season-folder-example naming-example"></p>
<p class="form-control-static x-album-folder-example naming-example"></p>
</div>
</div>
</fieldset>

@ -0,0 +1,11 @@
<li class="dropdown-submenu">
<a href="#" tabindex="-1" data-token="Album Title">Album Title</a>
<ul class="dropdown-menu">
<li><a href="#" data-token="Album Title">Album Title</a></li>
<li><a href="#" data-token="Album.Title">Album.Title</a></li>
<li><a href="#" data-token="Album_Title">Album_Title</a></li>
<li><a href="#" data-token="Album CleanTitle">Album CleanTitle</a></li>
<li><a href="#" data-token="Album.CleanTitle">Album.CleanTitle</a></li>
<li><a href="#" data-token="Album_CleanTitle">Album_CleanTitle</a></li>
</ul>
</li>

@ -0,0 +1,11 @@
<li class="dropdown-submenu">
<a href="#" tabindex="-1" data-token="Artist Name">Artist Name</a>
<ul class="dropdown-menu">
<li><a href="#" data-token="Artist Name">Artist Name</a></li>
<li><a href="#" data-token="Artist.Name">Artist.Name</a></li>
<li><a href="#" data-token="Artist_Name">Artist_Name</a></li>
<li><a href="#" data-token="Artist CleanName">Artist CleanName</a></li>
<li><a href="#" data-token="Artist.CleanName">Artist.CleanName</a></li>
<li><a href="#" data-token="Artist_CleanName">Artist_CleanName</a></li>
</ul>
</li>

@ -0,0 +1 @@
<li><a href="#" data-token="Release Year">Release Year</a></li>

@ -0,0 +1,7 @@
<li class="dropdown-submenu">
<a href="#" tabindex="-1" data-token="track">Track</a>
<ul class="dropdown-menu">
<li><a href="#" data-token="track">1</a></li>
<li><a href="#" data-token="track:00">01</a></li>
</ul>
</li>

@ -0,0 +1,11 @@
<li class="dropdown-submenu">
<a href="#" tabindex="-1" data-token="Track Title">Track Title</a>
<ul class="dropdown-menu">
<li><a href="#" data-token="Track Title">Track Title</a></li>
<li><a href="#" data-token="Track.Title">Track.Title</a></li>
<li><a href="#" data-token="Track_Title">Track_Title</a></li>
<li><a href="#" data-token="Track CleanTitle">Track CleanTitle</a></li>
<li><a href="#" data-token="Track.CleanTitle">Track.CleanTitle</a></li>
<li><a href="#" data-token="Track_CleanTitle">Track_CleanTitle</a></li>
</ul>
</li>
Loading…
Cancel
Save