Files
profilarr/services/parser/Core/EpisodeParser.cs
Sam Chau 5c26d6d7b2 feat(parser): implement C# parser microservice with regex-based title parsing
- Added RegexReplace class for handling regex replacements.
- Created ReleaseGroupParser for extracting release groups from titles.
- Developed TitleParser for parsing movie titles, including editions and IDs.
- Introduced QualitySource, Resolution, QualityModifier enums and QualityResult class for quality metadata.
- Set up Dockerfile and docker-compose for containerized deployment.
- Implemented ASP.NET Core web API for parsing requests.
- Added TypeScript client for interacting with the parser service.
- Enhanced configuration to support dynamic parser service URL.
2025-12-30 10:33:52 +10:30

554 lines
23 KiB
C#

using System.Text.RegularExpressions;
namespace Parser.Core;
public enum ReleaseType
{
Unknown = 0,
SingleEpisode = 1,
MultiEpisode = 2,
SeasonPack = 3
}
public class ParsedEpisodeInfo
{
public string? SeriesTitle { get; set; }
public int SeasonNumber { get; set; }
public int[] EpisodeNumbers { get; set; } = Array.Empty<int>();
public int[] AbsoluteEpisodeNumbers { get; set; } = Array.Empty<int>();
public string? AirDate { get; set; }
public bool FullSeason { get; set; }
public bool IsPartialSeason { get; set; }
public bool IsMultiSeason { get; set; }
public bool IsSeasonExtra { get; set; }
public bool IsSplitEpisode { get; set; }
public bool IsMiniSeries { get; set; }
public bool Special { get; set; }
public int SeasonPart { get; set; }
public int? DailyPart { get; set; }
public bool IsDaily => !string.IsNullOrWhiteSpace(AirDate);
public bool IsAbsoluteNumbering => AbsoluteEpisodeNumbers.Any();
public ReleaseType ReleaseType
{
get
{
if (EpisodeNumbers.Length > 1 || AbsoluteEpisodeNumbers.Length > 1)
{
return ReleaseType.MultiEpisode;
}
if (EpisodeNumbers.Length == 1 || AbsoluteEpisodeNumbers.Length == 1)
{
return ReleaseType.SingleEpisode;
}
if (FullSeason)
{
return ReleaseType.SeasonPack;
}
return ReleaseType.Unknown;
}
}
}
public static class EpisodeParser
{
private static readonly Regex[] ReportTitleRegex = new[]
{
// Daily episode with year in series title and air time after date (Plex DVR format)
new Regex(@"^^(?<title>.+?\((?<titleyear>\d{4})\))[-_. ]+(?<airyear>19[4-9]\d|20\d\d)(?<sep>[-_]?)(?<airmonth>0\d|1[0-2])\k<sep>(?<airday>[0-2]\d|3[01])[-_. ]\d{2}[-_. ]\d{2}[-_. ]\d{2}",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Daily episodes without title (2018-10-12, 20181012)
new Regex(@"^(?<airyear>19[6-9]\d|20\d\d)(?<sep>[-_]?)(?<airmonth>0\d|1[0-2])\k<sep>(?<airday>[0-2]\d|3[01])(?!\d)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-Part episodes without a title (S01E05.S01E06)
new Regex(@"^(?:\W*S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:e{1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-Part episodes without a title (1x05.1x06)
new Regex(@"^(?:\W*(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:x{1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes without a title, Multi (S01E04E05, 1x04x05, etc)
new Regex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_]|[ex]){1,2}(?<episode>\d{2,3}(?!\d+))){2,})",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Split episodes (S01E05a, S01E05b, etc)
new Regex(@"^(?<title>.+?)(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_ ]?[ex])(?<episode>\d{2,3}(?!\d+))(?<splitepisode>[a-d])(?:[ _.])))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes without a title, Single (S01E05, 1x05)
new Regex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_ ]?[ex])(?<episode>\d{2,3}(?!\d+))))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - [SubGroup] Title Absolute (Season+Episode)
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+))(?:[-_. ])+\((?:S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+))))(?:v\d+)?(?:\)(?!\d+)).*?(?<hash>[(\[]\w{8}[)\]])?$",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - [SubGroup] Title Season+Episode
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:v\d+)?(?:[_. ](?!\d+)).*?(?<hash>[(\[]\w{8}[)\]])?$",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - [SubGroup] Title Episode Absolute Episode Number
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>[(\[]\w{8}[)\]])?$",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - [SubGroup] Title with trailing number Absolute Episode Number
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - [SubGroup] Title Absolute Episode Number
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-episode Repeated (S01E05 - S01E06)
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:e|[-_. ]e){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-episode Repeated (1x05 - 1x06)
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:x{1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-episode with title (S01E99-100, S01E05-06)
new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))E(?<episode>\d{2,3}(?!\d+))(?:-(?<episode>\d{2,3}(?!\d+)))+(?:[-_. ]|$)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, etc)
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+)))*)(?:[-_. ]|$)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes with a title, 4 digit season number (S2016E05, etc)
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S(?<season>(?<!\d+)(?:\d{4})(?!\d+))(?:e|\We|_){1,2}(?<episode>\d{2,4}(?!\d+))(?:(?:\-|e|\We|_){1,2}(?<episode>\d{2,3}(?!\d+)))*)\W?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-season pack
new Regex(@"^(?<title>.+?)(Complete Series)?[-_. ]+(?:S|(?:Season|Saison|Series|Stagione)[_. ])(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:[-_. ]{1}|[-_. ]{3})(?:S|(?:Season|Saison|Series|Stagione)[_. ])?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Partial season pack
new Regex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:(?:Part|Vol)\W?|(?<!\d+\W+)e|p)(?<seasonpart>\d{1,2}(?!\d+)))+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Season only releases followed by year
new Regex(@"^(?<title>.+?)[-_. ]+?(?:S|Season|Saison|Series|Stagione)[-_. ]?(?<season>\d{1,2}(?=[-_. ]\d{4}[-_. ]+))(?<extras>EXTRAS|SUBPACK)?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Season only releases
new Regex(@"^(?<title>.+?)[-_. ]+?(?:S|Season|Saison|Series|Stagione)[-_. ]?(?<season>\d{1,2}(?![-_. ]?\d+))(?:[-_. ]|$)+(?<extras>EXTRAS|SUBPACK)?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// 4 digit season only releases
new Regex(@"^(?<title>.+?)[-_. ]+?(?:S|Season|Saison|Series|Stagione)[-_. ]?(?<season>\d{4}(?![-_. ]?\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Mini-Series with year in title (Part01, Part 01, Part.1)
new Regex(@"^(?<title>.+?\d{4})(?:\W+(?:(?:Part\W?|e)(?<episode>\d{1,2}(?!\d+)))+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Mini-Series (E1-E2)
new Regex(@"^(?<title>.+?)(?:[-._ ][e])(?<episode>\d{2,3}(?!\d+))(?:(?:\-?[e])(?<episode>\d{2,3}(?!\d+)))+",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes with airdate (2018.04.28)
new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})[-_. ]+(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])(?![-_. ]+[0-3][0-9])",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes with airdate (04.28.2018)
new Regex(@"^(?<title>.+?)?\W*(?<ambiguousairmonth>[0-1][0-9])[-_. ]+(?<ambiguousairday>[0-3][0-9])[-_. ]+(?<airyear>\d{4})(?!\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes with airdate (20180428)
new Regex(@"^(?<title>.+?)?\W*(?<!\d+)(?<airyear>\d{4})(?<airmonth>[0-1][0-9])(?<airday>[0-3][0-9])(?!\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Mini-Series (Part01, Part 01, Part.1)
new Regex(@"^(?<title>.+?)(?:\W+(?:(?:(?<!\()Part\W?|(?<!\d+\W+)e)(?<episode>\d{1,2}(?!\d+|\))))+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Mini-Series (Part One/Two/Three/...Nine)
new Regex(@"^(?<title>.+?)(?:\W+(?:Part[-._ ](?<episode>One|Two|Three|Four|Five|Six|Seven|Eight|Nine)(?>[-._ ])))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Mini-Series (XofY)
new Regex(@"^(?<title>.+?)(?:\W+(?:(?<episode>(?<!\d+)\d{1,2}(?!\d+))of\d+)+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Supports Season 01 Episode 03
new Regex(@"(?:.*(?:\""|^))(?<title>.*?)(?:[-_\W](?<![()\[]))+(?:\W?Season\W?)(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)+(?:Episode\W)(?:[-_. ]?(?<episode>(?<!\d+)\d{1,2}(?!\d+)))+",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-episode with single episode numbers (S6.E1-E2, S6.E1E2, S6E1E2, etc)
new Regex(@"^(?<title>.+?)[-_. ]S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:[-_. ]?[ex]?(?<episode>(?<!\d+)\d{1,2}(?!\d+)))+",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Single episode season or episode S1E1 or S1-E1 or S1.Ep1
new Regex(@"(?:.*(?:\""|^))(?<title>.*?)(?:\W?|_)S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?Ep?[ ._]?(?<episode>(?<!\d+)\d{1,2}(?!\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Supports 103/113 naming
new Regex(@"^(?<title>.+?)?(?:(?:[_.-](?<![()\[!]))+(?<season>(?<!\d+)[1-9])(?<episode>[1-9][0-9]|[0][1-9])(?![a-z]|\d+))+(?:[_.]|$)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - Title Absolute Episode Number
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime OVA special
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[-_. ]+(?<special>special|ova|ovd)).*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)",
RegexOptions.IgnoreCase | RegexOptions.Compiled)
};
private static readonly Regex[] RejectHashedReleasesRegex = new Regex[]
{
new Regex(@"^[0-9a-zA-Z]{32}", RegexOptions.Compiled),
new Regex(@"^[a-z0-9]{24}$", RegexOptions.Compiled),
new Regex(@"^[A-Z]{11}\d{3}$", RegexOptions.Compiled),
new Regex(@"^[a-z]{12}\d{3}$", RegexOptions.Compiled),
new Regex(@"^Backup_\d{5,}S\d{2}-\d{2}$", RegexOptions.Compiled),
new Regex(@"^123$", RegexOptions.Compiled),
new Regex(@"^abc$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"^abc[-_. ]xyz", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"^b00bs$", RegexOptions.Compiled | RegexOptions.IgnoreCase)
};
private static readonly Regex ReversedTitleRegex = new(
@"(?:^|[-._ ])(p027|p0801|\d{2,3}E\d{2}S)[-._ ]",
RegexOptions.Compiled);
private static readonly RegexReplace SimpleTitleRegex = new(
@"(?:(480|540|576|720|1080|1440|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(?<![a-f0-9])(8|10)[ -]?(b(?![a-z0-9])|bit))\s*?",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CleanQualityBracketsRegex = new(
@"\[[a-z0-9 ._-]+\]$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SixDigitAirDateRegex = new(
@"(?<=[_.-])(?<airdate>(?<!\d)(?<airyear>[1-9]\d{1})(?<airmonth>[0-1][0-9])(?<airday>[0-3][0-9]))(?=[_.-])",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex RequestInfoRegex = new(
@"^(?:\[.+?\])+",
RegexOptions.Compiled);
private static readonly string[] Numbers = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };
public static ParsedEpisodeInfo? ParseTitle(string title)
{
try
{
if (!ValidateBeforeParsing(title))
{
return null;
}
if (ReversedTitleRegex.IsMatch(title))
{
var titleWithoutExtension = ParserCommon.RemoveFileExtension(title).ToCharArray();
Array.Reverse(titleWithoutExtension);
title = $"{new string(titleWithoutExtension)}{title.Substring(titleWithoutExtension.Length)}";
}
var releaseTitle = ParserCommon.RemoveFileExtension(title);
releaseTitle = releaseTitle.Replace("【", "[").Replace("】", "]");
foreach (var replace in ParserCommon.PreSubstitutionRegex)
{
if (replace.TryReplace(ref releaseTitle))
{
break;
}
}
var simpleTitle = SimpleTitleRegex.Replace(releaseTitle);
simpleTitle = ParserCommon.WebsitePrefixRegex.Replace(simpleTitle);
simpleTitle = ParserCommon.WebsitePostfixRegex.Replace(simpleTitle);
simpleTitle = ParserCommon.CleanTorrentSuffixRegex.Replace(simpleTitle);
simpleTitle = CleanQualityBracketsRegex.Replace(simpleTitle, string.Empty);
// Handle 6-digit air dates (YYMMDD)
var sixDigitAirDateMatch = SixDigitAirDateRegex.Match(simpleTitle);
if (sixDigitAirDateMatch.Success)
{
var airYear = sixDigitAirDateMatch.Groups["airyear"].Value;
var airMonth = sixDigitAirDateMatch.Groups["airmonth"].Value;
var airDay = sixDigitAirDateMatch.Groups["airday"].Value;
if (airMonth != "00" || airDay != "00")
{
var fixedDate = $"20{airYear}.{airMonth}.{airDay}";
simpleTitle = simpleTitle.Replace(sixDigitAirDateMatch.Groups["airdate"].Value, fixedDate);
}
}
foreach (var regex in ReportTitleRegex)
{
var match = regex.Matches(simpleTitle);
if (match.Count != 0)
{
var result = ParseMatchCollection(match, releaseTitle);
if (result != null)
{
if (result.FullSeason && releaseTitle.Contains("Special", StringComparison.OrdinalIgnoreCase))
{
result.FullSeason = false;
result.Special = true;
}
return result;
}
}
}
}
catch
{
// Parsing failed
}
return null;
}
private static ParsedEpisodeInfo? ParseMatchCollection(MatchCollection matchCollection, string releaseTitle)
{
var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' ').Replace('_', ' ');
seriesName = RequestInfoRegex.Replace(seriesName, "").Trim(' ');
int.TryParse(matchCollection[0].Groups["airyear"].Value, out var airYear);
ParsedEpisodeInfo result;
if (airYear < 1900)
{
result = new ParsedEpisodeInfo
{
EpisodeNumbers = Array.Empty<int>(),
AbsoluteEpisodeNumbers = Array.Empty<int>()
};
foreach (Match matchGroup in matchCollection)
{
var episodeCaptures = matchGroup.Groups["episode"].Captures.Cast<Capture>().ToList();
var absoluteEpisodeCaptures = matchGroup.Groups["absoluteepisode"].Captures.Cast<Capture>().ToList();
if (episodeCaptures.Any())
{
var first = ParseNumber(episodeCaptures.First().Value);
var last = ParseNumber(episodeCaptures.Last().Value);
if (first > last)
{
return null;
}
var count = last - first + 1;
result.EpisodeNumbers = Enumerable.Range(first, count).ToArray();
if (matchGroup.Groups["special"].Success)
{
result.Special = true;
}
if (matchGroup.Groups["splitepisode"].Success)
{
result.IsSplitEpisode = true;
}
}
if (absoluteEpisodeCaptures.Any())
{
var first = ParseDecimal(absoluteEpisodeCaptures.First().Value);
var last = ParseDecimal(absoluteEpisodeCaptures.Last().Value);
if (first > last)
{
return null;
}
if ((first % 1) != 0 || (last % 1) != 0)
{
// Special episode with decimal
result.Special = true;
}
else
{
var count = last - first + 1;
result.AbsoluteEpisodeNumbers = Enumerable.Range((int)first, (int)count).ToArray();
if (matchGroup.Groups["special"].Success)
{
result.Special = true;
}
}
}
if (!episodeCaptures.Any() && !absoluteEpisodeCaptures.Any())
{
if (!string.IsNullOrWhiteSpace(matchCollection[0].Groups["extras"].Value))
{
result.IsSeasonExtra = true;
}
var seasonPart = matchCollection[0].Groups["seasonpart"].Value;
if (!string.IsNullOrWhiteSpace(seasonPart))
{
result.SeasonPart = Convert.ToInt32(seasonPart);
result.IsPartialSeason = true;
}
else if (matchCollection[0].Groups["special"].Success)
{
result.Special = true;
}
else
{
result.FullSeason = true;
}
}
}
var seasons = new List<int>();
foreach (Capture seasonCapture in matchCollection[0].Groups["season"].Captures)
{
if (int.TryParse(seasonCapture.Value, out var parsedSeason))
{
seasons.Add(parsedSeason);
}
}
if (seasons.Distinct().Count() > 1)
{
result.IsMultiSeason = true;
}
if (seasons.Any())
{
result.SeasonNumber = seasons.First();
}
else if (!result.AbsoluteEpisodeNumbers.Any() && result.EpisodeNumbers.Any())
{
result.SeasonNumber = 1;
result.IsMiniSeries = true;
}
}
else
{
// Daily show with air date
var airmonth = 0;
var airday = 0;
if (matchCollection[0].Groups["ambiguousairmonth"].Success &&
matchCollection[0].Groups["ambiguousairday"].Success)
{
var ambiguousAirMonth = Convert.ToInt32(matchCollection[0].Groups["ambiguousairmonth"].Value);
var ambiguousAirDay = Convert.ToInt32(matchCollection[0].Groups["ambiguousairday"].Value);
if (ambiguousAirDay <= 12 && ambiguousAirMonth <= 12)
{
// Ambiguous date
return null;
}
airmonth = ambiguousAirMonth;
airday = ambiguousAirDay;
}
else
{
airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value);
airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value);
}
// Swap day and month if month is bigger than 12
if (airmonth > 12)
{
(airday, airmonth) = (airmonth, airday);
}
try
{
var airDate = new DateTime(airYear, airmonth, airday);
if (airDate > DateTime.Now.AddDays(1).Date || airDate < new DateTime(1970, 1, 1))
{
return null;
}
result = new ParsedEpisodeInfo
{
AirDate = airDate.ToString("yyyy-MM-dd")
};
var partMatch = matchCollection[0].Groups["part"];
if (partMatch.Success)
{
result.DailyPart = Convert.ToInt32(partMatch.Value);
}
}
catch
{
return null;
}
}
result.SeriesTitle = seriesName;
return result;
}
private static bool ValidateBeforeParsing(string title)
{
if (title.ToLower().Contains("password") && title.ToLower().Contains("yenc"))
{
return false;
}
if (!title.Any(char.IsLetterOrDigit))
{
return false;
}
var titleWithoutExtension = ParserCommon.RemoveFileExtension(title);
if (RejectHashedReleasesRegex.Any(v => v.IsMatch(titleWithoutExtension)))
{
return false;
}
return true;
}
private static int ParseNumber(string value)
{
if (int.TryParse(value, out var number))
{
return number;
}
number = Array.IndexOf(Numbers, value.ToLower());
if (number != -1)
{
return number;
}
throw new FormatException($"{value} isn't a number");
}
private static decimal ParseDecimal(string value)
{
if (decimal.TryParse(value, System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var number))
{
return number;
}
throw new FormatException($"{value} isn't a number");
}
}