soulsync/core/video/release_parse.py
BoulderBadgeDad a52dda6a7f Video quality: recognize plain WEB + accept resolution-only releases
Two over-rejections that filtered out legit releases as "unknown quality":
- the source parser only matched WEB-DL/WEBDL, not plain "WEB" (very common)
- a release with a known resolution but no recognized source had no tier

Now plain WEB parses as web-dl, and a resolution-only release assumes web so
it lands on a tier instead of being rejected (ffprobe verifies the real quality
after download). Truly quality-less packs still reject. Tests added.
2026-06-21 02:11:40 -07:00

119 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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"\bweb\b", re.I), "web-dl"), # plain "WEB" (very common) — treat as WEB-DL
(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"]