"""Fluffy-compatible modinfo.ini and screenshot discovery.""" from __future__ import annotations from dataclasses import dataclass from pathlib import Path IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp") @dataclass class ModInfo: name: str = "" version: str = "" author: str = "" description: str = "" category: str = "" screenshot: str = "" # as written in the ini (relative) nexus_mod_id: str = "" def parse_modinfo(text: str) -> ModInfo: """Tolerant key=value parser; Fluffy files have no section header, but accept one.""" vals: dict[str, str] = {} for line in text.splitlines(): line = line.strip() if not line or line[0] in ";#[" or "=" not in line: continue k, _, v = line.partition("=") vals[k.strip().lower()] = v.strip() return ModInfo( name=vals.get("name", ""), version=vals.get("version", ""), author=vals.get("author", ""), description=vals.get("description", "").replace("\\n", "\n"), category=vals.get("category", ""), screenshot=vals.get("screenshot", ""), nexus_mod_id=vals.get("nexusid", vals.get("nexus_mod_id", "")), ) def _read_text(path: Path) -> str: raw = path.read_bytes() for enc in ("utf-8-sig", "utf-16", "latin-1"): try: return raw.decode(enc) except UnicodeError: continue return raw.decode("utf-8", "replace") def find_modinfo(root: Path) -> Path | None: """modinfo.ini at the root or one directory down.""" for base in [root, *sorted(p for p in root.iterdir() if p.is_dir())]: for p in base.iterdir(): if p.is_file() and p.name.lower() == "modinfo.ini": return p return None def read_modinfo(root: Path) -> tuple[ModInfo, Path | None]: p = find_modinfo(root) if p is None: return ModInfo(), None try: return parse_modinfo(_read_text(p)), p.parent except OSError: return ModInfo(), None def find_screenshot(base: Path, hint: str = "") -> Path | None: """Explicit `screenshot=` wins, else screenshot.png|jpg in the mod root (Fluffy convention).""" if not base.is_dir(): return None files = {p.name.lower(): p for p in base.iterdir() if p.is_file()} if hint: p = files.get(Path(hint).name.lower()) if p: return p for ext in IMAGE_EXTS: p = files.get("screenshot" + ext) if p: return p return None