diff --git a/src/NzbDrone.Api/Config/NamingModule.cs b/src/NzbDrone.Api/Config/NamingModule.cs index a6b5881aa..b2dfb0c9f 100644 --- a/src/NzbDrone.Api/Config/NamingModule.cs +++ b/src/NzbDrone.Api/Config/NamingModule.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using FluentValidation; using FluentValidation.Results; using Nancy.Responses; -using NzbDrone.Core.MediaFiles; +using NzbDrone.Api.REST; using NzbDrone.Core.Organizer; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; using Nancy.ModelBinding; using NzbDrone.Api.Mapping; using NzbDrone.Api.Extensions; @@ -19,13 +13,17 @@ namespace NzbDrone.Api.Config public class NamingModule : NzbDroneRestModule { private readonly INamingConfigService _namingConfigService; - private readonly IBuildFileNames _buildFileNames; + private readonly IFilenameSampleService _filenameSampleService; + private readonly IFilenameValidationService _filenameValidationService; - public NamingModule(INamingConfigService namingConfigService, IBuildFileNames buildFileNames) + public NamingModule(INamingConfigService namingConfigService, + IFilenameSampleService filenameSampleService, + IFilenameValidationService filenameValidationService) : base("config/naming") { _namingConfigService = namingConfigService; - _buildFileNames = buildFileNames; + _filenameSampleService = filenameSampleService; + _filenameValidationService = filenameValidationService; GetResourceSingle = GetNamingConfig; GetResourceById = GetNamingConfig; UpdateResource = UpdateNamingConfig; @@ -57,185 +55,56 @@ namespace NzbDrone.Api.Config private JsonResponse GetExamples(NamingConfigResource config) { + //TODO: Validate that the format is valid var nameSpec = config.InjectTo(); - - var series = new Core.Tv.Series - { - SeriesType = SeriesTypes.Standard, - Title = "Series Title" - }; - - var episode1 = new Episode - { - SeasonNumber = 1, - EpisodeNumber = 1, - Title = "Episode Title (1)", - AirDate = "2013-10-30" - }; - - var episode2 = new Episode - { - SeasonNumber = 1, - EpisodeNumber = 2, - Title = "Episode Title (2)" - }; - - var episodeFile = new EpisodeFile - { - Quality = new QualityModel(Quality.HDTV720p), - Path = @"C:\Test\Series.Title.S01E01.720p.HDTV.x264-EVOLVE.mkv" - }; - var sampleResource = new NamingSampleResource(); + + var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); + var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); - sampleResource.SingleEpisodeExample = BuildSample(new List { episode1 }, - series, - episodeFile, - nameSpec); + sampleResource.SingleEpisodeExample = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult) != null + ? "Invalid format" + : singleEpisodeSampleResult.Filename; - episodeFile.Path = @"C:\Test\Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE.mkv"; + sampleResource.MultiEpisodeExample = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult) != null + ? "Invalid format" + : multiEpisodeSampleResult.Filename; - sampleResource.MultiEpisodeExample = BuildSample(new List { episode1, episode2 }, - series, - episodeFile, - nameSpec); - - episodeFile.Path = @"C:\Test\Series.Title.2013.10.30.HDTV.x264-EVOLVE.mkv"; - series.SeriesType = SeriesTypes.Daily; - - sampleResource.DailyEpisodeExample = BuildSample(new List { episode1 }, - series, - episodeFile, - nameSpec); + sampleResource.DailyEpisodeExample = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult) != null + ? "Invalid format" + : dailyEpisodeSampleResult.Filename; return sampleResource.AsResponse(); } - private string BuildSample(List episodes, Core.Tv.Series series, EpisodeFile episodeFile, NamingConfig nameSpec) - { - try - { - //TODO: Validate the result is parsable - return _buildFileNames.BuildFilename(episodes, - series, - episodeFile, - nameSpec); - } - catch (NamingFormatException ex) - { - //Catching to avoid blowing up all samples - //TODO: Use validation to report error to client - - return String.Empty; - } - } - private void ValidateFormatResult(NamingConfig nameSpec) { - if (!nameSpec.RenameEpisodes) + var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); + var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); + var singleEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult); + var multiEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult); + var dailyEpisodeValidationResult = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult); + + var validationFailures = new List(); + + if (singleEpisodeValidationResult != null) { - return; + validationFailures.Add(singleEpisodeValidationResult); } - var series = new Core.Tv.Series + if (multiEpisodeValidationResult != null) { - SeriesType = SeriesTypes.Standard, - Title = "Series Title" - }; - - var episode1 = new Episode - { - SeasonNumber = 1, - EpisodeNumber = 1, - Title = "Episode Title (1)", - AirDate = "2013-10-30" - }; - - var episode2 = new Episode - { - SeasonNumber = 1, - EpisodeNumber = 2, - Title = "Episode Title (2)", - AirDate = "2013-10-30" - }; - - var episodeFile = new EpisodeFile - { - Quality = new QualityModel(Quality.HDTV720p) - }; - - if (!ValidateStandardFormat(nameSpec, series, new List { episode1 }, episodeFile)) - { - throw new ValidationException(new List - { - new ValidationFailure("StandardEpisodeFormat", "Results in unparsable filenames") - }.ToArray()); + validationFailures.Add(multiEpisodeValidationResult); } - if (!ValidateStandardFormat(nameSpec, series, new List { episode1, episode2 }, episodeFile)) + if (dailyEpisodeValidationResult != null) { - throw new ValidationException(new List - { - new ValidationFailure("StandardEpisodeFormat", "Results in unparsable multi-episode filenames") - }.ToArray()); + validationFailures.Add(dailyEpisodeValidationResult); } - if (!ValidateDailyFormat(nameSpec, series, episode1, episodeFile)) - { - throw new ValidationException(new List - { - new ValidationFailure("DailyEpisodeFormat", "Results in unparsable filenames") - }.ToArray()); - } - } - - private bool ValidateStandardFormat(NamingConfig nameSpec, Core.Tv.Series series, List episodes, EpisodeFile episodeFile) - { - var filename = _buildFileNames.BuildFilename(episodes, series, episodeFile, nameSpec); - var parsedEpisodeInfo = Parser.ParseTitle(filename); - - if (parsedEpisodeInfo == null) - { - return false; - } - - return ValidateSeasonAndEpisodeNumbers(episodes, parsedEpisodeInfo); - } - - private bool ValidateDailyFormat(NamingConfig nameSpec, Core.Tv.Series series, Episode episode, EpisodeFile episodeFile) - { - series.SeriesType = SeriesTypes.Daily; - - var filename = _buildFileNames.BuildFilename(new List { episode }, series, episodeFile, nameSpec); - var parsedEpisodeInfo = Parser.ParseTitle(filename); - - if (parsedEpisodeInfo == null) - { - return false; - } - - if (parsedEpisodeInfo.IsDaily()) - { - if (!parsedEpisodeInfo.AirDate.Equals(episode.AirDate)) - { - return false; - } - - return true; - } - - return ValidateSeasonAndEpisodeNumbers(new List {episode}, parsedEpisodeInfo); - } - - 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; + throw new ValidationException(validationFailures.ToArray()); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index a066a603a..8e32b557d 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -324,11 +324,14 @@ + + + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 22e484a8d..eb38719d2 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Organizer private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=}).+?)?(?s?{season(?:\:0+)?}(?e|x)?(?{episode(?:\:0+)?}))(?.+?(?={))?", + public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=}).+?)?(?s?{season(?:\:0+)?}(?e|x)(?{episode(?:\:0+)?}))(?.+?(?={))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); diff --git a/src/NzbDrone.Core/Organizer/FilenameSampleService.cs b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs new file mode 100644 index 000000000..2ea499b9b --- /dev/null +++ b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Organizer +{ + public interface IFilenameSampleService + { + SampleResult GetStandardSample(NamingConfig nameSpec); + SampleResult GetMultiEpisodeSample(NamingConfig nameSpec); + SampleResult GetDailySample(NamingConfig nameSpec); + } + + public class FilenameSampleService : IFilenameSampleService + { + private readonly IBuildFileNames _buildFileNames; + private static Series _standardSeries; + private static Series _dailySeries; + private static Episode _episode1; + private static Episode _episode2; + private static List _singleEpisode; + private static List _multiEpisodes; + private static EpisodeFile _singleEpisodeFile; + private static EpisodeFile _multiEpisodeFile; + private static EpisodeFile _dailyEpisodeFile; + + public FilenameSampleService(IBuildFileNames buildFileNames) + { + _buildFileNames = buildFileNames; + _standardSeries = new Series + { + SeriesType = SeriesTypes.Standard, + Title = "Series Title" + }; + + _dailySeries = new Series + { + SeriesType = SeriesTypes.Daily, + Title = "Series Title" + }; + + _episode1 = new Episode + { + SeasonNumber = 1, + EpisodeNumber = 1, + Title = "Episode Title (1)", + AirDate = "2013-10-30" + }; + + _episode2 = new Episode + { + SeasonNumber = 1, + EpisodeNumber = 2, + Title = "Episode Title (2)" + }; + + _singleEpisode = new List { _episode1 }; + _multiEpisodes = new List { _episode1, _episode2 }; + + _singleEpisodeFile = new EpisodeFile + { + Quality = new QualityModel(Quality.HDTV720p), + Path = @"C:\Test\Series.Title.S01E01.720p.HDTV.x264-EVOLVE.mkv" + }; + + _multiEpisodeFile = new EpisodeFile + { + Quality = new QualityModel(Quality.HDTV720p), + Path = @"C:\Test\Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE.mkv" + }; + + _dailyEpisodeFile = new EpisodeFile + { + Quality = new QualityModel(Quality.HDTV720p), + Path = @"C:\Test\Series.Title.2013.10.30.HDTV.x264-EVOLVE.mkv" + }; + } + + public SampleResult GetStandardSample(NamingConfig nameSpec) + { + var result = new SampleResult + { + Filename = BuildSample(_singleEpisode, _standardSeries, _singleEpisodeFile, nameSpec), + Series = _standardSeries, + Episodes = _singleEpisode, + EpisodeFile = _singleEpisodeFile + }; + + return result; + } + + public SampleResult GetMultiEpisodeSample(NamingConfig nameSpec) + { + var result = new SampleResult + { + Filename = BuildSample(_multiEpisodes, _standardSeries, _multiEpisodeFile, nameSpec), + Series = _standardSeries, + Episodes = _multiEpisodes, + EpisodeFile = _multiEpisodeFile + }; + + return result; + } + + public SampleResult GetDailySample(NamingConfig nameSpec) + { + var result = new SampleResult + { + Filename = BuildSample(_singleEpisode, _dailySeries, _dailyEpisodeFile, nameSpec), + Series = _dailySeries, + Episodes = _singleEpisode, + EpisodeFile = _dailyEpisodeFile + }; + + return result; + } + + private string BuildSample(List episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec) + { + try + { + return _buildFileNames.BuildFilename(episodes, series, episodeFile, nameSpec); + } + catch (NamingFormatException ex) + { + return String.Empty; + } + } + } +} diff --git a/src/NzbDrone.Core/Organizer/FilenameValidationService.cs b/src/NzbDrone.Core/Organizer/FilenameValidationService.cs new file mode 100644 index 000000000..53f64bf86 --- /dev/null +++ b/src/NzbDrone.Core/Organizer/FilenameValidationService.cs @@ -0,0 +1,76 @@ +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 ValidateDailyFilename(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 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; + } + + 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; + } + } +} diff --git a/src/NzbDrone.Core/Organizer/SampleResult.cs b/src/NzbDrone.Core/Organizer/SampleResult.cs new file mode 100644 index 000000000..928438e8c --- /dev/null +++ b/src/NzbDrone.Core/Organizer/SampleResult.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Organizer +{ + public class SampleResult + { + public string Filename { get; set; } + public Series Series { get; set; } + public List Episodes { get; set; } + public EpisodeFile EpisodeFile { get; set; } + } +}