krz/nfogen
A scene-style ASCII .nfo generator.
clone: git clone https://gitbay.org/krz/nfogen.git
907e55cc50ea05d1c0a317c2c8008a58abc5bb32
verified · cmc
author: Christian Cleberg <hello@cleberg.net> · 2026-08-08T05:44:03Z
.gitattributes | 2 + README.nfo | 7 +- examples/the-midnight-cartographer-2026.cp437.nfo | 42 ++++++++++++ examples/the-midnight-cartographer-2026.nfo | 78 ++++++++++++---------- examples/the-midnight-cartographer-2026.toml | 7 +- nfogen.py | 81 ++++++++++++++++++++--- nfogen.toml | 9 ++- 7 files changed, 175 insertions(+), 51 deletions(-) new file mode 100644 @@ -0,0 +1,2 @@ +# CP437-encoded nfos hold high bytes; keep git from normalizing them. +*.cp437.nfo binary @@ -34,7 +34,12 @@ │ media: format video audio resolution │ │ language genre runtime │ │ │ -│ OPTIONS: -o -c -g -s -w -i --no-input │ +│ STYLES: single, double, block (--style) │ +│ ENCODING: utf8 or cp437 (--encoding cp437 = DOS) │ +│ BANNERS: --presents --footer scene boilerplate │ +│ │ +│ OPTIONS: -o -c -g -s -w --style --encoding │ +│ --presents --footer -i --no-input │ │ │ │ Generated by nfogen itself. │ ├──────────────────────────────────────────────────────────────┤ new file mode 100644 @@ -0,0 +1,42 @@ +ͻ + THE.MIDNIGHT.CARTOGRAPHER.2026.1080P.BLURAY.X264-KRZ + [ KRZ ] krz.sh + -={ KRZ proudly presents }=- + + DATE 2026-08-08 + TYPE MOVIE + SOURCE BluRay (Retail) + FORMAT Matroska (MKV) + VIDEO x264 @ 8500 kbps, 23.976 fps + AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) + RESOLUTION 1920x1080p + LANGUAGE English (subs: EN, ES, FR) + GENRE Drama / Mystery + RUNTIME 1h 52m + SIZE 8.42 GiB + FILES 42 x 200 MB + URL imdb.com/title/tt9999999 + + NOTES + + A quiet cartographer maps a city that rearranges itself + after midnight. Encoded from retail BluRay, untouched + audio, nothing re-encoded twice. + + Source : COMPLETE.BLURAY (thanks to the supplier) + Encoder : x264 10-bit, --preset veryslow --crf 18 + Chapters : yes, named + Subtitles: muxed, forced flags set where needed + + Nothing to prove, just a clean encode. Fix it yourself + if you can do better. + + GREETS + + the-scene 0day our-suppliers you + + + SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE + if you enjoyed this release, buy it! + +ͼ @@ -1,36 +1,42 @@ -┌──────────────────────────────────────────────────────────────┐ -│ THE.MIDNIGHT.CARTOGRAPHER.2026.1080P.BLURAY.X264-KRZ │ -│ [ KRZ ] krz.sh │ -├──────────────────────────────────────────────────────────────┤ -│ DATE 2026-08-08 │ -│ TYPE MOVIE │ -│ SOURCE BluRay (Retail) │ -│ FORMAT Matroska (MKV) │ -│ VIDEO x264 @ 8500 kbps, 23.976 fps │ -│ AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) │ -│ RESOLUTION 1920x1080p │ -│ LANGUAGE English (subs: EN, ES, FR) │ -│ GENRE Drama / Mystery │ -│ RUNTIME 1h 52m │ -│ SIZE 8.42 GiB │ -│ FILES 42 x 200 MB │ -│ URL imdb.com/title/tt9999999 │ -├──────────────────────────────────────────────────────────────┤ -│ NOTES │ -│ │ -│ A quiet cartographer maps a city that rearranges itself │ -│ after midnight. Encoded from retail BluRay, untouched │ -│ audio, nothing re-encoded twice. │ -│ │ -│ Source : COMPLETE.BLURAY (thanks to the supplier) │ -│ Encoder : x264 10-bit, --preset veryslow --crf 18 │ -│ Chapters : yes, named │ -│ Subtitles: muxed, forced flags set where needed │ -│ │ -│ Nothing to prove, just a clean encode. Fix it yourself │ -│ if you can do better. │ -├──────────────────────────────────────────────────────────────┤ -│ GREETS │ -│ │ -│ the-scene 0day our-suppliers you │ -└──────────────────────────────────────────────────────────────┘ +╔═════════════════════════════════════════════════════════════════════════════╗ +║ THE.MIDNIGHT.CARTOGRAPHER.2026.1080P.BLURAY.X264-KRZ ║ +║ [ KRZ ] krz.sh ║ +║ -={ KRZ proudly presents }=- ║ +╠═════════════════════════════════════════════════════════════════════════════╣ +║ DATE 2026-08-08 ║ +║ TYPE MOVIE ║ +║ SOURCE BluRay (Retail) ║ +║ FORMAT Matroska (MKV) ║ +║ VIDEO x264 @ 8500 kbps, 23.976 fps ║ +║ AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) ║ +║ RESOLUTION 1920x1080p ║ +║ LANGUAGE English (subs: EN, ES, FR) ║ +║ GENRE Drama / Mystery ║ +║ RUNTIME 1h 52m ║ +║ SIZE 8.42 GiB ║ +║ FILES 42 x 200 MB ║ +║ URL imdb.com/title/tt9999999 ║ +╠═════════════════════════════════════════════════════════════════════════════╣ +║ NOTES ║ +║ ║ +║ A quiet cartographer maps a city that rearranges itself ║ +║ after midnight. Encoded from retail BluRay, untouched ║ +║ audio, nothing re-encoded twice. ║ +║ ║ +║ Source : COMPLETE.BLURAY (thanks to the supplier) ║ +║ Encoder : x264 10-bit, --preset veryslow --crf 18 ║ +║ Chapters : yes, named ║ +║ Subtitles: muxed, forced flags set where needed ║ +║ ║ +║ Nothing to prove, just a clean encode. Fix it yourself ║ +║ if you can do better. ║ +╠═════════════════════════════════════════════════════════════════════════════╣ +║ GREETS ║ +║ ║ +║ the-scene 0day our-suppliers you ║ +╠═════════════════════════════════════════════════════════════════════════════╣ +║ ║ +║ SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE ║ +║ if you enjoyed this release, buy it! ║ +║ ║ +╚═════════════════════════════════════════════════════════════════════════════╝ @@ -1,5 +1,8 @@ -group = "KRZ" -site = "krz.sh" +group = "KRZ" +site = "krz.sh" +style = "double" +presents = true +footer = true title = "The.Midnight.Cartographer.2026.1080p.BluRay.x264-KRZ" date = "2026-08-08" @@ -47,7 +47,24 @@ INFO_KEYS = [k for k, _, _, kind in FIELDS if kind == "line" and k != "title"] DEFAULT_SITE = "krz.sh" DEFAULT_GROUP = "KRZ" -DEFAULT_WIDTH = 64 +DEFAULT_WIDTH = 79 # DOS 80-column standard, less one margin column +DEFAULT_STYLE = "double" +DEFAULT_FOOTER = ( + "SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE\n" + "if you enjoyed this release, buy it!" +) + +# Box-drawing character sets. "single"/"double" are CP437-encodable; "block" +# is a solid fill frame. Keys: corners + horizontal/vertical + tee separators. +STYLES = { + "single": dict(tl="┌", tr="┐", bl="└", br="┘", + h="─", v="│", ls="├", rs="┤"), + "double": dict(tl="╔", tr="╗", bl="╚", br="╝", + h="═", v="║", ls="╠", rs="╣"), + "block": dict(tl="█", tr="█", bl="█", br="█", + h="█", v="█", ls="█", rs="█"), +} + CONFIG_CANDIDATES = [ Path("nfogen.toml"), Path(".nfogen.toml"), @@ -106,26 +123,31 @@ def _split_list(value) -> list[str]: return [p for p in str(value).replace(",", " ").split() if p] -def render(data: dict, group: str, site: str, width: int) -> str: +def render(data: dict, group: str, site: str, width: int, + style: str = DEFAULT_STYLE, presents: bool = False, + footer: str | None = None) -> str: inner = width - 4 # borders + one space of padding on each side - top = "┌" + "─" * (width - 2) + "┐" - sep = "├" + "─" * (width - 2) + "┤" - bot = "└" + "─" * (width - 2) + "┘" + s = STYLES[style] + top = s["tl"] + s["h"] * (width - 2) + s["tr"] + sep = s["ls"] + s["h"] * (width - 2) + s["rs"] + bot = s["bl"] + s["h"] * (width - 2) + s["br"] def line(content: str = "") -> str: - return "│ " + content.ljust(inner) + " │" + return f"{s['v']} " + content.ljust(inner) + f" {s['v']}" def center(content: str) -> str: - return "│ " + content.center(inner) + " │" + return f"{s['v']} " + content.center(inner) + f" {s['v']}" out: list[str] = [top] - # header: spaced title, then group tag + site + # header: spaced title, then group tag + site, optional presents banner 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))) + if presents: + out.append(center(_clip(f"-={{ {group.upper()} proudly presents }}=-", inner))) # info rows rows = [(label, str(data[key])) @@ -160,12 +182,21 @@ def render(data: dict, group: str, site: str, width: int) -> str: for wrapped in textwrap.wrap(" ".join(greets), inner - 4): out.append(line(" " + wrapped)) + # footer disclaimer, centered + if footer: + out.append(sep) + out.append(line()) + for para in footer.splitlines(): + for wrapped in (textwrap.wrap(para, inner - 4) or [""]): + out.append(center(wrapped)) + out.append(line()) + 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] + "…" + return text if len(text) <= width else text[: width - 2] + ".." # --- cli --------------------------------------------------------------------- @@ -182,6 +213,15 @@ def build_parser() -> argparse.ArgumentParser: 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("--style", choices=sorted(STYLES), + help=f"box-drawing style (default {DEFAULT_STYLE})") + p.add_argument("--encoding", choices=["utf8", "cp437"], default="utf8", + help="output encoding (default utf8; cp437 for true DOS nfos)") + p.add_argument("--presents", action=argparse.BooleanOptionalAction, + default=None, help="show the 'proudly presents' banner") + p.add_argument("--footer", action=argparse.BooleanOptionalAction, + default=None, help="show the closing disclaimer block") + p.add_argument("--footer-text", help="custom disclaimer text") p.add_argument("-i", "--interactive", action="store_true", help="prompt for every field") p.add_argument("--no-input", action="store_true", @@ -206,6 +246,17 @@ def main(argv: list[str] | None = None) -> int: 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 + style = args.style or config.get("style") or DEFAULT_STYLE + + def resolve(flag, key, default): + if flag is not None: + return flag + return config.get(key, default) + + presents = resolve(args.presents, "presents", False) + show_footer = resolve(args.footer, "footer", False) + footer_text = args.footer_text or config.get("footer_text") or DEFAULT_FOOTER + footer = footer_text if show_footer else None interactive = sys.stdin.isatty() and not args.no_input if args.interactive: @@ -216,8 +267,16 @@ def main(argv: list[str] | None = None) -> int: 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: + text = render(data, group=group, site=site, width=width, style=style, + presents=presents, footer=footer) + + if args.encoding == "cp437": + raw = text.encode("cp437", errors="replace") + if args.output: + Path(args.output).write_bytes(raw) + else: + sys.stdout.buffer.write(raw) + elif args.output: Path(args.output).write_text(text, encoding="utf-8") else: sys.stdout.write(text) @@ -1,5 +1,7 @@ group = "KRZ" site = "krz.sh" +width = 64 +style = "single" title = "nfogen 1.0" type = "APP" files = "nfogen.py" @@ -31,7 +33,12 @@ url notes greets media: format video audio resolution language genre runtime -OPTIONS: -o -c -g -s -w -i --no-input +STYLES: single, double, block (--style) +ENCODING: utf8 or cp437 (--encoding cp437 = DOS) +BANNERS: --presents --footer scene boilerplate + +OPTIONS: -o -c -g -s -w --style --encoding + --presents --footer -i --no-input Generated by nfogen itself. """