krz/nfogen
A scene-style ASCII .nfo generator.
clone: git clone https://gitbay.org/krz/nfogen.git
8efe76f580129941630216ae920851ca911477a3
verified · cmc
author: Christian Cleberg <hello@cleberg.net> · 2026-08-08T05:33:15Z
README.nfo | 53 +++++++++++--- nfogen.py | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ nfogen.toml | 37 ++++++++++ 3 files changed, 307 insertions(+), 11 deletions(-) @@ -1,13 +1,44 @@ ┌──────────────────────────────────────────────────────────────┐ -│ N F O G E N [ KRZ ] krz.sh │ -└──────────────────────────────────────────────────────────────┘ - -WHAT - nfogen. - -STATUS - placeholder. no code yet. - -┌──────────────────────────────────────────────────────────────┐ -│ krz.sh │ +│ N F O G E N 1 . 0 │ +│ [ KRZ ] krz.sh │ +├──────────────────────────────────────────────────────────────┤ +│ DATE 2026-08-08 │ +│ TYPE APP │ +│ FILES nfogen.py │ +│ URL krz.sh │ +├──────────────────────────────────────────────────────────────┤ +│ NOTES │ +│ │ +│ A scene-style ASCII .nfo generator. A single │ +│ zero-dependency Python file. Builds this very box │ +│ from CLI flags, a config file, prompts, or a mix. │ +│ │ +│ REQUIRES python3 (3.11+ for TOML, else JSON). │ +│ │ +│ USAGE │ +│ nfogen.py --title "nfogen 1.0" --type APP │ +│ nfogen.py -c nfogen.toml --title "nfogen 1.0" │ +│ nfogen.py -i prompts for every field. │ +│ nfogen.py alone prompts when no title given. │ +│ │ +│ ORDER: config file < CLI flags < prompts. │ +│ │ +│ CONFIG: a .toml or .json auto-loaded from │ +│ ./nfogen.toml, ./.nfogen.toml, or the path │ +│ ~/.config/nfogen/config.toml. Holds any field │ +│ default plus group, site, width. Or use -c PATH. │ +│ │ +│ FIELDS (all optional but title): │ +│ core: title date type source size files │ +│ url notes greets │ +│ media: format video audio resolution │ +│ language genre runtime │ +│ │ +│ OPTIONS: -o -c -g -s -w -i --no-input │ +│ │ +│ Generated by nfogen itself. │ +├──────────────────────────────────────────────────────────────┤ +│ GREETS │ +│ │ +│ you the-scene │ └──────────────────────────────────────────────────────────────┘ new file mode 100755 @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""nfogen - a scene-style NFO generator. + +Builds a boxed ASCII .nfo from CLI flags, an optional config file, and +interactive prompts for anything still missing. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import textwrap +from datetime import date +from pathlib import Path + +# --- field schema ------------------------------------------------------------ +# One source of truth. Drives argparse flags, interactive prompts, and render. +# key : internal name / config key / flag name +# label : shown in the rendered nfo (info rows) or as a section header +# help : argparse + prompt help text +# kind : "line" scalar shown as an aligned INFO row +# "list" many values, shown space-joined in its own section +# "block" free text, wrapped in its own section + +FIELDS = [ + ("title", "TITLE", "release title (required)", "line"), + ("date", "DATE", "release date", "line"), + ("type", "TYPE", "release type (MOVIE, TV, APP, ...)", "line"), + ("source", "SOURCE", "source (BluRay, WEB-DL, CD, ...)", "line"), + ("format", "FORMAT", "container / format", "line"), + ("video", "VIDEO", "video codec / details", "line"), + ("audio", "AUDIO", "audio codec / details", "line"), + ("resolution", "RESOLUTION", "resolution", "line"), + ("language", "LANGUAGE", "language(s)", "line"), + ("genre", "GENRE", "genre", "line"), + ("runtime", "RUNTIME", "runtime / duration", "line"), + ("size", "SIZE", "total size", "line"), + ("files", "FILES", "file count / listing", "line"), + ("url", "URL", "reference url (imdb, homepage, ...)", "line"), + ("notes", "NOTES", "free-text notes / description", "block"), + ("greets", "GREETS", "groups to greet (space/comma sep)", "list"), +] +FIELD_KINDS = {k: kind for k, _, _, kind in FIELDS} +INFO_KEYS = [k for k, _, _, kind in FIELDS if kind == "line" and k != "title"] + +DEFAULT_SITE = "krz.sh" +DEFAULT_GROUP = "KRZ" +DEFAULT_WIDTH = 64 +CONFIG_CANDIDATES = [ + Path("nfogen.toml"), + Path(".nfogen.toml"), + Path.home() / ".config" / "nfogen" / "config.toml", +] + + +# --- config ------------------------------------------------------------------ +def load_config(explicit: str | None) -> dict: + """Load defaults from a TOML or JSON file. Returns {} if none found.""" + path = None + if explicit: + path = Path(explicit) + if not path.exists(): + sys.exit(f"nfogen: config not found: {explicit}") + else: + path = next((p for p in CONFIG_CANDIDATES if p.exists()), None) + if path is None: + return {} + + text = path.read_text(encoding="utf-8") + if path.suffix == ".json": + return json.loads(text) + try: + import tomllib + except ModuleNotFoundError: + sys.exit("nfogen: TOML config needs Python 3.11+ (or use a .json file)") + return tomllib.loads(text) + + +# --- interactive ------------------------------------------------------------- +def prompt_missing(data: dict, force_all: bool) -> None: + """Fill fields from stdin. Prompts every field when force_all, else only + the missing ones. Enter keeps the current/blank value.""" + keys = [k for k, *_ in FIELDS] if force_all else \ + [k for k, *_ in FIELDS if not data.get(k)] + if not keys: + return + print("nfogen: interactive mode (blank to skip)\n", file=sys.stderr) + for key in keys: + _, label, help_text, _ = next(f for f in FIELDS if f[0] == key) + current = data.get(key, "") + suffix = f" [{current}]" if current else "" + try: + answer = input(f"{label} ({help_text}){suffix}: ").strip() + except EOFError: + break + if answer: + data[key] = answer + + +# --- rendering --------------------------------------------------------------- +def _split_list(value) -> list[str]: + if isinstance(value, (list, tuple)): + return [str(v).strip() for v in value if str(v).strip()] + return [p for p in str(value).replace(",", " ").split() if p] + + +def render(data: dict, group: str, site: str, width: int) -> str: + inner = width - 4 # borders + one space of padding on each side + top = "┌" + "─" * (width - 2) + "┐" + sep = "├" + "─" * (width - 2) + "┤" + bot = "└" + "─" * (width - 2) + "┘" + + def line(content: str = "") -> str: + return "│ " + content.ljust(inner) + " │" + + def center(content: str) -> str: + return "│ " + content.center(inner) + " │" + + out: list[str] = [top] + + # header: spaced title, then group tag + site + title = str(data.get("title") or "UNTITLED").upper() + spaced = " ".join(title) + heading = spaced if len(spaced) <= inner else title + out.append(center(_clip(heading, inner))) + out.append(center(_clip(f"[ {group} ] {site}", inner))) + + # info rows + rows = [(label, str(data[key])) + for key, label, _, _ in FIELDS + if key in INFO_KEYS and data.get(key)] + if rows: + out.append(sep) + lw = max(len(label) for label, _ in rows) + avail = inner - (2 + lw + 2) + for label, value in rows: + wrapped = textwrap.wrap(value, avail) or [""] + out.append(line(f" {label:<{lw}} {wrapped[0]}")) + for cont in wrapped[1:]: + out.append(line(" " * (2 + lw + 2) + cont)) + + # notes block + notes = str(data.get("notes") or "").strip() + if notes: + out.append(sep) + out.append(line(" NOTES")) + out.append(line()) + for para in notes.splitlines() or [""]: + for wrapped in (textwrap.wrap(para, inner - 4) or [""]): + out.append(line(" " + wrapped)) + + # greets block + greets = _split_list(data.get("greets") or []) + if greets: + out.append(sep) + out.append(line(" GREETS")) + out.append(line()) + for wrapped in textwrap.wrap(" ".join(greets), inner - 4): + out.append(line(" " + wrapped)) + + out.append(bot) + return "\n".join(out) + "\n" + + +def _clip(text: str, width: int) -> str: + return text if len(text) <= width else text[: width - 1] + "…" + + +# --- cli --------------------------------------------------------------------- +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + prog="nfogen", + description="Generate a scene-style ASCII .nfo file.", + ) + p.add_argument("-c", "--config", metavar="PATH", + help="config file (.toml or .json) with field defaults") + p.add_argument("-o", "--output", metavar="PATH", + help="write to PATH instead of stdout") + p.add_argument("-g", "--group", help=f"release group (default {DEFAULT_GROUP})") + p.add_argument("-s", "--site", help=f"site tag (default {DEFAULT_SITE})") + p.add_argument("-w", "--width", type=int, + help=f"box width in chars (default {DEFAULT_WIDTH})") + p.add_argument("-i", "--interactive", action="store_true", + help="prompt for every field") + p.add_argument("--no-input", action="store_true", + help="never prompt, even if fields are missing") + for key, _, help_text, _ in FIELDS: + p.add_argument(f"--{key}", help=help_text) + return p + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + config = load_config(args.config) + + # merge: config defaults, then any CLI flag that was supplied + data = {k: config[k] for k, *_ in FIELDS if k in config and config[k] != ""} + for key, *_ in FIELDS: + val = getattr(args, key) + if val is not None: + data[key] = val + data.setdefault("date", date.today().isoformat()) + + group = args.group or config.get("group") or DEFAULT_GROUP + site = args.site or config.get("site") or DEFAULT_SITE + width = args.width or config.get("width") or DEFAULT_WIDTH + + interactive = sys.stdin.isatty() and not args.no_input + if args.interactive: + prompt_missing(data, force_all=True) + elif not data.get("title") and interactive: + prompt_missing(data, force_all=False) + + if not data.get("title"): + sys.exit("nfogen: a title is required (use --title, a config, or -i)") + + text = render(data, group=group, site=site, width=width) + if args.output: + Path(args.output).write_text(text, encoding="utf-8") + else: + sys.stdout.write(text) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) new file mode 100644 @@ -0,0 +1,37 @@ +group = "KRZ" +site = "krz.sh" +title = "nfogen 1.0" +type = "APP" +files = "nfogen.py" +url = "krz.sh" +greets = ["you", "the-scene"] +notes = """ +A scene-style ASCII .nfo generator. A single +zero-dependency Python file. Builds this very box +from CLI flags, a config file, prompts, or a mix. + +REQUIRES python3 (3.11+ for TOML, else JSON). + +USAGE +nfogen.py --title "nfogen 1.0" --type APP +nfogen.py -c nfogen.toml --title "nfogen 1.0" +nfogen.py -i prompts for every field. +nfogen.py alone prompts when no title given. + +ORDER: config file < CLI flags < prompts. + +CONFIG: a .toml or .json auto-loaded from +./nfogen.toml, ./.nfogen.toml, or the path +~/.config/nfogen/config.toml. Holds any field +default plus group, site, width. Or use -c PATH. + +FIELDS (all optional but title): +core: title date type source size files +url notes greets +media: format video audio resolution +language genre runtime + +OPTIONS: -o -c -g -s -w -i --no-input + +Generated by nfogen itself. +"""