"""Build canonical filenames: ``Title (Region) (tags) (Serial).iso``. Revision (``(v1.03)``), disc (``(Disc 1)``) and edition tags are preserved; language tags (``(En,Fr,De)``) are dropped; leading articles are moved back in front (``Sims 2, The`` -> ``The Sims 2``). """ import re _ILLEGAL = re.compile(r'[<>:"/\\|?*\x00-\x1f]') _TAG_RE = re.compile(r"\s*\(([^()]*)\)\s*$") _ARTICLE_RE = re.compile(r"^(.+),\s*(The|A|An)(?:\s+-\s+(.+))?$", re.IGNORECASE) _LANG_CODES = { "en", "fr", "de", "es", "it", "nl", "pt", "sv", "no", "da", "fi", "ja", "ko", "zh", "pl", "ru", "el", "tr", "cs", "hu", "hi", "ar", "th", } def _is_language_tag(inner): parts = [p.strip().lower() for p in inner.split(",")] return bool(parts) and all(p in _LANG_CODES for p in parts) def _split_tags(name): """Split trailing parenthetical tags; keep non-language tags, drop languages.""" title = name kept = [] while True: m = _TAG_RE.search(title) if not m: break inner = m.group(1).strip() if _is_language_tag(inner): title = title[: m.start()] else: kept.insert(0, f"({inner})") title = title[: m.start()] return title.strip(), kept def region_from_name(name): """Extract the region from a release name (first non-language tag). Used when a DAT leaves the region field empty and embeds it in the name (e.g. ``Oceans 11 (Japan, Europe)``). """ _, kept = _split_tags(str(name)) if not kept: return "" inner = kept[0][1:-1].strip() return inner def _reorder_article(title): m = _ARTICLE_RE.match(title) if m: prefix, article, suffix = m.group(1), m.group(2), m.group(3) result = f"{article} {prefix}" if suffix: result += f" - {suffix}" return result return title def canonical_name(release, serial, ext=".iso"): name = release["name"] region = release.get("region") if region: for r in [x.strip() for x in region.split(",") if x.strip()]: name = re.sub(rf"\s*\(\s*{re.escape(r)}\s*\)", "", name, flags=re.IGNORECASE) title, tags = _split_tags(name) title = _reorder_article(title) parts = [title] if region and region != "World": parts.append(f"({region})") parts.extend(tags) if serial: parts.append(f"({serial})") name = _ILLEGAL.sub(" ", " ".join(parts)).strip() name = re.sub(r"\s+", " ", name) return f"{name}{ext}"