Merge pull request #141 from Sonarr/multi-episode-range

Various naming fixes
pull/2/head
Mark McDowall 10 years ago
commit f214e5cb25

@ -82,6 +82,7 @@ namespace NzbDrone.Api.Config
var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec);
var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec);
var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec);
var animeMultiEpisodeSampleResult = _filenameSampleService.GetAnimeMultiEpisodeSample(nameSpec);
sampleResource.SingleEpisodeExample = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult) != null
? "Invalid format"
@ -99,6 +100,10 @@ namespace NzbDrone.Api.Config
? "Invalid format"
: animeEpisodeSampleResult.FileName;
sampleResource.AnimeMultiEpisodeExample = _filenameValidationService.ValidateAnimeFilename(animeMultiEpisodeSampleResult) != null
? "Invalid format"
: animeMultiEpisodeSampleResult.FileName;
sampleResource.SeriesFolderExample = nameSpec.SeriesFolderFormat.IsNullOrWhiteSpace()
? "Invalid format"
: _filenameSampleService.GetSeriesFolderSample(nameSpec);
@ -115,26 +120,22 @@ namespace NzbDrone.Api.Config
var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(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 multiEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult);
var dailyEpisodeValidationResult = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult);
var animeEpisodeValidationResult = _filenameValidationService.ValidateAnimeFilename(animeEpisodeSampleResult);
var animeMultiEpisodeValidationResult = _filenameValidationService.ValidateAnimeFilename(animeMultiEpisodeSampleResult);
var validationFailures = new List<ValidationFailure>();
if (singleEpisodeValidationResult != null)
{
validationFailures.Add(singleEpisodeValidationResult);
}
if (multiEpisodeValidationResult != null)
{
validationFailures.Add(multiEpisodeValidationResult);
}
if (dailyEpisodeValidationResult != null)
{
validationFailures.Add(dailyEpisodeValidationResult);
}
validationFailures.AddIfNotNull(singleEpisodeValidationResult);
validationFailures.AddIfNotNull(multiEpisodeValidationResult);
validationFailures.AddIfNotNull(dailyEpisodeValidationResult);
validationFailures.AddIfNotNull(animeEpisodeValidationResult);
validationFailures.AddIfNotNull(animeMultiEpisodeValidationResult);
if (validationFailures.Any())
{

@ -6,6 +6,7 @@
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; }
}

@ -20,6 +20,7 @@ namespace NzbDrone.Core.Test.OrganizerTests
private Series _series;
private Episode _episode1;
private Episode _episode2;
private Episode _episode3;
private EpisodeFile _episodeFile;
private NamingConfig _namingConfig;
@ -53,6 +54,13 @@ namespace NzbDrone.Core.Test.OrganizerTests
.With(e => e.AbsoluteEpisodeNumber = 101)
.Build();
_episode3 = Builder<Episode>.CreateNew()
.With(e => e.Title = "City Sushi")
.With(e => e.SeasonNumber = 15)
.With(e => e.EpisodeNumber = 8)
.With(e => e.AbsoluteEpisodeNumber = 102)
.Build();
_episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "DRONE" };
Mocker.GetMock<IQualityDefinitionService>()
@ -549,8 +557,8 @@ namespace NzbDrone.Core.Test.OrganizerTests
_namingConfig.MultiEpisodeStyle = (int)MultiEpisodeStyle.Duplicate;
_namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:000} - {Episode Title}";
Subject.BuildFileName(new List<Episode> { _episode1, _episode2 }, _series, _episodeFile)
.Should().Be("South Park - 100 - 101 - City Sushi");
Subject.BuildFileName(new List<Episode> { _episode1, _episode2, _episode3 }, _series, _episodeFile)
.Should().Be("South Park - 100 - 101 - 102 - City Sushi");
}
[Test]
@ -682,5 +690,59 @@ namespace NzbDrone.Core.Test.OrganizerTests
Subject.BuildFileName(new List<Episode> { _episode1, _episode2 }, _series, _episodeFile)
.Should().Be("South Park - 15x06 - 15x07 - [100-101] - City Sushi - HDTV-720p");
}
[Test]
public void should_format_range_multi_episode_properly()
{
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}";
_namingConfig.MultiEpisodeStyle = 4;
Subject.BuildFileName(new List<Episode> { _episode1, _episode2, _episode3 }, _series, _episodeFile)
.Should().Be("South Park - S15E06-08 - City Sushi");
}
[Test]
public void should_format_range_multi_episode_anime_properly()
{
_series.SeriesType = SeriesTypes.Anime;
_namingConfig.MultiEpisodeStyle = 4;
_namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:000} - {Episode Title}";
Subject.BuildFileName(new List<Episode> { _episode1, _episode2, _episode3 }, _series, _episodeFile)
.Should().Be("South Park - 100-102 - City Sushi");
}
[Test]
public void should_format_repeat_multi_episode_anime_properly()
{
_series.SeriesType = SeriesTypes.Anime;
_namingConfig.MultiEpisodeStyle = 2;
_namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:000} - {Episode Title}";
Subject.BuildFileName(new List<Episode> { _episode1, _episode2, _episode3 }, _series, _episodeFile)
.Should().Be("South Park - 100-101-102 - City Sushi");
}
[Test]
public void should_format_single_episode_with_range_multi_episode_properly()
{
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}";
_namingConfig.MultiEpisodeStyle = 4;
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be("South Park - S15E06 - City Sushi");
}
[Test]
public void should_format_single_anime_episode_with_range_multi_episode_properly()
{
_series.SeriesType = SeriesTypes.Anime;
_namingConfig.MultiEpisodeStyle = 4;
_namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:000} - {Episode Title}";
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be("South Park - 100 - City Sushi");
}
}
}

@ -97,6 +97,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[ANBU-AonE]_Naruto_26-27_[F224EF26].avi", "Naruto", new[] { 26, 27 })]
[TestCase("[Doutei] Recently, My Sister is Unusual - 01-12 [BD][720p-AAC]", "Recently, My Sister is Unusual", new [] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 })]
[TestCase("Series Title (2010) - 01-02-03 - Episode Title (1) HDTV-720p", "Series Title (2010)", new [] { 1, 2, 3 })]
public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int[] absoluteEpisodeNumbers)
{
var result = Parser.Parser.ParseTitle(postTitle);

@ -280,32 +280,41 @@ namespace NzbDrone.Core.Organizer
foreach (var episodeFormat in episodeFormats)
{
var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern;
string formatPattern;
foreach (var episode in episodes.Skip(1))
switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle)
{
switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle)
{
case MultiEpisodeStyle.Duplicate:
seasonEpisodePattern += episodeFormat.Separator + episodeFormat.SeasonEpisodePattern;
break;
case MultiEpisodeStyle.Repeat:
seasonEpisodePattern += episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern;
break;
case MultiEpisodeStyle.Scene:
seasonEpisodePattern += "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern;
break;
//MultiEpisodeStyle.Extend
default:
seasonEpisodePattern += "-" + episodeFormat.EpisodePattern;
break;
}
case MultiEpisodeStyle.Duplicate:
formatPattern = episodeFormat.Separator + episodeFormat.SeasonEpisodePattern;
seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes);
break;
case MultiEpisodeStyle.Repeat:
formatPattern = episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern;
seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes);
break;
case MultiEpisodeStyle.Scene:
formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern;
seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes);
break;
case MultiEpisodeStyle.Range:
formatPattern = "-" + episodeFormat.EpisodePattern;
var eps = new List<Episode> { episodes.First() };
if (episodes.Count > 1) eps.Add(episodes.Last());
seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, eps);
break;
//MultiEpisodeStyle.Extend
default:
formatPattern = "-" + episodeFormat.EpisodePattern;
seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes);
break;
}
seasonEpisodePattern = ReplaceNumberTokens(seasonEpisodePattern, episodes);
var token = String.Format("{{Season Episode{0}}}", index++);
pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, token);
tokenHandlers[token] = m => seasonEpisodePattern;
@ -339,34 +348,44 @@ namespace NzbDrone.Core.Organizer
}
var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern;
string formatPattern;
foreach (var episode in episodes.Skip(1))
switch ((MultiEpisodeStyle) namingConfig.MultiEpisodeStyle)
{
switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle)
{
case MultiEpisodeStyle.Duplicate:
absoluteEpisodePattern += absoluteEpisodeFormat.Separator +
absoluteEpisodeFormat.AbsoluteEpisodePattern;
break;
case MultiEpisodeStyle.Repeat:
absoluteEpisodePattern += absoluteEpisodeFormat.Separator +
absoluteEpisodeFormat.AbsoluteEpisodePattern;
break;
case MultiEpisodeStyle.Duplicate:
formatPattern = absoluteEpisodeFormat.Separator + absoluteEpisodeFormat.AbsoluteEpisodePattern;
absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes);
break;
case MultiEpisodeStyle.Repeat:
var repeatSeparator = absoluteEpisodeFormat.Separator.Trim().IsNullOrWhiteSpace() ? " " : absoluteEpisodeFormat.Separator.Trim();
formatPattern = repeatSeparator + absoluteEpisodeFormat.AbsoluteEpisodePattern;
absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes);
break;
case MultiEpisodeStyle.Scene:
absoluteEpisodePattern += "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern;
break;
case MultiEpisodeStyle.Scene:
formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern;
absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes);
break;
case MultiEpisodeStyle.Range:
formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern;
var eps = new List<Episode> {episodes.First()};
if (episodes.Count > 1) eps.Add(episodes.Last());
absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, eps);
break;
//MultiEpisodeStyle.Extend
default:
absoluteEpisodePattern += "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern;
break;
}
default:
formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern;
absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes);
break;
}
absoluteEpisodePattern = ReplaceAbsoluteNumberTokens(absoluteEpisodePattern, episodes);
var token = String.Format("{{Absolute Pattern{0}}}", index++);
pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, token);
tokenHandlers[token] = m => absoluteEpisodePattern;
@ -554,31 +573,30 @@ namespace NzbDrone.Core.Organizer
return replacementText;
}
private string ReplaceNumberTokens(string pattern, List<Episode> episodes)
private string FormatNumberTokens(string basePattern, string formatPattern, List<Episode> episodes)
{
var episodeIndex = 0;
pattern = EpisodeRegex.Replace(pattern, match =>
var pattern = String.Empty;
for (int i = 0; i < episodes.Count; i++)
{
var episode = episodes[episodeIndex];
episodeIndex++;
var patternToReplace = i == 0 ? basePattern : formatPattern;
return ReplaceNumberToken(match.Groups["episode"].Value, episode.EpisodeNumber);
});
pattern += EpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["episode"].Value, episodes[i].EpisodeNumber));
}
return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber);
}
private string ReplaceAbsoluteNumberTokens(string pattern, List<Episode> episodes)
private string FormatAbsoluteNumberTokens(string basePattern, string formatPattern, List<Episode> episodes)
{
var episodeIndex = 0;
pattern = AbsoluteEpisodeRegex.Replace(pattern, match =>
var pattern = String.Empty;
for (int i = 0; i < episodes.Count; i++)
{
var episode = episodes[episodeIndex];
episodeIndex++;
var patternToReplace = i == 0 ? basePattern : formatPattern;
//TODO: We need to handle this null check somewhere, I think earlier is better...
return ReplaceNumberToken(match.Groups["absolute"].Value, episode.AbsoluteEpisodeNumber.Value);
});
pattern += AbsoluteEpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["absolute"].Value, episodes[i].AbsoluteEpisodeNumber.Value));
}
return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber);
}
@ -684,6 +702,7 @@ namespace NzbDrone.Core.Organizer
Extend = 0,
Duplicate = 1,
Repeat = 2,
Scene = 3
Scene = 3,
Range = 4
}
}

@ -13,6 +13,7 @@ namespace NzbDrone.Core.Organizer
SampleResult GetMultiEpisodeSample(NamingConfig nameSpec);
SampleResult GetDailySample(NamingConfig nameSpec);
SampleResult GetAnimeSample(NamingConfig nameSpec);
SampleResult GetAnimeMultiEpisodeSample(NamingConfig nameSpec);
String GetSeriesFolderSample(NamingConfig nameSpec);
String GetSeasonFolderSample(NamingConfig nameSpec);
}
@ -25,12 +26,14 @@ namespace NzbDrone.Core.Organizer
private static Series _animeSeries;
private static Episode _episode1;
private static Episode _episode2;
private static Episode _episode3;
private static List<Episode> _singleEpisode;
private static List<Episode> _multiEpisodes;
private static EpisodeFile _singleEpisodeFile;
private static EpisodeFile _multiEpisodeFile;
private static EpisodeFile _dailyEpisodeFile;
private static EpisodeFile _animeEpisodeFile;
private static EpisodeFile _animeMultiEpisodeFile;
public FileNameSampleService(IBuildFileNames buildFileNames)
{
@ -71,8 +74,16 @@ namespace NzbDrone.Core.Organizer
AbsoluteEpisodeNumber = 2
};
_episode3 = new Episode
{
SeasonNumber = 1,
EpisodeNumber = 3,
Title = "Episode Title (3)",
AbsoluteEpisodeNumber = 3
};
_singleEpisode = new List<Episode> { _episode1 };
_multiEpisodes = new List<Episode> { _episode1, _episode2 };
_multiEpisodes = new List<Episode> { _episode1, _episode2, _episode3 };
var mediaInfo = new MediaInfoModel()
{
@ -102,8 +113,8 @@ namespace NzbDrone.Core.Organizer
_multiEpisodeFile = new EpisodeFile
{
Quality = new QualityModel(Quality.HDTV720p),
RelativePath = "Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE.mkv",
SceneName = "Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE",
RelativePath = "Series.Title.S01E01-E03.720p.HDTV.x264-EVOLVE.mkv",
SceneName = "Series.Title.S01E01-E03.720p.HDTV.x264-EVOLVE",
ReleaseGroup = "RlsGrp",
MediaInfo = mediaInfo,
};
@ -120,8 +131,17 @@ namespace NzbDrone.Core.Organizer
_animeEpisodeFile = new EpisodeFile
{
Quality = new QualityModel(Quality.HDTV720p),
RelativePath = "Series.Title.001.HDTV.x264-EVOLVE.mkv",
SceneName = "Series.Title.001.HDTV.x264-EVOLVE",
RelativePath = "[RlsGroup] Series Title - 001 [720p].mkv",
SceneName = "[RlsGroup] Series Title - 001 [720p]",
ReleaseGroup = "RlsGrp",
MediaInfo = mediaInfoAnime
};
_animeMultiEpisodeFile = new EpisodeFile
{
Quality = new QualityModel(Quality.HDTV720p),
RelativePath = "[RlsGroup] Series Title - 001 - 103 [720p].mkv",
SceneName = "[RlsGroup] Series Title - 001 - 103 [720p]",
ReleaseGroup = "RlsGrp",
MediaInfo = mediaInfoAnime
};
@ -179,6 +199,19 @@ namespace NzbDrone.Core.Organizer
return result;
}
public SampleResult GetAnimeMultiEpisodeSample(NamingConfig nameSpec)
{
var result = new SampleResult
{
FileName = BuildSample(_multiEpisodes, _animeSeries, _animeMultiEpisodeFile, nameSpec),
Series = _animeSeries,
Episodes = _multiEpisodes,
EpisodeFile = _animeMultiEpisodeFile
};
return result;
}
public string GetSeriesFolderSample(NamingConfig nameSpec)
{
return _buildFileNames.GetSeriesFolder(_standardSeries, nameSpec);

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NzbDrone.Core.Parser.Model;

@ -87,7 +87,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Episodes with airdate
new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])",
new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])(?!\W+[0-3][0-9])",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Supports 1103/1113 naming

@ -13,16 +13,17 @@ define(
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',
namingTokenHelper : '.x-naming-token-helper',
multiEpisodeStyle : '.x-multi-episode-style',
seriesFolderExample : '.x-series-folder-example',
seasonFolderExample : '.x-season-folder-example'
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',
namingTokenHelper : '.x-naming-token-helper',
multiEpisodeStyle : '.x-multi-episode-style',
seriesFolderExample : '.x-series-folder-example',
seasonFolderExample : '.x-season-folder-example'
},
events: {
@ -70,6 +71,7 @@ define(
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'));
},

@ -175,6 +175,7 @@
<option value="1">Duplicate</option>
<option value="2">Repeat</option>
<option value="3">Scene</option>
<option value="4">Range</option>
</select>
</div>
</div>
@ -212,6 +213,14 @@
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Anime Multi-Episode Example</label>
<div class="col-sm-8">
<p class="form-control-static x-anime-multi-episode-example naming-example"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Series Folder Example</label>

Loading…
Cancel
Save