Skip to content

Commit 2b4429f

Browse files
committed
Fixed: Erroneously matching Anime 10.5 special as 10.
fixes Sonarr#2868
1 parent 2446c41 commit 2b4429f

File tree

3 files changed

+47
-21
lines changed

3 files changed

+47
-21
lines changed

src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,17 @@ public void should_parse_anime_season_packs(string postTitle, string title, int
148148
result.SeasonNumber.Should().Be(seasonNumber);
149149
}
150150

151+
[TestCase("[HorribleSubs] Goblin Slayer - 10.5 [1080p].mkv", "Goblin Slayer", 10.5)]
152+
public void should_handle_anime_recap_numbering(string postTitle, string title, double specialEpisodeNumber)
153+
{
154+
var result = Parser.Parser.ParseTitle(postTitle);
155+
result.Should().NotBeNull();
156+
result.SeriesTitle.Should().Be(title);
157+
result.AbsoluteEpisodeNumbers.Should().BeEmpty();
158+
result.SpecialAbsoluteEpisodeNumbers.Should().NotBeEmpty();
159+
result.SpecialAbsoluteEpisodeNumbers.Should().BeEquivalentTo(new[] { (decimal)specialEpisodeNumber });
160+
result.FullSeason.Should().BeFalse();
161+
}
162+
151163
}
152164
}

src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class ParsedEpisodeInfo
1313
public int SeasonNumber { get; set; }
1414
public int[] EpisodeNumbers { get; set; }
1515
public int[] AbsoluteEpisodeNumbers { get; set; }
16+
public decimal[] SpecialAbsoluteEpisodeNumbers { get; set; }
1617
public string AirDate { get; set; }
1718
public Language Language { get; set; }
1819
public bool FullSeason { get; set; }
@@ -27,6 +28,7 @@ public ParsedEpisodeInfo()
2728
{
2829
EpisodeNumbers = new int[0];
2930
AbsoluteEpisodeNumbers = new int[0];
31+
SpecialAbsoluteEpisodeNumbers = new decimal[0];
3032
}
3133

3234
public bool IsDaily

src/NzbDrone.Core/Parser/Parser.cs

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using System.IO;
45
using System.Linq;
56
using System.Text.RegularExpressions;
@@ -39,31 +40,31 @@ public static class Parser
3940
RegexOptions.IgnoreCase | RegexOptions.Compiled),
4041

4142
//Anime - [SubGroup] Title Episode Absolute Episode Number ([SubGroup] Series Title Episode 01)
42-
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
43+
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
4344
RegexOptions.IgnoreCase | RegexOptions.Compiled),
4445

4546
//Anime - [SubGroup] Title Absolute Episode Number + Season+Episode
46-
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>\d{2,3}))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+).*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.)",
47+
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+).*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.)",
4748
RegexOptions.IgnoreCase | RegexOptions.Compiled),
4849

4950
//Anime - [SubGroup] Title Season+Episode + Absolute Episode Number
50-
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:(?:_|-|\s|\.)+(?<absoluteepisode>(?<!\d+)\d{2,3}(?!\d+)))+.*?(?<hash>\[\w{8}\])?(?:$|\.)",
51+
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:(?:_|-|\s|\.)+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>\[\w{8}\])?(?:$|\.)",
5152
RegexOptions.IgnoreCase | RegexOptions.Compiled),
5253

5354
//Anime - [SubGroup] Title Season+Episode
5455
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:\s|\.).*?(?<hash>\[\w{8}\])?(?:$|\.)",
5556
RegexOptions.IgnoreCase | RegexOptions.Compiled),
5657

5758
//Anime - [SubGroup] Title with trailing number Absolute Episode Number
58-
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?\d+?)[-_. ]+(?:[-_. ]?(?<absoluteepisode>\d{3}(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
59+
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?\d+?)[-_. ]+(?:[-_. ]?(?<absoluteepisode>\d{3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
5960
RegexOptions.IgnoreCase | RegexOptions.Compiled),
6061

6162
//Anime - [SubGroup] Title - Absolute Episode Number
62-
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[. ]-[. ](?<absoluteepisode>\d{2,3}(?!\d+|[-])))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
63+
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-])))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
6364
RegexOptions.IgnoreCase | RegexOptions.Compiled),
6465

6566
//Anime - [SubGroup] Title Absolute Episode Number
66-
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(?!\d+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
67+
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
6768
RegexOptions.IgnoreCase | RegexOptions.Compiled),
6869

6970
//Multi-episode Repeated (S01E05 - S01E06, 1x05 - 1x06, etc)
@@ -75,19 +76,19 @@ public static class Parser
7576
RegexOptions.IgnoreCase | RegexOptions.Compiled),
7677

7778
//Anime - Title Season EpisodeNumber + Absolute Episode Number [SubGroup]
78-
new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>(?<!\d+)\d{2}(?!\d+)))).+?(?:[-_. ]?(?<absoluteepisode>(?<!\d+)\d{3}(?!\d+)))+.+?\[(?<subgroup>.+?)\](?:$|\.mkv)",
79+
new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>(?<!\d+)\d{2}(?!\d+)))).+?(?:[-_. ]?(?<absoluteepisode>(?<!\d+)\d{3}(\.\d{1,2})?(?!\d+)))+.+?\[(?<subgroup>.+?)\](?:$|\.mkv)",
7980
RegexOptions.IgnoreCase | RegexOptions.Compiled),
8081

8182
//Multi-Episode with a title (S01E05E06, S01E05-06, S01E05 E06, etc) and trailing info in slashes
8283
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))+).+?(?:\[.+?\])(?!\\)",
8384
RegexOptions.IgnoreCase | RegexOptions.Compiled),
8485

8586
//Anime - Title Absolute Episode Number [SubGroup]
86-
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[\w{8}\])?(?:$|\.)",
87+
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(\.\d{1,2})?(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[\w{8}\])?(?:$|\.)",
8788
RegexOptions.IgnoreCase | RegexOptions.Compiled),
8889

8990
//Anime - Title Absolute Episode Number [Hash]
90-
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?[-_. ]+.*?(?<hash>\[\w{8}\])(?:$|\.)",
91+
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?[-_. ]+.*?(?<hash>\[\w{8}\])(?:$|\.)",
9192
RegexOptions.IgnoreCase | RegexOptions.Compiled),
9293

9394
//Episodes with airdate AND season/episode number, capture season/epsiode only
@@ -171,11 +172,11 @@ public static class Parser
171172
RegexOptions.IgnoreCase | RegexOptions.Compiled),
172173

173174
// Anime - Title with season number - Absolute Episode Number (Title S01 - EP14)
174-
new Regex(@"^(?<title>.+?S\d{1,2})[-_. ]{3,}(?:EP)?(?<absoluteepisode>\d{2,3}(?!\d+|[-]))",
175+
new Regex(@"^(?<title>.+?S\d{1,2})[-_. ]{3,}(?:EP)?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))",
175176
RegexOptions.IgnoreCase | RegexOptions.Compiled),
176177

177178
// Anime - French titles with single episode numbers, with or without leading sub group ([RlsGroup] Title - Episode 1)
178-
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+?(?:Episode[-_. ]+?)(?<absoluteepisode>\d{1}(?!\d+))",
179+
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+?(?:Episode[-_. ]+?)(?<absoluteepisode>\d{1}(\.\d{1,2})?(?!\d+))",
179180
RegexOptions.IgnoreCase | RegexOptions.Compiled),
180181

181182
//Season only releases
@@ -230,19 +231,19 @@ public static class Parser
230231

231232
// TODO: THIS ONE
232233
//Anime - Title Absolute Episode Number (e66)
233-
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}))+.*?(?<hash>\[\w{8}\])?(?:$|\.)",
234+
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}(\.\d{1,2})?))+.*?(?<hash>\[\w{8}\])?(?:$|\.)",
234235
RegexOptions.IgnoreCase | RegexOptions.Compiled),
235236

236237
//Anime - Title Episode Absolute Episode Number (Series Title Episode 01)
237-
new Regex(@"^(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
238+
new Regex(@"^(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
238239
RegexOptions.IgnoreCase | RegexOptions.Compiled),
239240

240241
//Anime - Title Absolute Episode Number
241-
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
242+
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
242243
RegexOptions.IgnoreCase | RegexOptions.Compiled),
243244

244245
//Anime - Title {Absolute Episode Number}
245-
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>(?<!\d+)\d{2,3}(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
246+
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
246247
RegexOptions.IgnoreCase | RegexOptions.Compiled),
247248

248249
//Extant, terrible multi-episode naming (extant.10708.hdtv-lol.mp4)
@@ -653,21 +654,32 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle
653654

654655
if (absoluteEpisodeCaptures.Any())
655656
{
656-
var first = Convert.ToInt32(absoluteEpisodeCaptures.First().Value);
657-
var last = Convert.ToInt32(absoluteEpisodeCaptures.Last().Value);
657+
var first = Convert.ToDecimal(absoluteEpisodeCaptures.First().Value, CultureInfo.InvariantCulture);
658+
var last = Convert.ToDecimal(absoluteEpisodeCaptures.Last().Value, CultureInfo.InvariantCulture);
658659

659660
if (first > last)
660661
{
661662
return null;
662663
}
663664

664-
var count = last - first + 1;
665-
result.AbsoluteEpisodeNumbers = Enumerable.Range(first, count).ToArray();
666-
667-
if (matchGroup.Groups["special"].Success)
665+
if ((first % 1) != 0 || (last % 1) != 0)
668666
{
667+
if (absoluteEpisodeCaptures.Count != 1)
668+
return null; // Multiple matches not allowed for specials
669+
670+
result.SpecialAbsoluteEpisodeNumbers = new decimal[] { first };
669671
result.Special = true;
670672
}
673+
else
674+
{
675+
var count = last - first + 1;
676+
result.AbsoluteEpisodeNumbers = Enumerable.Range((int)first, (int)count).ToArray();
677+
678+
if (matchGroup.Groups["special"].Success)
679+
{
680+
result.Special = true;
681+
}
682+
}
671683
}
672684

673685
if (!episodeCaptures.Any() && !absoluteEpisodeCaptures.Any())

0 commit comments

Comments
 (0)