From 2e694485fea026db7a50801f6d9f9c5991f296c0 Mon Sep 17 00:00:00 2001
From: Mark McDowall <markus.mcd5@gmail.com>
Date: Fri, 15 Nov 2013 21:03:42 -0800
Subject: [PATCH] Validation for samples and saving

---
 src/NzbDrone.Api/Config/NamingModule.cs       | 211 ++++--------------
 src/NzbDrone.Core/NzbDrone.Core.csproj        |   3 +
 .../Organizer/FileNameBuilder.cs              |   2 +-
 .../Organizer/FilenameSampleService.cs        | 135 +++++++++++
 .../Organizer/FilenameValidationService.cs    |  76 +++++++
 src/NzbDrone.Core/Organizer/SampleResult.cs   |  17 ++
 6 files changed, 272 insertions(+), 172 deletions(-)
 create mode 100644 src/NzbDrone.Core/Organizer/FilenameSampleService.cs
 create mode 100644 src/NzbDrone.Core/Organizer/FilenameValidationService.cs
 create mode 100644 src/NzbDrone.Core/Organizer/SampleResult.cs

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<NamingConfigResource>
     {
         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<NamingSampleResource> GetExamples(NamingConfigResource config)
         {
+            //TODO: Validate that the format is valid
             var nameSpec = config.InjectTo<NamingConfig>();
-
-            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<Episode> { 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<Episode> { 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<Episode> { episode1 },
-                                                             series,
-                                                             episodeFile,
-                                                             nameSpec);
+            sampleResource.DailyEpisodeExample = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult) != null
+                    ? "Invalid format"
+                    : dailyEpisodeSampleResult.Filename;
 
             return sampleResource.AsResponse();
         }
 
-        private string BuildSample(List<Episode> 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<ValidationFailure>();
+
+            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<Episode> { episode1 }, episodeFile))
-            {
-                throw new ValidationException(new List<ValidationFailure>
-                {
-                    new ValidationFailure("StandardEpisodeFormat", "Results in unparsable filenames")
-                }.ToArray());
+                validationFailures.Add(multiEpisodeValidationResult);
             }
 
-            if (!ValidateStandardFormat(nameSpec, series, new List<Episode> { episode1, episode2 }, episodeFile))
+            if (dailyEpisodeValidationResult != null)
             {
-                throw new ValidationException(new List<ValidationFailure>
-                {
-                    new ValidationFailure("StandardEpisodeFormat", "Results in unparsable multi-episode filenames")
-                }.ToArray());
+                validationFailures.Add(dailyEpisodeValidationResult);
             }
 
-            if (!ValidateDailyFormat(nameSpec, series, episode1, episodeFile))
-            {
-                throw new ValidationException(new List<ValidationFailure>
-                {
-                    new ValidationFailure("DailyEpisodeFormat", "Results in unparsable filenames")
-                }.ToArray());
-            }
-        }
-
-        private bool ValidateStandardFormat(NamingConfig nameSpec, Core.Tv.Series series, List<Episode> 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> { 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> {episode}, parsedEpisodeInfo);
-        }
-
-        private bool ValidateSeasonAndEpisodeNumbers(List<Episode> episodes, ParsedEpisodeInfo parsedEpisodeInfo)
-        {
-            if (parsedEpisodeInfo.SeasonNumber != episodes.First().SeasonNumber ||
-                !parsedEpisodeInfo.EpisodeNumbers.OrderBy(e => e).SequenceEqual(episodes.Select(e => e.EpisodeNumber).OrderBy(e => e)))
-            {
-                return false;
-            }
-
-            return true;
+            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 @@
     <Compile Include="Notifications\Xbmc\Model\VersionResult.cs" />
     <Compile Include="Notifications\Xbmc\Model\XbmcJsonResult.cs" />
     <Compile Include="Notifications\Xbmc\Model\XbmcVersion.cs" />
+    <Compile Include="Organizer\FilenameValidationService.cs" />
     <Compile Include="Organizer\EpisodeFormat.cs" />
     <Compile Include="Organizer\Exception.cs" />
     <Compile Include="Organizer\FilenameBuilderTokenEqualityComparer.cs" />
     <Compile Include="Organizer\FileNameValidation.cs" />
     <Compile Include="Organizer\NamingConfigService.cs" />
+    <Compile Include="Organizer\FilenameSampleService.cs" />
+    <Compile Include="Organizer\SampleResult.cs" />
     <Compile Include="Parser\InvalidDateException.cs" />
     <Compile Include="Parser\Model\SeriesTitleInfo.cs" />
     <Compile Include="ProgressMessaging\CommandUpdatedEvent.cs" />
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>\{season(?:\:0+)?})",
                                                               RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
-        public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?<separator>(?<=}).+?)?(?<seasonEpisode>s?{season(?:\:0+)?}(?<episodeSeparator>e|x)?(?<episode>{episode(?:\:0+)?}))(?<separator>.+?(?={))?",
+        public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?<separator>(?<=}).+?)?(?<seasonEpisode>s?{season(?:\:0+)?}(?<episodeSeparator>e|x)(?<episode>{episode(?:\:0+)?}))(?<separator>.+?(?={))?",
                                                                             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<Episode> _singleEpisode;
+        private static List<Episode> _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<Episode> { _episode1 };
+            _multiEpisodes = new List<Episode> { _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<Episode> 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<Episode> episodes, ParsedEpisodeInfo parsedEpisodeInfo)
+        {
+            if (parsedEpisodeInfo.SeasonNumber != episodes.First().SeasonNumber ||
+                !parsedEpisodeInfo.EpisodeNumbers.OrderBy(e => e).SequenceEqual(episodes.Select(e => e.EpisodeNumber).OrderBy(e => e)))
+            {
+                return false;
+            }
+
+            return true;
+        }
+    }
+}
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<Episode> Episodes { get; set; }
+        public EpisodeFile EpisodeFile { get; set; }
+    }
+}