#!/usr/bin/env python3 """gen.py -- emit the guide's JSON from the catalog below (the single source of truth). Run from the repo root: python3 tools/gen.py Rewrites docs/json/depot/** in place. Deterministic: trash_ids are md5("depot-guides:cf:"), so re-running never churns ids, and a renamed CF keeps its id only if its slug is kept too. Regex dialect is Python `re` with IGNORECASE (Depot's matcher), NOT .NET -- this guide feeds Depot, not Radarr, even though it travels over the Radarr-shaped sync protocol. """ import hashlib import json import os ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT = os.path.join(ROOT, "docs", "json", "depot") def tid(kind, slug): return hashlib.md5(("depot-guides:%s:%s" % (kind, slug)).encode()).hexdigest() def spec(name, value, negate=False, required=False): return {"name": name, "implementation": "ReleaseTitleSpecification", "negate": negate, "required": required, "fields": {"value": value}} # slug, name, scores {set: score}, specifications # Names that match Depot's seeded defaults are deliberate: the sync adopts the seeded row in # place (same name = same format), upgrading it to the guide's pattern instead of duplicating it. CFS = [ # ---- PC: source tiers ------------------------------------------------------------------- ("drm-free", "DRM-Free", {"default": 2000, "archival": 2000, "console": 0}, [spec("DRM-free marker", r"\bDRM[ ._-]?Free\b|\bGOG\b")]), ("full-iso", "Full ISO", {"default": 300, "archival": 800, "console": 0}, [spec("Full ISO marker", r"\bFull[ ._-]?ISO\b|\bFULLdvd\d*\b")]), ("portable", "Portable", {"default": 150, "archival": -200, "console": 0}, [spec("Portable / pre-installed", r"\b(portable|pre-?installed)\b")]), ("repack-fitgirl", "Repack: FitGirl", {"default": 1200, "archival": -500, "console": 0}, [spec("FitGirl", r"\bFitGirl\b")]), ("repack-dodi", "Repack: DODI", {"default": 1100, "archival": -500, "console": 0}, [spec("DODI", r"\bDODI\b")]), ("repack-elamigos", "Repack: ElAmigos", {"default": 600, "archival": -500, "console": 0}, [spec("ElAmigos", r"\bElAmigos\b")]), ("repack-kaoskrew", "Repack: KaOsKrew", {"default": 500, "archival": -500, "console": 0}, [spec("KaOsKrew", r"\bKaOsKrew\b")]), ("repack-generic", "Repack", {"default": 100, "archival": -300, "console": 0}, [spec("Repack word", r"\brepack\b"), spec("Not a named repacker (they carry their own format)", r"FitGirl|DODI|ElAmigos|KaOsKrew", negate=True, required=True)]), ("scene-modern", "Scene Release (Modern)", {"default": 400, "archival": 400, "console": 0}, [spec("Modern scene group suffix", r"-(RUNE|TENOKE|EMPRESS|FLT|RAZOR1911|SKIDROW)\b")]), ("scene-legacy", "Scene Release (Legacy)", {"default": 300, "archival": 300, "console": 0}, [spec("Legacy scene group suffix", r"-(CODEX|PLAZA|CPY|HOODLUM|RELOADED|PROPHET|FAiRLiGHT|SiMPLEX|DARKSiDERS|TiNYiSO|DOGE)\b")]), ("scene-fix", "Scene fix (PROPER)", {"default": 50, "archival": 50, "console": 0}, [spec("PROPER / RERIP", r"\b(proper|re-?rip|rerepack)\b")]), # ---- PC: junk and hazards --------------------------------------------------------------- ("rus-first-repack", "Russian-First Repack", {"default": -3000, "archival": -3000, "console": -3000}, [spec("RUS-first repacker", r"\bxatab\b|\bigruha\b|R\.?G\.?[ ._-]?(Mechanics|Catalyst|Freedom)|\bMizantrop\b")]), ("online-fix", "Online-Fix", {"default": -10000, "archival": -10000, "console": -10000}, [spec("Online-Fix", r"online[ ._-]?fix")]), ("portal-rip", "Portal Rip", {"default": -8000, "archival": -8000, "console": -8000}, [spec("Rip-portal branding", r"\bIGG(-?Games)?\b|SteamRIP|apunkagames|oceanofgames|\bFullRip\b")]), ("foreign-only", "Foreign-Language Only", {"default": -5000, "archival": -5000, "console": -5000}, [spec("Single-foreign-language marker", r"\b(RUS|CZE|HUN|TUR|POL)[ ._-]?only\b|\bRUSSiAN\b")]), ("multi-language", "Multi-Language", {"default": 50, "archival": 50, "console": 50}, [spec("Multi-language marker", r"MULTi\d*|Multi[ ._-]?Language")]), ("early-access", "Beta / prototype", {"default": -1500, "archival": -1500, "console": -100}, [spec("Not a release build", r"\b(early[ ._-]?access|beta|proto(type)?|alpha|playtest|preload)\b")]), ("trainer-only", "Trainer only", {"default": -2000, "archival": -2000, "console": -2000}, [spec("Trainer", r"\btrainer\b")]), # ---- Console: dump provenance ----------------------------------------------------------- ("redump", "Redump Verified", {"default": 800, "archival": 1500, "console": 1500}, [spec("Redump set tag", r"\[Redump\b")]), ("no-intro", "No-Intro Verified", {"default": 800, "archival": 1500, "console": 1500}, [spec("No-Intro set tag", r"\[No-Intro\b")]), ("goodtools-verified", "Verified good dump", {"default": 100, "archival": 300, "console": 300}, [spec("GoodTools verified-good", r"\[!\]")]), ("no-intro-region", "No-Intro region tag", {"default": 50, "archival": 100, "console": 100}, [spec("No-Intro naming convention", r"\((USA|Europe|Japan|World)\)")]), ("hacked-dump", "Hacked / trained / cracktro", {"default": -1000, "archival": -1000, "console": -1000}, [spec("Modified dump", r"\[[htob]\]|\bcracktro\b|\btrained\b")]), ("scrubbed", "Scrubbed", {"default": 100, "archival": 0, "console": 100}, [spec("Scrubbed image", r"\bScrubbed\b")]), ("compressed-image", "Compressed Disc Image", {"default": 100, "archival": -100, "console": 100}, [spec("Emulator-native compressed format", r"\b(RVZ|CHD|CSO|WBFS)\b")]), ("nkit", "NKit", {"default": -100, "archival": -300, "console": -100}, [spec("NKit image", r"\bNKit\b")]), ("xiso", "XISO (Xbox)", {"default": 200, "archival": -100, "console": 200}, [spec("XISO-sliced Xbox image", r"\bXISO\b")]), ("region-usa", "Region: USA / Region-Free", {"default": 100, "archival": 100, "console": 100}, [spec("USA / World / region-free marker", r"\bUSA\b|\bWorld\b|Region[ ._-]?Free|\(U\)")]), ("region-japan-only", "Region: Japan Only", {"default": -300, "archival": 0, "console": -300}, [spec("No western-region marker", r"\bUSA\b|\bEurope\b|\bWorld\b|Region[ ._-]?Free|\(U\)|\(E\)", negate=True, required=True), spec("Japan / Asia marker", r"\bJapan\b|\bJPN\b|\bAsia\b|\(J\)")]), ] # Platform-slug size bands, GB (Depot's quality axis; min/max ride the Radarr fields as plain GB). # Depot's built-in defaults stand underneath -- the guide states only what it wants to pin, and a # platform absent here keeps Depot's own band (or stays unconstrained, like pc used to be). QUALITY_SIZE = { "trash_id": tid("quality-size", "depot"), "type": "depot", "qualities": [ {"quality": "pc", "min": 0.005, "preferred": 40, "max": 350}, {"quality": "linux", "min": 0.005, "preferred": 40, "max": 350}, {"quality": "switch", "min": 0.3, "preferred": 8, "max": 35}, {"quality": "wiiu", "min": 2, "preferred": 10, "max": 30}, {"quality": "wii", "min": 0.5, "preferred": 4, "max": 9}, {"quality": "gc", "min": 0.3, "preferred": 1.4, "max": 2.2}, {"quality": "ps3", "min": 1.0, "preferred": 15, "max": 50}, {"quality": "ps2", "min": 0.3, "preferred": 4, "max": 9}, {"quality": "psx", "min": 0.05, "preferred": 0.6, "max": 3.0}, {"quality": "psp", "min": 0.05, "preferred": 1, "max": 3.0}, {"quality": "xbox", "min": 0.8, "preferred": 4, "max": 9.0}, {"quality": "dreamcast", "min": 0.1, "preferred": 1, "max": 2.5}, {"quality": "saturn", "min": 0.05, "preferred": 0.6, "max": 1.6}, {"quality": "segacd", "min": 0.05, "preferred": 0.5, "max": 1.2}, {"quality": "nds", "min": 0.004, "preferred": 0.1, "max": 0.6}, {"quality": "n3ds", "min": 0.02, "preferred": 1, "max": 6.0}, {"quality": "n64", "min": 0.00001, "preferred": 0.03, "max": 0.07}, {"quality": "snes", "min": 0.00001, "preferred": 0.003, "max": 0.02}, {"quality": "genesis", "min": 0.00001, "preferred": 0.002, "max": 0.02}, {"quality": "nes", "min": 0.00001, "preferred": 0.0005, "max": 0.01}, {"quality": "gba", "min": 0.00001, "preferred": 0.008, "max": 0.05}, {"quality": "gbc", "min": 0.00001, "preferred": 0.001, "max": 0.02}, {"quality": "gb", "min": 0.00001, "preferred": 0.0005, "max": 0.02}, {"quality": "mastersystem", "min": 0.00001, "preferred": 0.0005, "max": 0.02}, {"quality": "gamegear", "min": 0.00001, "preferred": 0.0005, "max": 0.02}, ], } # The platform ladders, best first (guide items are written top=best; the sync tool reverses to # the service's wire order itself). allowed=false platforms are the profile's not-wanted list. _DECK_LADDER = ["linux", "pc", "switch", "gc", "wii", "ps2", "psx", "xbox", "psp", "n64", "dreamcast", "nds", "n3ds", "snes", "genesis", "gba", "nes"] _CONSOLE_LADDER = ["switch", "wii", "gc", "ps2", "psx", "xbox", "psp", "n64", "dreamcast", "saturn", "segacd", "nds", "n3ds", "snes", "genesis", "gba", "gbc", "gb", "mastersystem", "gamegear", "nes"] def _format_items(): """Every guide CF, name -> trash_id: Recyclarr expands these into implicit CF sync entries assigned to the profile, so listing a profile in recyclarr.yml is ALL the configuration a user needs -- the formats ride in with it.""" return {name: tid("cf", slug) for slug, name, _scores, _specs in CFS} PROFILES = [ { "trash_id": tid("quality-profile", "deck-ready"), "name": "Deck Ready", "trash_score_set": "default", "upgradeAllowed": True, "cutoff": "pc", "minFormatScore": -100, "cutoffFormatScore": 10000, "minUpgradeFormatScore": 1, "items": [{"name": q, "allowed": True} for q in _DECK_LADDER], }, { "trash_id": tid("quality-profile", "archival"), "name": "Archival", "trash_score_set": "archival", "upgradeAllowed": True, "cutoff": "pc", "minFormatScore": 0, "cutoffFormatScore": 10000, "minUpgradeFormatScore": 1, "items": [{"name": q, "allowed": True} for q in ["pc", "linux"] + _CONSOLE_LADDER], }, { "trash_id": tid("quality-profile", "console-verified"), "name": "Console Verified", "trash_score_set": "console", "upgradeAllowed": True, "cutoff": "switch", "minFormatScore": 800, "cutoffFormatScore": 10000, "minUpgradeFormatScore": 1, "items": ([{"name": q, "allowed": True} for q in _CONSOLE_LADDER] + [{"name": q, "allowed": False} for q in ("pc", "linux", "mac")]), }, ] METADATA = { "$schema": "https://raw.githubusercontent.com/TRaSH-Guides/Guides/master/metadata.schema.json", "json_paths": { "radarr": { "custom_formats": ["docs/json/depot/cf"], "qualities": ["docs/json/depot/quality-size"], "quality_profiles": ["docs/json/depot/quality-profiles"], } }, } def slugfile(name): return "".join(c if c.isalnum() else "-" for c in name.lower()).strip("-") + ".json" def write(path, obj): os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as f: json.dump(obj, f, indent=2, ensure_ascii=False) f.write("\n") def main(): for slug, name, scores, specs in CFS: write(os.path.join(OUT, "cf", slug + ".json"), { "trash_id": tid("cf", slug), "trash_scores": scores, "name": name, "includeCustomFormatWhenRenaming": False, "specifications": specs, }) write(os.path.join(OUT, "quality-size", "depot.json"), QUALITY_SIZE) for p in PROFILES: write(os.path.join(OUT, "quality-profiles", slugfile(p["name"])), dict(p, formatItems=_format_items())) write(os.path.join(ROOT, "metadata.json"), METADATA) print("wrote %d CFs, 1 quality-size, %d profiles + metadata.json under %s" % (len(CFS), len(PROFILES), ROOT)) if __name__ == "__main__": main()