nx_4chan.nx
buildroot/runtime/nx_4chan.nx
about
nx_4chan.nx -- SOVEREIGN 4chan BROWSE / PREVIEW / DOWNLOAD adapter: the first of the media-manager
per-source ingesters ported off Python (mm/src/sites/fourchan.py) into the estate. Emits JSON on
stdout so the gallery/vault surface consumes it directly; downloads land in a plain directory that
`nx_mvault_walk <dir> real 4chan commit` then records into the vault (CID dedup + DSM layout).
COMPOSITION, NOT REIMPLEMENTATION: the vault already owns classification/dedup/layout (B1-B10), so
this organ deliberately stops at "validated bytes in a directory" and hands off.
nx_4chan boards -- board list (+nsfw flag)
nx_4chan catalog <board> [max] -- thread previews: subject, comment, counts, THUMB+FULL urls
nx_4chan thread <board> <threadno> [max] -- every media item in a thread
nx_4chan fetch <board> <threadno> <dir> [max] -- download the thread's media into <dir>
nx_4chan selftest -- offline gate: parser teeth incl. neg-controls
★ WHY A BRACE-DEPTH PARSER AND NOT KEY-SCANNING. catalog.json embeds a `last_replies` array whose
post objects each carry their OWN "no": field, so scanning for `"no":` reports replies AS THREADS --
and it fails silently, inflating a 15-thread page into 60 "threads". Depth tracking is immune to
field order and to nesting. It is also STRING-AWARE: a 4chan comment routinely contains { } and ",
and a structural scanner that reads braces inside string data mis-nests the whole document.
Conveniently ALL THREE endpoints put their objects of interest at brace-depth 2:
catalog.json [ {page {1}, threads:[ {thread {2}, last_replies:[ {reply {3}} ]} ]} ]
thread.json { {1} "posts":[ {post {2}} ] }
boards.json { {1} "boards":[ {board {2}} ] }
so one scanner serves all three and `want_depth` stays a parameter rather than three constants.
LEGAL/RATE POSTURE: read-only public JSON API, one polite delay between media fetches. General
capability; the operator points it where they like.
license_tier: ORIGINAL
dependencies 6 imports · 0 importers
imports: nx_syscalls.nxnx_x509_trust_store.nxnx_trust_store_load_from_certdata.nxnx_https_fetch_follow.nxnx_json_lib.nxnx_crawl_pace.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 44 | const FC_API_HOST: *u8 = "a.4cdn.org" |
| 45 | const FC_API_HOST_LEN: i64 = 10 |
| 46 | const FC_IMG_HOST: *u8 = "i.4cdn.org" |
| 47 | const FC_IMG_HOST_LEN: i64 = 10 |
| 48 | const FC_MAGIC_1000000: i64 = 1000000 |
| 49 | const FC_MAGIC_1700000000001: i64 = 1700000000001 |
| 51 | const FC_MAGIC_4194304: i64 = 4194304 |
| 52 | const FC_MAGIC_2048: i64 = 2048 |
| 53 | const FC_MAGIC_65536: i64 = 65536 |
| 54 | const FC_MAGIC_1024: i64 = 1024 |
| 55 | const FC_MAGIC_512: i64 = 512 |
| 57 | const FC_JSON_CAP: i64 = 8388608 // catalog.json for a busy board runs ~1-3MB |
| 58 | const FC_IMG_CAP: i64 = 33554432 // 32MB: /gif/ webm legitimately exceeds the manga 16MB cap |
| 59 | const FC_MAX_OBJ: i64 = 4096 // objects enumerated per document (threads/posts/boards) |
| 60 | const FC_OUT_CAP: i64 = 4194304 // emitted JSON |
| 61 | const FC_TEXT_CAP: i64 = 8192 // one sub/com field |
| 62 | const FC_MIN_BYTES: i64 = 512 // below this a "media file" is an error page or a spacer |
| 63 | const FC_REDIRECTS: i64 = 6 |
| 66 | const FC_FETCH_DELAY_MS: i64 = 350 |
| 71 | const FC_FETCH_RETRIES: i64 = 4 |
functions
| 73 | func fc_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } |
| 74 | func fc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } |
| 75 | func fc_apps(dst: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[o + i] = s[i]; i = i + 1 } return o + i } |
| 76 | func fc_num(dst: *u8, o: i64, v: i64) -> i64 |
| 88 | func fc_putn(v: i64) -> i64 { let b: *u8 = sys_mmap(32); let e: i64 = fc_num(b, 0, v); sys_write(1, b, e); return 0 } |
| 89 | func fc_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] == (0 as u8) { return 1 } return 0 } |
| 90 | func fc_sleep_ms(ms: i64) -> i64 calls 1: sys_mmap |
| 104 | func fc_scan_objects(buf: *u8, n: i64, want_depth: i64, offs: *i64, lens: *i64, cap: i64) -> i64 |
| 145 | func fc_emit_text(dst: *u8, o0: i64, src: *u8, maxout: i64) -> i64 |
| 185 | func fc_media_url(dst: *u8, o0: i64, board: *u8, tim: i64, ext: *u8, thumb: i64) -> i64 |
| 199 | func fc_media_complete(b: *u8, n: i64) -> i64 |
| 219 | func fc_mkdir_p(path: *u8) -> i64 |
| 240 | func fc_fetch_one(url: *u8, store: *TrustStore, dir: *u8, name: *u8) -> i64 |
| 271 | func fc_get_json(url: *u8, store: *TrustStore, buf: *u8, out_status: *i64) -> i64 |
| 275 | func fc_api_url(dst: *u8, board: *u8, tail: *u8, thread: i64) -> i64 |
| 285 | func fc_boards(store: *TrustStore) -> i64 |
| 329 | func fc_list(store: *TrustStore, board: *u8, thread: i64, maxn: i64, mode: i64) -> i64 |
| 422 | func fc_fetch(store: *TrustStore, board: *u8, thread: i64, dir: *u8, maxn: i64) -> i64 |
| 505 | func fc_selftest() -> i64 |
| 667 | func fc_atoi(s: *u8) -> i64 |
| 677 | func fc_saveone(store: *TrustStore, board: *u8, file: *u8, dir: *u8) -> i64 |
| 692 | func main(argc: i64, argv: *i64) -> i64 |