diff --git a/core/video/release_parse.py b/core/video/release_parse.py new file mode 100644 index 00000000..a0306437 --- /dev/null +++ b/core/video/release_parse.py @@ -0,0 +1,118 @@ +"""Parse a scene/p2p release title into structured quality + scope fields. + +This is the video equivalent of Sonarr/Radarr's release parser: given a raw name +like ``The Wire S02 1080p BluRay x265 DDP5.1-GROUP`` it pulls out resolution, +source, codec, HDR, audio, group, repack/proper, and the SEASON/EPISODE scope +(single episode vs season pack vs complete-series pack). The download engine and +the search UI both rely on this to validate that a hit actually matches what was +searched (a season search must return the *whole* season, etc.). + +Pure + regex-only (no DB, no network) so it's unit-tested in isolation. Isolated — +imports only re/typing; the music side never imports it. +""" + +from __future__ import annotations + +import re +from typing import Any + +# Resolution — first match wins (most specific first). +_RES = [ + (re.compile(r"\b(2160p|4k|uhd)\b", re.I), "2160p"), + (re.compile(r"\b1080[pi]\b", re.I), "1080p"), + (re.compile(r"\b720[pi]\b", re.I), "720p"), + (re.compile(r"\b(480[pi]|576[pi])\b", re.I), "480p"), +] +# Source — order matters (remux before bluray; web-dl before webrip). +_SOURCE = [ + (re.compile(r"\bremux\b", re.I), "remux"), + (re.compile(r"\b(blu-?ray|bdrip|brrip|bd25|bd50)\b", re.I), "bluray"), + (re.compile(r"\b(web-?dl|web\.?dl|amzn|nf|dsnp|hmax|atvp)\b", re.I), "web-dl"), + (re.compile(r"\bweb-?rip\b", re.I), "webrip"), + (re.compile(r"\bhdtv\b", re.I), "hdtv"), + (re.compile(r"\b(dvdrip|dvd)\b", re.I), "dvd"), + (re.compile(r"\b(cam|hdcam|ts|telesync|hdts)\b", re.I), "cam"), + (re.compile(r"\b(scr|screener|dvdscr|bdscr)\b", re.I), "screener"), + (re.compile(r"\bworkprint\b", re.I), "workprint"), +] +_CODEC = [ + (re.compile(r"\b(x265|h\.?265|hevc)\b", re.I), "hevc"), + (re.compile(r"\b(x264|h\.?264|avc)\b", re.I), "x264"), + (re.compile(r"\bav1\b", re.I), "av1"), +] +_HDR = [ + (re.compile(r"\b(dolby\s?vision|do?vi|\bdv\b)\b", re.I), "dv"), + (re.compile(r"\bhdr10\+\b", re.I), "hdr10"), + (re.compile(r"\bhdr10\b", re.I), "hdr10"), + (re.compile(r"\bhdr\b", re.I), "hdr"), +] +_AUDIO = [ + (re.compile(r"\batmos\b", re.I), "atmos"), + (re.compile(r"\b(truehd|true-hd)\b", re.I), "truehd"), + (re.compile(r"\b(dts-?hd|dts-?x)\b", re.I), "dts-hd"), + (re.compile(r"\bdts\b", re.I), "dts"), + (re.compile(r"\b(ddp|eac3|dd\+)\b", re.I), "eac3"), + (re.compile(r"\b(ac3|dd5\.?1|dd2\.?0)\b", re.I), "ac3"), + (re.compile(r"\baac\b", re.I), "aac"), +] + +_RANGE = re.compile(r"\bS(\d{1,2})\s*[-–]\s*S?(\d{1,2})\b", re.I) # S01-S05 +_SXXEXX = re.compile(r"\bS(\d{1,2})[\s.]?E(\d{1,3})\b", re.I) # S02E03 +_SXX = re.compile(r"\bS(\d{1,2})\b", re.I) # S02 (pack) +_SEASON_WORD = re.compile(r"\bseason[\s.]?(\d{1,2})\b", re.I) # Season 2 +_COMPLETE = re.compile(r"\b(complete|collection|all\s?seasons)\b", re.I) +_GROUP = re.compile(r"-([A-Za-z0-9]{2,})\s*$") + + +def _first(table, text) -> Any: + for rx, val in table: + if rx.search(text): + return val + return None + + +def parse_release(title: Any) -> dict: + """Parse a release name into quality + scope fields. Never raises.""" + t = str(title or "") + out = { + "title": t, + "resolution": _first(_RES, t), + "source": _first(_SOURCE, t), + "codec": _first(_CODEC, t), + "hdr": _first(_HDR, t), + "audio": _first(_AUDIO, t), + "group": None, + "repack": bool(re.search(r"\brepack\b", t, re.I)), + "proper": bool(re.search(r"\bproper\b", t, re.I)), + "three_d": bool(re.search(r"\b3d\b", t, re.I)), + "season": None, + "season_end": None, + "episode": None, + "is_season_pack": False, + "is_series_pack": False, + } + g = _GROUP.search(t) + if g: + out["group"] = g.group(1) + + rng = _RANGE.search(t) + m = _SXXEXX.search(t) + if rng: + out["season"] = int(rng.group(1)) + out["season_end"] = int(rng.group(2)) + out["is_series_pack"] = True + elif m: + out["season"] = int(m.group(1)) + out["episode"] = int(m.group(2)) + else: + sm = _SXX.search(t) or _SEASON_WORD.search(t) + if sm: + out["season"] = int(sm.group(1)) + out["is_season_pack"] = True + if _COMPLETE.search(t): + out["is_series_pack"] = True + out["is_season_pack"] = False + return out + + +__all__ = ["parse_release"] diff --git a/tests/test_video_release_parse.py b/tests/test_video_release_parse.py new file mode 100644 index 00000000..94cefc26 --- /dev/null +++ b/tests/test_video_release_parse.py @@ -0,0 +1,65 @@ +"""Release-title parser — the Sonarr-style seam that pulls quality + season/episode +scope out of a raw release name. Isolated from music.""" + +from __future__ import annotations + +from core.video.release_parse import parse_release + + +def test_movie_release(): + p = parse_release("The Matrix 1999 2160p UHD BluRay x265 TrueHD Atmos-FraMeSToR") + assert p["resolution"] == "2160p" and p["source"] == "bluray" + assert p["codec"] == "hevc" and p["audio"] == "atmos" + assert p["group"] == "FraMeSToR" + assert p["season"] is None and p["episode"] is None + assert not p["is_season_pack"] and not p["is_series_pack"] + + +def test_single_episode(): + p = parse_release("The.Wire.S02E03.1080p.WEB-DL.x264-GROUP") + assert p["season"] == 2 and p["episode"] == 3 + assert p["source"] == "web-dl" and p["codec"] == "x264" + assert not p["is_season_pack"] and not p["is_series_pack"] + + +def test_season_pack_no_episode(): + p = parse_release("The Wire S02 1080p BluRay x265 DDP5.1-GROUP") + assert p["season"] == 2 and p["episode"] is None + assert p["is_season_pack"] is True and p["is_series_pack"] is False + + +def test_season_word_pack(): + p = parse_release("The.Wire.Season.2.COMPLETE.720p.HDTV.x264") + assert p["season"] == 2 + # "COMPLETE" + a single season word → treated as a series/complete pack + assert p["is_series_pack"] is True + + +def test_series_range_pack(): + p = parse_release("The Wire S01-S05 COMPLETE 1080p BluRay x265-GROUP") + assert p["season"] == 1 and p["season_end"] == 5 + assert p["is_series_pack"] is True + + +def test_hdr_and_dv(): + assert parse_release("Dune 2021 2160p BluRay HDR10 x265")["hdr"] == "hdr10" + assert parse_release("Dune 2021 2160p WEB-DL DV HEVC")["hdr"] == "dv" + assert parse_release("Dune 2021 1080p BluRay x264")["hdr"] is None + + +def test_junk_sources_detected_for_rejection(): + assert parse_release("Some Movie 2024 HDCAM x264")["source"] == "cam" + assert parse_release("Some Movie 2024 DVDScr XviD")["source"] == "screener" + assert parse_release("Some.Movie.2024.1080p.WEB-DL.x264.3D")["three_d"] is True + + +def test_repack_proper(): + p = parse_release("Show.S01E01.REPACK.1080p.WEB-DL.x265-GRP") + assert p["repack"] is True + assert parse_release("Show.S01E01.PROPER.1080p.HDTV.x264")["proper"] is True + + +def test_garbage_never_raises(): + p = parse_release(None) + assert p["resolution"] is None and p["title"] == "" + assert parse_release(12345)["title"] == "12345"