krz/nfogen

A scene-style ASCII .nfo generator. ascii cli generator nfo

Commit 907e55cc50

907e55cc50ea05d1c0a317c2c8008a58abc5bb32

parent: ad2fa192d7

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-08 05:44 UTC

add CP437 output, box styles, 79-col default, presents/footer banners

Layout: unified · split

.gitattributes added +2
@@ -0,0 +1,2 @@
1# CP437-encoded nfos hold high bytes; keep git from normalizing them.
2*.cp437.nfo binary
README.nfo +6 −1
@@ -34,7 +34,12 @@
34│ media: format video audio resolution │ 34│ media: format video audio resolution │
35│ language genre runtime │ 35│ language genre runtime │
36│ │ 36│ │
37│ OPTIONS: -o -c -g -s -w -i --no-input │ 37│ STYLES: single, double, block (--style) │
38│ ENCODING: utf8 or cp437 (--encoding cp437 = DOS) │
39│ BANNERS: --presents --footer scene boilerplate │
40│ │
41│ OPTIONS: -o -c -g -s -w --style --encoding │
42│ --presents --footer -i --no-input │
38│ │ 43│ │
39│ Generated by nfogen itself. │ 44│ Generated by nfogen itself. │
40├──────────────────────────────────────────────────────────────┤ 45├──────────────────────────────────────────────────────────────┤
examples/the-midnight-cartographer-2026.cp437.nfo added +42
@@ -0,0 +1,42 @@
1ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͻ
2º THE.MIDNIGHT.CARTOGRAPHER.2026.1080P.BLURAY.X264-KRZ º
3º [ KRZ ] krz.sh º
4º -={ KRZ proudly presents }=- º
5ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
6º DATE 2026-08-08 º
7º TYPE MOVIE º
8º SOURCE BluRay (Retail) º
9º FORMAT Matroska (MKV) º
10º VIDEO x264 @ 8500 kbps, 23.976 fps º
11º AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) º
12º RESOLUTION 1920x1080p º
13º LANGUAGE English (subs: EN, ES, FR) º
14º GENRE Drama / Mystery º
15º RUNTIME 1h 52m º
16º SIZE 8.42 GiB º
17º FILES 42 x 200 MB º
18º URL imdb.com/title/tt9999999 º
19ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
20º NOTES º
21º º
22º A quiet cartographer maps a city that rearranges itself º
23º after midnight. Encoded from retail BluRay, untouched º
24º audio, nothing re-encoded twice. º
25º º
26º Source : COMPLETE.BLURAY (thanks to the supplier) º
27º Encoder : x264 10-bit, --preset veryslow --crf 18 º
28º Chapters : yes, named º
29º Subtitles: muxed, forced flags set where needed º
30º º
31º Nothing to prove, just a clean encode. Fix it yourself º
32º if you can do better. º
33ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
34º GREETS º
35º º
36º the-scene 0day our-suppliers you º
37ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
38º º
39º SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE º
40º if you enjoyed this release, buy it! º
41º º
42ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ
examples/the-midnight-cartographer-2026.nfo +42 −36
@@ -1,36 +1,42 @@
1┌──────────────────────────────────────────────────────────────┐ 1╔═════════════════════════════════════════════════════════════════════════════╗
2│ THE.MIDNIGHT.CARTOGRAPHER.2026.1080P.BLURAY.X264-KRZ │ 2║ THE.MIDNIGHT.CARTOGRAPHER.2026.1080P.BLURAY.X264-KRZ ║
3│ [ KRZ ] krz.sh │ 3║ [ KRZ ] krz.sh ║
4├──────────────────────────────────────────────────────────────┤ 4║ -={ KRZ proudly presents }=- ║
5│ DATE 2026-08-08 │ 5╠═════════════════════════════════════════════════════════════════════════════╣
6│ TYPE MOVIE │ 6║ DATE 2026-08-08 ║
7│ SOURCE BluRay (Retail) │ 7║ TYPE MOVIE ║
8│ FORMAT Matroska (MKV) │ 8║ SOURCE BluRay (Retail) ║
9│ VIDEO x264 @ 8500 kbps, 23.976 fps │ 9║ FORMAT Matroska (MKV) ║
10│ AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) │ 10║ VIDEO x264 @ 8500 kbps, 23.976 fps ║
11│ RESOLUTION 1920x1080p │ 11║ AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) ║
12│ LANGUAGE English (subs: EN, ES, FR) │ 12║ RESOLUTION 1920x1080p ║
13│ GENRE Drama / Mystery │ 13║ LANGUAGE English (subs: EN, ES, FR) ║
14│ RUNTIME 1h 52m │ 14║ GENRE Drama / Mystery ║
15│ SIZE 8.42 GiB │ 15║ RUNTIME 1h 52m ║
16│ FILES 42 x 200 MB │ 16║ SIZE 8.42 GiB ║
17│ URL imdb.com/title/tt9999999 │ 17║ FILES 42 x 200 MB ║
18├──────────────────────────────────────────────────────────────┤ 18║ URL imdb.com/title/tt9999999 ║
19│ NOTES │ 19╠═════════════════════════════════════════════════════════════════════════════╣
20│ │ 20║ NOTES ║
21│ A quiet cartographer maps a city that rearranges itself │ 21║ ║
22│ after midnight. Encoded from retail BluRay, untouched │ 22║ A quiet cartographer maps a city that rearranges itself ║
23│ audio, nothing re-encoded twice. │ 23║ after midnight. Encoded from retail BluRay, untouched ║
24│ │ 24║ audio, nothing re-encoded twice. ║
25│ Source : COMPLETE.BLURAY (thanks to the supplier) │ 25║ ║
26│ Encoder : x264 10-bit, --preset veryslow --crf 18 │ 26║ Source : COMPLETE.BLURAY (thanks to the supplier) ║
27│ Chapters : yes, named │ 27║ Encoder : x264 10-bit, --preset veryslow --crf 18 ║
28│ Subtitles: muxed, forced flags set where needed │ 28║ Chapters : yes, named ║
29│ │ 29║ Subtitles: muxed, forced flags set where needed ║
30│ Nothing to prove, just a clean encode. Fix it yourself │ 30║ ║
31│ if you can do better. │ 31║ Nothing to prove, just a clean encode. Fix it yourself ║
32├──────────────────────────────────────────────────────────────┤ 32║ if you can do better. ║
33│ GREETS │ 33╠═════════════════════════════════════════════════════════════════════════════╣
34│ │ 34║ GREETS ║
35│ the-scene 0day our-suppliers you │ 35║ ║
36└──────────────────────────────────────────────────────────────┘ 36║ the-scene 0day our-suppliers you ║
37╠═════════════════════════════════════════════════════════════════════════════╣
38║ ║
39║ SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE ║
40║ if you enjoyed this release, buy it! ║
41║ ║
42╚═════════════════════════════════════════════════════════════════════════════╝
examples/the-midnight-cartographer-2026.toml +5 −2
@@ -1,5 +1,8 @@
1group = "KRZ" 1group = "KRZ"
2site = "krz.sh" 2site = "krz.sh"
3style = "double"
4presents = true
5footer = true
3 6
4title = "The.Midnight.Cartographer.2026.1080p.BluRay.x264-KRZ" 7title = "The.Midnight.Cartographer.2026.1080p.BluRay.x264-KRZ"
5date = "2026-08-08" 8date = "2026-08-08"
nfogen.py +70 −11
@@ -47,7 +47,24 @@ INFO_KEYS = [k for k, _, _, kind in FIELDS if kind == "line" and k != "title"]
47 47
48DEFAULT_SITE = "krz.sh" 48DEFAULT_SITE = "krz.sh"
49DEFAULT_GROUP = "KRZ" 49DEFAULT_GROUP = "KRZ"
50DEFAULT_WIDTH = 64 50DEFAULT_WIDTH = 79 # DOS 80-column standard, less one margin column
51DEFAULT_STYLE = "double"
52DEFAULT_FOOTER = (
53 "SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE\n"
54 "if you enjoyed this release, buy it!"
55)
56
57# Box-drawing character sets. "single"/"double" are CP437-encodable; "block"
58# is a solid fill frame. Keys: corners + horizontal/vertical + tee separators.
59STYLES = {
60 "single": dict(tl="┌", tr="┐", bl="└", br="┘",
61 h="─", v="│", ls="├", rs="┤"),
62 "double": dict(tl="╔", tr="╗", bl="╚", br="╝",
63 h="═", v="║", ls="╠", rs="╣"),
64 "block": dict(tl="█", tr="█", bl="█", br="█",
65 h="█", v="█", ls="█", rs="█"),
66}
67
51CONFIG_CANDIDATES = [ 68CONFIG_CANDIDATES = [
52 Path("nfogen.toml"), 69 Path("nfogen.toml"),
53 Path(".nfogen.toml"), 70 Path(".nfogen.toml"),
@@ -106,26 +123,31 @@ def _split_list(value) -> list[str]:
106 return [p for p in str(value).replace(",", " ").split() if p] 123 return [p for p in str(value).replace(",", " ").split() if p]
107 124
108 125
109def render(data: dict, group: str, site: str, width: int) -> str: 126def render(data: dict, group: str, site: str, width: int,
127 style: str = DEFAULT_STYLE, presents: bool = False,
128 footer: str | None = None) -> str:
110 inner = width - 4 # borders + one space of padding on each side 129 inner = width - 4 # borders + one space of padding on each side
111 top = "┌" + "─" * (width - 2) + "┐" 130 s = STYLES[style]
112 sep = "├" + "─" * (width - 2) + "┤" 131 top = s["tl"] + s["h"] * (width - 2) + s["tr"]
113 bot = "└" + "─" * (width - 2) + "┘" 132 sep = s["ls"] + s["h"] * (width - 2) + s["rs"]
133 bot = s["bl"] + s["h"] * (width - 2) + s["br"]
114 134
115 def line(content: str = "") -> str: 135 def line(content: str = "") -> str:
116 return "│ " + content.ljust(inner) + " │" 136 return f"{s['v']} " + content.ljust(inner) + f" {s['v']}"
117 137
118 def center(content: str) -> str: 138 def center(content: str) -> str:
119 return "│ " + content.center(inner) + " │" 139 return f"{s['v']} " + content.center(inner) + f" {s['v']}"
120 140
121 out: list[str] = [top] 141 out: list[str] = [top]
122 142
123 # header: spaced title, then group tag + site 143 # header: spaced title, then group tag + site, optional presents banner
124 title = str(data.get("title") or "UNTITLED").upper() 144 title = str(data.get("title") or "UNTITLED").upper()
125 spaced = " ".join(title) 145 spaced = " ".join(title)
126 heading = spaced if len(spaced) <= inner else title 146 heading = spaced if len(spaced) <= inner else title
127 out.append(center(_clip(heading, inner))) 147 out.append(center(_clip(heading, inner)))
128 out.append(center(_clip(f"[ {group} ] {site}", inner))) 148 out.append(center(_clip(f"[ {group} ] {site}", inner)))
149 if presents:
150 out.append(center(_clip(f"-={{ {group.upper()} proudly presents }}=-", inner)))
129 151
130 # info rows 152 # info rows
131 rows = [(label, str(data[key])) 153 rows = [(label, str(data[key]))
@@ -160,12 +182,21 @@ def render(data: dict, group: str, site: str, width: int) -> str:
160 for wrapped in textwrap.wrap(" ".join(greets), inner - 4): 182 for wrapped in textwrap.wrap(" ".join(greets), inner - 4):
161 out.append(line(" " + wrapped)) 183 out.append(line(" " + wrapped))
162 184
185 # footer disclaimer, centered
186 if footer:
187 out.append(sep)
188 out.append(line())
189 for para in footer.splitlines():
190 for wrapped in (textwrap.wrap(para, inner - 4) or [""]):
191 out.append(center(wrapped))
192 out.append(line())
193
163 out.append(bot) 194 out.append(bot)
164 return "\n".join(out) + "\n" 195 return "\n".join(out) + "\n"
165 196
166 197
167def _clip(text: str, width: int) -> str: 198def _clip(text: str, width: int) -> str:
168 return text if len(text) <= width else text[: width - 1] + "…" 199 return text if len(text) <= width else text[: width - 2] + ".."
169 200
170 201
171# --- cli --------------------------------------------------------------------- 202# --- cli ---------------------------------------------------------------------
@@ -182,6 +213,15 @@ def build_parser() -> argparse.ArgumentParser:
182 p.add_argument("-s", "--site", help=f"site tag (default {DEFAULT_SITE})") 213 p.add_argument("-s", "--site", help=f"site tag (default {DEFAULT_SITE})")
183 p.add_argument("-w", "--width", type=int, 214 p.add_argument("-w", "--width", type=int,
184 help=f"box width in chars (default {DEFAULT_WIDTH})") 215 help=f"box width in chars (default {DEFAULT_WIDTH})")
216 p.add_argument("--style", choices=sorted(STYLES),
217 help=f"box-drawing style (default {DEFAULT_STYLE})")
218 p.add_argument("--encoding", choices=["utf8", "cp437"], default="utf8",
219 help="output encoding (default utf8; cp437 for true DOS nfos)")
220 p.add_argument("--presents", action=argparse.BooleanOptionalAction,
221 default=None, help="show the 'proudly presents' banner")
222 p.add_argument("--footer", action=argparse.BooleanOptionalAction,
223 default=None, help="show the closing disclaimer block")
224 p.add_argument("--footer-text", help="custom disclaimer text")
185 p.add_argument("-i", "--interactive", action="store_true", 225 p.add_argument("-i", "--interactive", action="store_true",
186 help="prompt for every field") 226 help="prompt for every field")
187 p.add_argument("--no-input", action="store_true", 227 p.add_argument("--no-input", action="store_true",
@@ -206,6 +246,17 @@ def main(argv: list[str] | None = None) -> int:
206 group = args.group or config.get("group") or DEFAULT_GROUP 246 group = args.group or config.get("group") or DEFAULT_GROUP
207 site = args.site or config.get("site") or DEFAULT_SITE 247 site = args.site or config.get("site") or DEFAULT_SITE
208 width = args.width or config.get("width") or DEFAULT_WIDTH 248 width = args.width or config.get("width") or DEFAULT_WIDTH
249 style = args.style or config.get("style") or DEFAULT_STYLE
250
251 def resolve(flag, key, default):
252 if flag is not None:
253 return flag
254 return config.get(key, default)
255
256 presents = resolve(args.presents, "presents", False)
257 show_footer = resolve(args.footer, "footer", False)
258 footer_text = args.footer_text or config.get("footer_text") or DEFAULT_FOOTER
259 footer = footer_text if show_footer else None
209 260
210 interactive = sys.stdin.isatty() and not args.no_input 261 interactive = sys.stdin.isatty() and not args.no_input
211 if args.interactive: 262 if args.interactive:
@@ -216,8 +267,16 @@ def main(argv: list[str] | None = None) -> int:
216 if not data.get("title"): 267 if not data.get("title"):
217 sys.exit("nfogen: a title is required (use --title, a config, or -i)") 268 sys.exit("nfogen: a title is required (use --title, a config, or -i)")
218 269
219 text = render(data, group=group, site=site, width=width) 270 text = render(data, group=group, site=site, width=width, style=style,
220 if args.output: 271 presents=presents, footer=footer)
272
273 if args.encoding == "cp437":
274 raw = text.encode("cp437", errors="replace")
275 if args.output:
276 Path(args.output).write_bytes(raw)
277 else:
278 sys.stdout.buffer.write(raw)
279 elif args.output:
221 Path(args.output).write_text(text, encoding="utf-8") 280 Path(args.output).write_text(text, encoding="utf-8")
222 else: 281 else:
223 sys.stdout.write(text) 282 sys.stdout.write(text)
nfogen.toml +8 −1
@@ -1,5 +1,7 @@
1group = "KRZ" 1group = "KRZ"
2site = "krz.sh" 2site = "krz.sh"
3width = 64
4style = "single"
3title = "nfogen 1.0" 5title = "nfogen 1.0"
4type = "APP" 6type = "APP"
5files = "nfogen.py" 7files = "nfogen.py"
@@ -31,7 +33,12 @@ url notes greets
31media: format video audio resolution 33media: format video audio resolution
32language genre runtime 34language genre runtime
33 35
34OPTIONS: -o -c -g -s -w -i --no-input 36STYLES: single, double, block (--style)
37ENCODING: utf8 or cp437 (--encoding cp437 = DOS)
38BANNERS: --presents --footer scene boilerplate
39
40OPTIONS: -o -c -g -s -w --style --encoding
41 --presents --footer -i --no-input
35 42
36Generated by nfogen itself. 43Generated by nfogen itself.
37""" 44"""