Commit 907e55cc50
Verified · cmc
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 | 34 | │ media: format video audio resolution │ |
| 35 | 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 | 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 | ┌──────────────────────────────────────────────────────────────┐ | |
| 2 | │ THE.MIDNIGHT.CARTOGRAPHER.2026.1080P.BLURAY.X264-KRZ │ | |
| 3 | │ [ KRZ ] krz.sh │ | |
| 4 | ├──────────────────────────────────────────────────────────────┤ | |
| 5 | │ DATE 2026-08-08 │ | |
| 6 | │ TYPE MOVIE │ | |
| 7 | │ SOURCE BluRay (Retail) │ | |
| 8 | │ FORMAT Matroska (MKV) │ | |
| 9 | │ VIDEO x264 @ 8500 kbps, 23.976 fps │ | |
| 10 | │ AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) │ | |
| 11 | │ RESOLUTION 1920x1080p │ | |
| 12 | │ LANGUAGE English (subs: EN, ES, FR) │ | |
| 13 | │ GENRE Drama / Mystery │ | |
| 14 | │ RUNTIME 1h 52m │ | |
| 15 | │ SIZE 8.42 GiB │ | |
| 16 | │ FILES 42 x 200 MB │ | |
| 17 | │ URL imdb.com/title/tt9999999 │ | |
| 18 | ├──────────────────────────────────────────────────────────────┤ | |
| 19 | │ NOTES │ | |
| 20 | │ │ | |
| 21 | │ A quiet cartographer maps a city that rearranges itself │ | |
| 22 | │ after midnight. Encoded from retail BluRay, untouched │ | |
| 23 | │ audio, nothing re-encoded twice. │ | |
| 24 | │ │ | |
| 25 | │ Source : COMPLETE.BLURAY (thanks to the supplier) │ | |
| 26 | │ Encoder : x264 10-bit, --preset veryslow --crf 18 │ | |
| 27 | │ Chapters : yes, named │ | |
| 28 | │ Subtitles: muxed, forced flags set where needed │ | |
| 29 | │ │ | |
| 30 | │ Nothing to prove, just a clean encode. Fix it yourself │ | |
| 31 | │ if you can do better. │ | |
| 32 | ├──────────────────────────────────────────────────────────────┤ | |
| 33 | │ GREETS │ | |
| 34 | │ │ | |
| 35 | │ the-scene 0day our-suppliers you │ | |
| 36 | └──────────────────────────────────────────────────────────────┘ | |
| 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.toml +5 −2
| @@ -1,5 +1,8 @@ | ||
| 1 | group = "KRZ" | |
| 2 | site = "krz.sh" | |
| 1 | group = "KRZ" | |
| 2 | site = "krz.sh" | |
| 3 | style = "double" | |
| 4 | presents = true | |
| 5 | footer = true | |
| 3 | 6 | |
| 4 | 7 | title = "The.Midnight.Cartographer.2026.1080p.BluRay.x264-KRZ" |
| 5 | 8 | date = "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 | |
| 48 | 48 | DEFAULT_SITE = "krz.sh" |
| 49 | 49 | DEFAULT_GROUP = "KRZ" |
| 50 | DEFAULT_WIDTH = 64 | |
| 50 | DEFAULT_WIDTH = 79 # DOS 80-column standard, less one margin column | |
| 51 | DEFAULT_STYLE = "double" | |
| 52 | DEFAULT_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. | |
| 59 | STYLES = { | |
| 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 | ||
| 51 | 68 | CONFIG_CANDIDATES = [ |
| 52 | 69 | Path("nfogen.toml"), |
| 53 | 70 | Path(".nfogen.toml"), |
| @@ -106,26 +123,31 @@ def _split_list(value) -> list[str]: | ||
| 106 | 123 | return [p for p in str(value).replace(",", " ").split() if p] |
| 107 | 124 | |
| 108 | 125 | |
| 109 | def render(data: dict, group: str, site: str, width: int) -> str: | |
| 126 | def render(data: dict, group: str, site: str, width: int, | |
| 127 | style: str = DEFAULT_STYLE, presents: bool = False, | |
| 128 | footer: str | None = None) -> str: | |
| 110 | 129 | inner = width - 4 # borders + one space of padding on each side |
| 111 | top = "┌" + "─" * (width - 2) + "┐" | |
| 112 | sep = "├" + "─" * (width - 2) + "┤" | |
| 113 | bot = "└" + "─" * (width - 2) + "┘" | |
| 130 | s = STYLES[style] | |
| 131 | top = s["tl"] + s["h"] * (width - 2) + s["tr"] | |
| 132 | sep = s["ls"] + s["h"] * (width - 2) + s["rs"] | |
| 133 | bot = s["bl"] + s["h"] * (width - 2) + s["br"] | |
| 114 | 134 | |
| 115 | 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 | 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 | 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 | 144 | title = str(data.get("title") or "UNTITLED").upper() |
| 125 | 145 | spaced = " ".join(title) |
| 126 | 146 | heading = spaced if len(spaced) <= inner else title |
| 127 | 147 | out.append(center(_clip(heading, inner))) |
| 128 | 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 | 152 | # info rows |
| 131 | 153 | rows = [(label, str(data[key])) |
| @@ -160,12 +182,21 @@ def render(data: dict, group: str, site: str, width: int) -> str: | ||
| 160 | 182 | for wrapped in textwrap.wrap(" ".join(greets), inner - 4): |
| 161 | 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 | 194 | out.append(bot) |
| 164 | 195 | return "\n".join(out) + "\n" |
| 165 | 196 | |
| 166 | 197 | |
| 167 | 198 | def _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 | 202 | # --- cli --------------------------------------------------------------------- |
| @@ -182,6 +213,15 @@ def build_parser() -> argparse.ArgumentParser: | ||
| 182 | 213 | p.add_argument("-s", "--site", help=f"site tag (default {DEFAULT_SITE})") |
| 183 | 214 | p.add_argument("-w", "--width", type=int, |
| 184 | 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 | 225 | p.add_argument("-i", "--interactive", action="store_true", |
| 186 | 226 | help="prompt for every field") |
| 187 | 227 | p.add_argument("--no-input", action="store_true", |
| @@ -206,6 +246,17 @@ def main(argv: list[str] | None = None) -> int: | ||
| 206 | 246 | group = args.group or config.get("group") or DEFAULT_GROUP |
| 207 | 247 | site = args.site or config.get("site") or DEFAULT_SITE |
| 208 | 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 | 261 | interactive = sys.stdin.isatty() and not args.no_input |
| 211 | 262 | if args.interactive: |
| @@ -216,8 +267,16 @@ def main(argv: list[str] | None = None) -> int: | ||
| 216 | 267 | if not data.get("title"): |
| 217 | 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) | |
| 220 | if args.output: | |
| 270 | text = render(data, group=group, site=site, width=width, style=style, | |
| 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 | 280 | Path(args.output).write_text(text, encoding="utf-8") |
| 222 | 281 | else: |
| 223 | 282 | sys.stdout.write(text) |
nfogen.toml +8 −1
| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | group = "KRZ" |
| 2 | 2 | site = "krz.sh" |
| 3 | width = 64 | |
| 4 | style = "single" | |
| 3 | 5 | title = "nfogen 1.0" |
| 4 | 6 | type = "APP" |
| 5 | 7 | files = "nfogen.py" |
| @@ -31,7 +33,12 @@ url notes greets | ||
| 31 | 33 | media: format video audio resolution |
| 32 | 34 | language genre runtime |
| 33 | 35 | |
| 34 | OPTIONS: -o -c -g -s -w -i --no-input | |
| 36 | STYLES: single, double, block (--style) | |
| 37 | ENCODING: utf8 or cp437 (--encoding cp437 = DOS) | |
| 38 | BANNERS: --presents --footer scene boilerplate | |
| 39 | ||
| 40 | OPTIONS: -o -c -g -s -w --style --encoding | |
| 41 | --presents --footer -i --no-input | |
| 35 | 42 | |
| 36 | 43 | Generated by nfogen itself. |
| 37 | 44 | """ |