Files
profilarr/services/parser/Core/QualityParser.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

294 lines
11 KiB
C#

using System.Text.RegularExpressions;
namespace Parser.Core;
public static class QualityParser
{
private static readonly Regex SourceRegex = new(@"\b(?:
(?<bluray>M?Blu[-_. ]?Ray|HD[-_. ]?DVD|BD(?!$)|UHD2?BD|BDISO|BDMux|BD25|BD50|BR[-_. ]?DISK)|
(?<webdl>WEB[-_. ]?DL(?:mux)?|AmazonHD|AmazonSD|iTunesHD|MaxdomeHD|NetflixU?HD|WebHD|HBOMaxHD|DisneyHD|[. ]WEB[. ](?:[xh][ .]?26[45]|AVC|HEVC|DDP?5[. ]1)|[. ](?-i:WEB)$|(?:\d{3,4}0p)[-. ](?:Hybrid[-_. ]?)?WEB[-. ]|[-. ]WEB[-. ]\d{3,4}0p|\b\s\/\sWEB\s\/\s\b|(?:AMZN|NF|DP)[. -]WEB[. -](?!Rip))|
(?<webrip>WebRip|Web-Rip|WEBMux)|
(?<hdtv>HDTV)|
(?<bdrip>BDRip|BDLight|HD[-_. ]?DVDRip|UHDBDRip)|
(?<brrip>BRRip)|
(?<dvdr>\d?x?M?DVD-?[R59])|
(?<dvd>DVD(?!-R)|DVDRip|xvidvd)|
(?<dsr>WS[-_. ]DSR|DSR)|
(?<regional>R[0-9]{1}|REGIONAL)|
(?<scr>SCR|SCREENER|DVDSCR|DVDSCREENER)|
(?<ts>TS[-_. ]|TELESYNCH?|HD-TS|HDTS|PDVD|TSRip|HDTSRip)|
(?<tc>TC|TELECINE|HD-TC|HDTC)|
(?<cam>CAMRIP|(?:NEW)?CAM|HD-?CAM(?:Rip)?|HQCAM)|
(?<wp>WORKPRINT|WP)|
(?<pdtv>PDTV)|
(?<sdtv>SDTV)|
(?<tvrip>TVRip)
)(?:\b|$|[ .])",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
private static readonly Regex ResolutionRegex = new(
@"\b(?:(?<R360p>360p)|(?<R480p>480p|480i|640x480|848x480)|(?<R540p>540p)|(?<R576p>576p)|(?<R720p>720p|1280x720|960p)|(?<R1080p>1080p|1920x1080|1440p|FHD|1080i|4kto1080p)|(?<R2160p>2160p|3840x2160|4k[-_. ](?:UHD|HEVC|BD|H\.?265)|(?:UHD|HEVC|BD|H\.?265)[-_. ]4k))\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AlternativeResolutionRegex = new(
@"\b(?<R2160p>UHD)\b|(?<R2160p>\[4K\])",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex RemuxRegex = new(
@"(?:[_. \[]|\d{4}p-|\bHybrid-)(?<remux>(?:(BD|UHD)[-_. ]?)?Remux)\b|(?<remux>(?:(BD|UHD)[-_. ]?)?Remux[_. ]\d{4}p)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ProperRegex = new(@"\b(?<proper>proper)\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex RepackRegex = new(@"\b(?<repack>repack\d?|rerip\d?)\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex VersionRegex = new(
@"\d[-._ ]?v(?<version>\d)[-._ ]|\[v(?<version>\d)\]|repack(?<version>\d)|rerip(?<version>\d)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex RealRegex = new(@"\b(?<real>REAL)\b", RegexOptions.Compiled);
private static readonly Regex RawHDRegex = new(@"\b(?<rawhd>RawHD|Raw[-_. ]HD)\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex BRDISKRegex = new(
@"^(?!.*\b((?<!HD[._ -]|HD)DVD|BDRip|720p|MKV|XviD|WMV|d3g|(BD)?REMUX|^(?=.*1080p)(?=.*HEVC)|[xh][-_. ]?26[45]|German.*[DM]L|((?<=\d{4}).*German.*([DM]L)?)(?=.*\b(AVC|HEVC|VC[-_. ]?1|MVC|MPEG[-_. ]?2)\b))\b)(((?=.*\b(Blu[-_. ]?ray|BD|HD[-_. ]?DVD)\b)(?=.*\b(AVC|HEVC|VC[-_. ]?1|MVC|MPEG[-_. ]?2|BDMV|ISO)\b))|^((?=.*\b(((?=.*\b((.*_)?COMPLETE.*|Dis[ck])\b)(?=.*(Blu[-_. ]?ray|HD[-_. ]?DVD)))|3D[-_. ]?BD|BR[-_. ]?DISK|Full[-_. ]?Blu[-_. ]?ray|^((?=.*((BD|UHD)[-_. ]?(25|50|66|100|ISO)))))))).*",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex CodecRegex = new(
@"\b(?:(?<x264>x264)|(?<h264>h264)|(?<xvidhd>XvidHD)|(?<xvid>X-?vid)|(?<divx>divx))\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AnimeBlurayRegex = new(
@"bd(?:720|1080|2160)|(?<=[-_. (\[])bd(?=[-_. )\]])",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AnimeWebDlRegex = new(
@"\[WEB\]|[\[\(]WEB[ .]",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MPEG2Regex = new(@"\b(?<mpeg2>MPEG[-_. ]?2)\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static QualityResult ParseQuality(string name)
{
var normalizedName = name.Replace('_', ' ').Trim();
var result = new QualityResult();
// Parse revision/modifiers
ParseRevision(name, normalizedName, result);
// Parse resolution
var resolution = ParseResolution(normalizedName);
result.Resolution = resolution;
// Check RawHD
if (RawHDRegex.IsMatch(normalizedName) && !BRDISKRegex.IsMatch(normalizedName))
{
result.Modifier = QualityModifier.RawHD;
return result;
}
// Check source
var sourceMatch = SourceRegex.Match(normalizedName);
var isRemux = RemuxRegex.IsMatch(normalizedName);
var isBRDisk = BRDISKRegex.IsMatch(normalizedName);
var codecMatch = CodecRegex.Match(normalizedName);
if (sourceMatch.Success)
{
if (sourceMatch.Groups["bluray"].Success)
{
result.Source = QualitySource.Bluray;
if (isBRDisk)
{
result.Modifier = QualityModifier.BRDisk;
return result;
}
if (codecMatch.Groups["xvid"].Success || codecMatch.Groups["divx"].Success)
{
result.Resolution = Resolution.R480p;
return result;
}
result.Modifier = isRemux ? QualityModifier.Remux : QualityModifier.None;
if (result.Resolution == Resolution.Unknown)
result.Resolution = Resolution.R720p;
return result;
}
if (sourceMatch.Groups["webdl"].Success)
{
result.Source = QualitySource.WebDL;
if (result.Resolution == Resolution.Unknown)
result.Resolution = Resolution.R480p;
return result;
}
if (sourceMatch.Groups["webrip"].Success)
{
result.Source = QualitySource.WebRip;
if (result.Resolution == Resolution.Unknown)
result.Resolution = Resolution.R480p;
return result;
}
if (sourceMatch.Groups["hdtv"].Success)
{
result.Source = QualitySource.TV;
if (MPEG2Regex.IsMatch(normalizedName))
{
result.Modifier = QualityModifier.RawHD;
}
return result;
}
if (sourceMatch.Groups["bdrip"].Success || sourceMatch.Groups["brrip"].Success)
{
result.Source = QualitySource.Bluray;
if (result.Resolution == Resolution.Unknown)
result.Resolution = Resolution.R480p;
return result;
}
if (sourceMatch.Groups["dvdr"].Success || sourceMatch.Groups["dvd"].Success)
{
result.Source = QualitySource.DVD;
result.Resolution = Resolution.R480p;
return result;
}
if (sourceMatch.Groups["scr"].Success)
{
result.Source = QualitySource.DVD;
result.Resolution = Resolution.R480p;
result.Modifier = QualityModifier.Screener;
return result;
}
if (sourceMatch.Groups["cam"].Success)
{
result.Source = QualitySource.Cam;
return result;
}
if (sourceMatch.Groups["ts"].Success)
{
result.Source = QualitySource.Telesync;
return result;
}
if (sourceMatch.Groups["tc"].Success)
{
result.Source = QualitySource.Telecine;
return result;
}
if (sourceMatch.Groups["wp"].Success)
{
result.Source = QualitySource.Workprint;
return result;
}
if (sourceMatch.Groups["regional"].Success)
{
result.Source = QualitySource.DVD;
result.Resolution = Resolution.R480p;
result.Modifier = QualityModifier.Regional;
return result;
}
if (sourceMatch.Groups["pdtv"].Success || sourceMatch.Groups["sdtv"].Success ||
sourceMatch.Groups["dsr"].Success || sourceMatch.Groups["tvrip"].Success)
{
result.Source = QualitySource.TV;
return result;
}
}
// No source - check remux with resolution
if (isRemux && resolution != Resolution.Unknown)
{
result.Source = QualitySource.Bluray;
result.Modifier = QualityModifier.Remux;
return result;
}
// Anime patterns
if (AnimeBlurayRegex.IsMatch(normalizedName))
{
result.Source = QualitySource.Bluray;
result.Modifier = isRemux ? QualityModifier.Remux : QualityModifier.None;
if (result.Resolution == Resolution.Unknown)
result.Resolution = Resolution.R720p;
return result;
}
if (AnimeWebDlRegex.IsMatch(normalizedName))
{
result.Source = QualitySource.WebDL;
if (result.Resolution == Resolution.Unknown)
result.Resolution = Resolution.R720p;
return result;
}
// Resolution only
if (resolution != Resolution.Unknown && isRemux)
{
result.Source = QualitySource.Bluray;
result.Modifier = QualityModifier.Remux;
}
return result;
}
private static Resolution ParseResolution(string name)
{
var match = ResolutionRegex.Match(name);
var altMatch = AlternativeResolutionRegex.Match(name);
if (!match.Success && !altMatch.Success)
return Resolution.Unknown;
if (match.Groups["R360p"].Success) return Resolution.R360p;
if (match.Groups["R480p"].Success) return Resolution.R480p;
if (match.Groups["R540p"].Success) return Resolution.R540p;
if (match.Groups["R576p"].Success) return Resolution.R576p;
if (match.Groups["R720p"].Success) return Resolution.R720p;
if (match.Groups["R1080p"].Success) return Resolution.R1080p;
if (match.Groups["R2160p"].Success || altMatch.Groups["R2160p"].Success) return Resolution.R2160p;
return Resolution.Unknown;
}
private static void ParseRevision(string name, string normalizedName, QualityResult result)
{
var versionMatch = VersionRegex.Match(normalizedName);
if (versionMatch.Success && versionMatch.Groups["version"].Success)
{
result.Revision.Version = int.Parse(versionMatch.Groups["version"].Value);
}
if (ProperRegex.IsMatch(normalizedName))
{
result.Revision.Version = versionMatch.Success ? result.Revision.Version + 1 : 2;
}
if (RepackRegex.IsMatch(normalizedName))
{
result.Revision.Version = versionMatch.Success ? result.Revision.Version + 1 : 2;
result.Revision.IsRepack = true;
}
var realMatches = RealRegex.Matches(name);
if (realMatches.Count > 0)
{
result.Revision.Real = realMatches.Count;
}
}
}