nx_media_harvest.nx source
↩ module page · 368 lines · 22625 B
1// nx_media_harvest.nx -- SUPERSET media-URL harvester (operator 2026-07-08: "how do i get my nishi browser to
2// see the source of gifs and vids and images on a page like this" -- an ad/popunder lander hides media in lazy
3// attrs, <source>/<video>, HLS manifests, JSON blobs and CSS url()). This is nx_img_harvest's proven multi-
4// strategy raw-byte scan EXTENDED past images: it also finds VIDEO (.mp4/.webm/.m4v/.mov/.mkv/.ogv), STREAM
5// manifests (.m3u8/.mpd) and AUDIO (.mp3/.m4a/.ogg/.opus/.wav/.aac), and it adds a CSS url(...) pass so
6// UNQUOTED backgrounds (background:url(/bg/hero.gif)) surface too -- the quoted-region scan alone misses those.
7// Every hit is CLASSIFIED into a kind (0 image / 1 gif / 2 video / 3 stream / 4 audio) so the caller can label
8// and preview it. Same offs/lens/url_buf interface as nx_img_harvest, plus a parallel kinds[] array.
9// nx_media_harvest(html,hlen, base_url,base_len, url_buf,cap, offs,lens,kinds, max_n) -> count
10// Mechanism (per token): unescape JSON "\/"->"/", classify by path extension (path = up to ?/#), resolve
11// rel->abs (nx_url_resolve), dedup. Junk (data: URIs, .svg, favicon/sprite/logo) rejected for image kinds.
12// license_tier: ORIGINAL
13import "nx_syscalls.nx"
14import "nx_url_resolve.nx" // nx_url_resolve + NX_URL_RESOLVE_OK (proven relative->absolute resolver)
15
16const MH_KIND_IMAGE: i64 = 0
17const MH_KIND_GIF: i64 = 1
18const MH_KIND_VIDEO: i64 = 2
19const MH_KIND_STREAM: i64 = 3
20const MH_KIND_AUDIO: i64 = 4
21
22// lowercase a byte
23func mh_lc(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c }
24func mh_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 }
25
26// case-insensitive substring within hay[0..hn)
27func mh_has_ci(hay: *u8, hn: i64, needle: *u8) -> i64 {
28 var nl: i64 = 0; while needle[nl] != (0 as u8) { nl = nl + 1 }
29 if nl == 0 { return 0 }
30 var i: i64 = 0
31 while i + nl <= hn {
32 var m: i64 = 1; var jx: i64 = 0
33 while jx < nl { if mh_lc(hay[i+jx] as i64) != mh_lc(needle[jx] as i64) { m = 0; jx = nl } else { jx = jx + 1 } }
34 if m == 1 { return 1 }
35 i = i + 1
36 }
37 return 0
38}
39
40// Classify tok[0..tn) by its path extension. Returns MH_KIND_* or -1 (not media). Rejects data: URIs, .svg,
41// and (for image kinds only) favicon/sprite/logo UI junk. path = bytes up to the first ? or #.
42func mh_classify(tok: *u8, tn: i64) -> i64 {
43 if tn < 5 { return 0 - 1 }
44 // reject data: URIs
45 if mh_lc(tok[0] as i64)==100 { if mh_lc(tok[1] as i64)==97 { if mh_lc(tok[2] as i64)==116 { if mh_lc(tok[3] as i64)==97 { if tok[4]==(58 as u8) { return 0 - 1 } } } } } // "data:"
46 // clean-URL charset + must contain a path '/'. Measured on real Wikipedia HTML 2026-07-08: this removes prose
47 // words that merely end in .gif (com.compuserve.gif, (large).gif -- no slash) and nested-quote garbage a
48 // single-quoted attribute region can smuggle into a token (srcset="//... -- contains a quote), while keeping
49 // every real asset (real URLs never contain a raw quote/space/angle-bracket and always carry a path slash).
50 var slash: i64 = 0
51 var g: i64 = 0
52 while g < tn {
53 let cc: i64 = tok[g] as i64
54 if cc == 47 { slash = 1 } // '/'
55 if cc == 34 { return 0 - 1 } // "
56 if cc == 39 { return 0 - 1 } // '
57 if cc == 60 { return 0 - 1 } // <
58 if cc == 62 { return 0 - 1 } // >
59 if cc == 32 { return 0 - 1 } // space
60 if cc == 9 { return 0 - 1 } // tab
61 if cc == 10 { return 0 - 1 } // nl
62 if cc == 13 { return 0 - 1 } // cr
63 if cc == 92 { return 0 - 1 } // backslash
64 if cc == 96 { return 0 - 1 } // backtick
65 if cc == 123 { return 0 - 1 } // {
66 if cc == 125 { return 0 - 1 } // }
67 if cc == 124 { return 0 - 1 } // |
68 g = g + 1
69 }
70 if slash == 0 { return 0 - 1 }
71 // path end = first ? or #
72 var end: i64 = tn
73 var i: i64 = 0
74 while i < tn { let c: i64 = tok[i] as i64; if c == 63 { if end == tn { end = i } } if c == 35 { if end == tn { end = i } } i = i + 1 }
75 if end < 5 { return 0 - 1 }
76 // last '.'
77 var dot: i64 = 0 - 1
78 var j: i64 = 0
79 while j < end { if tok[j] == (46 as u8) { dot = j } j = j + 1 }
80 if dot < 0 { return 0 - 1 }
81 if dot == 0 { return 0 - 1 } // leading-dot dotfile, no name
82 if tok[dot-1] == (47 as u8) { return 0 - 1 } // empty filename like ".../.gif"
83 let el: i64 = end - dot
84 if el < 4 { return 0 - 1 }
85 if el > 5 { return 0 - 1 }
86 // build lowercased ext (".xxx" / ".xxxx"), NUL-terminated
87 let e: *u8 = sys_mmap(8)
88 var k: i64 = 0
89 while dot + k < end { if k < 7 { e[k] = mh_lc(tok[dot + k] as i64) as u8 } k = k + 1 }
90 e[k] = 0 as u8
91 var kind: i64 = 0 - 1
92 // images
93 if mh_streq(e, ".jpg" as *u8)==1 { kind = MH_KIND_IMAGE }
94 if mh_streq(e, ".jpeg" as *u8)==1 { kind = MH_KIND_IMAGE }
95 if mh_streq(e, ".png" as *u8)==1 { kind = MH_KIND_IMAGE }
96 if mh_streq(e, ".webp" as *u8)==1 { kind = MH_KIND_IMAGE }
97 if mh_streq(e, ".avif" as *u8)==1 { kind = MH_KIND_IMAGE }
98 if mh_streq(e, ".bmp" as *u8)==1 { kind = MH_KIND_IMAGE }
99 // gif (called out separately -- the operator asked for gifs by name)
100 if mh_streq(e, ".gif" as *u8)==1 { kind = MH_KIND_GIF }
101 // video
102 if mh_streq(e, ".mp4" as *u8)==1 { kind = MH_KIND_VIDEO }
103 if mh_streq(e, ".webm" as *u8)==1 { kind = MH_KIND_VIDEO }
104 if mh_streq(e, ".m4v" as *u8)==1 { kind = MH_KIND_VIDEO }
105 if mh_streq(e, ".mov" as *u8)==1 { kind = MH_KIND_VIDEO }
106 if mh_streq(e, ".mkv" as *u8)==1 { kind = MH_KIND_VIDEO }
107 if mh_streq(e, ".ogv" as *u8)==1 { kind = MH_KIND_VIDEO }
108 // streaming manifests (HLS/DASH) -- how modern "vids" actually ship
109 if mh_streq(e, ".m3u8" as *u8)==1 { kind = MH_KIND_STREAM }
110 if mh_streq(e, ".mpd" as *u8)==1 { kind = MH_KIND_STREAM }
111 // audio
112 if mh_streq(e, ".mp3" as *u8)==1 { kind = MH_KIND_AUDIO }
113 if mh_streq(e, ".m4a" as *u8)==1 { kind = MH_KIND_AUDIO }
114 if mh_streq(e, ".ogg" as *u8)==1 { kind = MH_KIND_AUDIO }
115 if mh_streq(e, ".opus" as *u8)==1 { kind = MH_KIND_AUDIO }
116 if mh_streq(e, ".wav" as *u8)==1 { kind = MH_KIND_AUDIO }
117 if mh_streq(e, ".aac" as *u8)==1 { kind = MH_KIND_AUDIO }
118 if kind < 0 { return 0 - 1 }
119 // image-kind UI-junk denylist. ONLY favicon/sprite -- those are never content. NOT "logo" (an og:image or
120 // hero logo IS a picture the operator asked to see; measured: /logo denylist wrongly dropped a real og:image).
121 if kind == MH_KIND_IMAGE {
122 if mh_has_ci(tok, end, "favicon" as *u8)==1 { return 0 - 1 }
123 if mh_has_ci(tok, end, "sprite" as *u8)==1 { return 0 - 1 }
124 }
125 return kind
126}
127
128// copy tok[0..tn) into out, converting JSON escape "\/"->"/"; NUL-term. returns length.
129func mh_unescape(tok: *u8, tn: i64, out: *u8, cap: i64) -> i64 {
130 var o: i64 = 0
131 var i: i64 = 0
132 while i < tn {
133 if o >= cap - 1 { i = tn } else {
134 let c: i64 = tok[i] as i64
135 if c == 92 { if i + 1 < tn { if tok[i+1] == (47 as u8) { out[o] = 47 as u8; o = o + 1; i = i + 2 } else { out[o] = tok[i] as u8; o = o + 1; i = i + 1 } } else { i = tn } }
136 else { out[o] = tok[i]; o = o + 1; i = i + 1 }
137 }
138 }
139 out[o] = 0 as u8
140 return o
141}
142
143// already-collected? linear scan of url_buf via offs/lens
144func mh_seen(url_buf: *u8, offs: *i64, lens: *i64, count: i64, cand: *u8, cn: i64) -> i64 {
145 var i: i64 = 0
146 while i < count {
147 if lens[i] == cn {
148 var eq: i64 = 1; var k: i64 = 0
149 let bp: *u8 = ((url_buf as i64) + offs[i]) as *u8
150 while k < cn { if bp[k] != cand[k] { eq = 0; k = cn } else { k = k + 1 } }
151 if eq == 1 { return 1 }
152 }
153 i = i + 1
154 }
155 return 0
156}
157
158// resolve a candidate -> out; returns length or 0. absolute http(s) passes through; //h/p gets https:; else
159// nx_url_resolve against base.
160func mh_resolve(base: *u8, blen: i64, cand: *u8, cn: i64, out: *u8, cap: i64) -> i64 {
161 if cn >= 7 { if mh_lc(cand[0] as i64)==104 { if mh_lc(cand[1] as i64)==116 { if mh_lc(cand[2] as i64)==116 { if mh_lc(cand[3] as i64)==112 {
162 var s: i64 = 4
163 if mh_lc(cand[4] as i64)==115 { s = 5 }
164 if cand[s]==(58 as u8) { if cand[s+1]==(47 as u8) { if cand[s+2]==(47 as u8) {
165 var o: i64 = 0; while o < cn { if o < cap - 1 { out[o] = cand[o] } o = o + 1 } out[cn] = 0 as u8; return cn
166 } } }
167 } } } } }
168 // protocol-relative //host/path -> https:
169 if cn >= 2 { if cand[0]==(47 as u8) { if cand[1]==(47 as u8) {
170 let pfx: *u8 = "https:" as *u8
171 var o: i64 = 0; while o < 6 { out[o] = pfx[o]; o = o + 1 }
172 var q: i64 = 0; while q < cn { if o < cap - 1 { out[o] = cand[q]; o = o + 1 } q = q + 1 }
173 out[o] = 0 as u8; return o
174 } } }
175 // relative -> proven resolver
176 let olp: *i64 = sys_mmap(8) as *i64
177 let rc: i64 = nx_url_resolve(base, blen, cand, cn, out, cap, olp)
178 if rc == NX_URL_RESOLVE_OK { return olp[0] }
179 return 0
180}
181
182// consider one raw token: if it classifies as media, unescape+resolve+dedup+append (with its kind). Returns new count.
183func mh_consider(tok: *u8, tn: i64, base_url: *u8, base_url_len: i64,
184 url_buf: *u8, url_buf_cap: i64, offs: *i64, lens: *i64, kinds: *i64, max_n: i64,
185 count: i64, buf_pos_p: *i64, unesc: *u8, resolved: *u8) -> i64 {
186 if count >= max_n { return count }
187 if tn < 5 { return count }
188 let un: i64 = mh_unescape(tok, tn, unesc, 8192)
189 let kind: i64 = mh_classify(unesc, un)
190 if kind < 0 { return count }
191 let rn: i64 = mh_resolve(base_url, base_url_len, unesc, un, resolved, 8192)
192 if rn <= 0 { return count }
193 if mh_seen(url_buf, offs, lens, count, resolved, rn) == 1 { return count }
194 if buf_pos_p[0] + rn >= url_buf_cap { return count }
195 var k: i64 = 0
196 while k < rn { url_buf[buf_pos_p[0] + k] = resolved[k]; k = k + 1 }
197 offs[count] = buf_pos_p[0]
198 lens[count] = rn
199 kinds[count] = kind
200 buf_pos_p[0] = buf_pos_p[0] + rn
201 return count + 1
202}
203
204// THE HARVEST. Two passes over the raw bytes: (1) every quoted region, tokenized on space/comma (catches
205// <img src>, lazy data-*, srcset candidates, JSON arrays, quoted CSS url('..')); (2) every url(...) region
206// (catches UNQUOTED CSS backgrounds). Returns count; fills offs/lens/kinds into url_buf up to max_n.
207func nx_media_harvest(html: *u8, hlen: i64, base_url: *u8, base_url_len: i64,
208 url_buf: *u8, url_buf_cap: i64, offs: *i64, lens: *i64, kinds: *i64, max_n: i64) -> i64 {
209 var count: i64 = 0
210 let bpp: *i64 = sys_mmap(8) as *i64; bpp[0] = 0
211 let tokbuf: *u8 = sys_mmap(8192)
212 let unesc: *u8 = sys_mmap(8192)
213 let resolved: *u8 = sys_mmap(8192)
214
215 // ---- pass 1: quoted regions ----
216 var i: i64 = 0
217 while i < hlen {
218 if count >= max_n { i = hlen } else {
219 let ch: i64 = html[i] as i64
220 var q: i64 = 0
221 if ch == 34 { q = 1 }
222 if ch == 39 { q = 1 }
223 if q == 1 {
224 var s: i64 = i + 1
225 var e: i64 = s
226 var go: i64 = 1
227 while go == 1 { if e >= hlen { go = 0 } else { if (html[e] as i64) == ch { go = 0 } else { e = e + 1 } } }
228 var ts: i64 = s
229 var p: i64 = s
230 while p <= e {
231 var brk: i64 = 0
232 if p == e { brk = 1 } else { let c2: i64 = html[p] as i64; if c2 == 32 { brk = 1 } if c2 == 9 { brk = 1 } if c2 == 10 { brk = 1 } if c2 == 13 { brk = 1 } if c2 == 44 { brk = 1 } }
233 if brk == 1 {
234 let tl: i64 = p - ts
235 if tl > 0 { if tl < 8192 {
236 var w: i64 = 0
237 while w < tl { tokbuf[w] = html[ts + w]; w = w + 1 }
238 tokbuf[tl] = 0 as u8
239 count = mh_consider(tokbuf, tl, base_url, base_url_len, url_buf, url_buf_cap, offs, lens, kinds, max_n, count, bpp, unesc, resolved)
240 } }
241 ts = p + 1
242 }
243 p = p + 1
244 }
245 if e < hlen { i = e + 1 } else { i = hlen }
246 } else { i = i + 1 }
247 }
248 }
249
250 // ---- pass 2: url(...) regions (catches UNQUOTED CSS backgrounds the quote scan misses) ----
251 var u: i64 = 0
252 while u + 4 < hlen {
253 if count >= max_n { u = hlen } else {
254 // match "url(" case-insensitively
255 var hit: i64 = 0
256 if mh_lc(html[u] as i64)==117 { if mh_lc(html[u+1] as i64)==114 { if mh_lc(html[u+2] as i64)==108 { if html[u+3]==(40 as u8) { hit = 1 } } } }
257 if hit == 1 {
258 var s2: i64 = u + 4
259 // skip leading spaces and a single opening quote
260 var f: i64 = 1
261 while f == 1 { if s2 >= hlen { f = 0 } else { let cc: i64 = html[s2] as i64; if cc == 32 { s2 = s2 + 1 } else { if cc == 34 { s2 = s2 + 1; f = 0 } else { if cc == 39 { s2 = s2 + 1; f = 0 } else { f = 0 } } } } }
262 var e2: i64 = s2
263 var g2: i64 = 1
264 while g2 == 1 { if e2 >= hlen { g2 = 0 } else { let cc2: i64 = html[e2] as i64; if cc2 == 41 { g2 = 0 } else { if cc2 == 34 { g2 = 0 } else { if cc2 == 39 { g2 = 0 } else { e2 = e2 + 1 } } } } }
265 let tl2: i64 = e2 - s2
266 if tl2 > 0 { if tl2 < 8192 {
267 var w2: i64 = 0
268 while w2 < tl2 { tokbuf[w2] = html[s2 + w2]; w2 = w2 + 1 }
269 tokbuf[tl2] = 0 as u8
270 count = mh_consider(tokbuf, tl2, base_url, base_url_len, url_buf, url_buf_cap, offs, lens, kinds, max_n, count, bpp, unesc, resolved)
271 } }
272 if e2 > u { u = e2 } else { u = u + 1 }
273 } else { u = u + 1 }
274 }
275 }
276
277 return count
278}
279
280// human label for a kind (for callers that print/emit)
281func nx_media_kind_label(kind: i64) -> *u8 {
282 if kind == MH_KIND_GIF { return "gif" as *u8 }
283 if kind == MH_KIND_VIDEO { return "video" as *u8 }
284 if kind == MH_KIND_STREAM { return "stream" as *u8 }
285 if kind == MH_KIND_AUDIO { return "audio" as *u8 }
286 return "image" as *u8
287}
288
289// ---- shared HTML MEDIA PANEL emitter (used by nx_media_inspect AND the in-browser 'm' key) ----
290// buffer-append helpers
291func mh_ap(buf: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; var w: i64=off; while s[i]!=(0 as u8){ buf[w]=s[i]; w=w+1; i=i+1 } return w }
292func mh_apr(buf: *u8, off: i64, ptr: *u8, len: i64) -> i64 { var i: i64=0; var w: i64=off; while i<len { buf[w]=ptr[i]; w=w+1; i=i+1 } return w }
293func mh_apn(buf: *u8, off: i64, v0: i64) -> i64 { var v: i64=v0; var w: i64=off; if v<0 { buf[w]=45 as u8; w=w+1; v=0-v } let b: *u8=sys_mmap(24); var k: i64=0; if v==0{b[0]=48 as u8;k=1} while v>0{b[k]=(48+(v%10)) as u8;v=v/10;k=k+1} var j: i64=k-1; while j>=0{ buf[w]=b[j]; w=w+1; j=j-1 } return w }
294
295func mh_card_img(H: *u8, off: i64, p: *u8, ulen: i64) -> i64 {
296 var o: i64 = off
297 o = mh_ap(H, o, "<div class='card'><a class='prev' href='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "' target='_blank' rel='noreferrer'><img loading='lazy' src='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "' alt=''></a><div class='body'><div class='u mono'>" as *u8); o = mh_apr(H, o, p, ulen)
298 o = mh_ap(H, o, "</div><div class='row'><a class='btn' href='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "' target='_blank' rel='noreferrer'>open</a><a class='btn' href='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "' download>download</a></div></div></div>" as *u8)
299 return o
300}
301func mh_card_video(H: *u8, off: i64, p: *u8, ulen: i64) -> i64 {
302 var o: i64 = off
303 o = mh_ap(H, o, "<div class='card'><div class='prev'><video controls preload='metadata' src='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "'></video></div><div class='body'><div class='u mono'>" as *u8); o = mh_apr(H, o, p, ulen)
304 o = mh_ap(H, o, "</div><div class='row'><a class='btn' href='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "' target='_blank' rel='noreferrer'>open</a><a class='btn' href='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "' download>download</a></div></div></div>" as *u8)
305 return o
306}
307func mh_card_audio(H: *u8, off: i64, p: *u8, ulen: i64) -> i64 {
308 var o: i64 = off
309 o = mh_ap(H, o, "<div class='card'><div class='prev'><audio controls preload='metadata' src='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "'></audio></div><div class='body'><div class='u mono'>" as *u8); o = mh_apr(H, o, p, ulen)
310 o = mh_ap(H, o, "</div><div class='row'><a class='btn' href='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "' target='_blank' rel='noreferrer'>open</a><a class='btn' href='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "' download>download</a></div></div></div>" as *u8)
311 return o
312}
313func mh_card_link(H: *u8, off: i64, p: *u8, ulen: i64, note: *u8) -> i64 {
314 var o: i64 = off
315 o = mh_ap(H, o, "<div class='card'><div class='prev'><span class='ph'>" as *u8); o = mh_ap(H, o, note); o = mh_ap(H, o, "</span></div><div class='body'><div class='u mono'>" as *u8); o = mh_apr(H, o, p, ulen)
316 o = mh_ap(H, o, "</div><div class='row'><a class='btn' href='" as *u8); o = mh_apr(H, o, p, ulen); o = mh_ap(H, o, "' target='_blank' rel='noreferrer'>open</a></div></div></div>" as *u8)
317 return o
318}
319
320// Build the full media-panel HTML into H (caller sizes H for ~600B/item + 3KB chrome). Returns byte length.
321func nx_media_emit_panel(H: *u8, srcurl: *u8, srclen: i64, ub: *u8, offs: *i64, lens: *i64, kinds: *i64, cnt: i64) -> i64 {
322 var ci: i64=0; var cg: i64=0; var cv: i64=0; var cs: i64=0; var ca: i64=0
323 var t: i64 = 0
324 while t < cnt {
325 let kk: i64 = kinds[t]
326 if kk == MH_KIND_IMAGE { ci = ci + 1 }
327 if kk == MH_KIND_GIF { cg = cg + 1 }
328 if kk == MH_KIND_VIDEO { cv = cv + 1 }
329 if kk == MH_KIND_STREAM { cs = cs + 1 }
330 if kk == MH_KIND_AUDIO { ca = ca + 1 }
331 t = t + 1
332 }
333 var o: i64 = 0
334 o = mh_ap(H, o, "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><meta name='color-scheme' content='dark light'><title>Media sources</title><style>" as *u8)
335 o = mh_ap(H, o, "body{margin:0;background:#12131a;color:#e8e8ef;font:15px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,Arial,sans-serif}" as *u8)
336 o = mh_ap(H, o, ".wrap{max-width:1180px;margin:0 auto;padding:22px 18px 60px}h1{font-size:1.15rem;margin:0 0 4px}.src{color:#8a93a6;font-size:.85rem;word-break:break-all;margin:0 0 14px}" as *u8)
337 o = mh_ap(H, o, ".counts{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 20px}.pill{background:#1e2130;border:1px solid #2c3040;border-radius:999px;padding:4px 12px;font-size:.82rem;color:#c8cfe0}" as *u8)
338 o = mh_ap(H, o, "h2{font-size:.78rem;letter-spacing:.12em;text-transform:uppercase;color:#7d879b;margin:26px 0 10px;border-bottom:1px solid #242838;padding-bottom:6px}" as *u8)
339 o = mh_ap(H, o, ".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:14px}.card{background:#191b26;border:1px solid #272b3b;border-radius:12px;overflow:hidden;display:flex;flex-direction:column}" as *u8)
340 o = mh_ap(H, o, ".card .prev{background:#0d0e14;aspect-ratio:16/10;display:flex;align-items:center;justify-content:center;overflow:hidden}.card .prev img,.card .prev video{max-width:100%;max-height:100%;object-fit:contain}" as *u8)
341 o = mh_ap(H, o, ".card .body{padding:9px 11px;display:flex;flex-direction:column;gap:7px}.u{font-size:.74rem;color:#aeb6c9;word-break:break-all;line-height:1.35}" as *u8)
342 o = mh_ap(H, o, ".row{display:flex;gap:7px}.btn{flex:1;text-align:center;font-size:.76rem;text-decoration:none;padding:5px 8px;border-radius:7px;border:1px solid #313648;color:#dfe4f0;background:#232838;cursor:pointer}.btn:hover{background:#2b3145}" as *u8)
343 o = mh_ap(H, o, ".ph{color:#5f677d;font-size:.8rem;text-align:center;padding:0 10px}.mono{font-family:ui-monospace,Menlo,Consolas,monospace}</style></head><body><div class='wrap'>" as *u8)
344 o = mh_ap(H, o, "<h1>Media sources on this page</h1><p class='src mono'>" as *u8)
345 o = mh_apr(H, o, srcurl, srclen)
346 o = mh_ap(H, o, "</p><div class='counts'>" as *u8)
347 o = mh_ap(H, o, "<span class='pill'>" as *u8); o = mh_apn(H, o, cnt); o = mh_ap(H, o, " total</span>" as *u8)
348 o = mh_ap(H, o, "<span class='pill'>" as *u8); o = mh_apn(H, o, ci); o = mh_ap(H, o, " image</span>" as *u8)
349 o = mh_ap(H, o, "<span class='pill'>" as *u8); o = mh_apn(H, o, cg); o = mh_ap(H, o, " gif</span>" as *u8)
350 o = mh_ap(H, o, "<span class='pill'>" as *u8); o = mh_apn(H, o, cv); o = mh_ap(H, o, " video</span>" as *u8)
351 o = mh_ap(H, o, "<span class='pill'>" as *u8); o = mh_apn(H, o, cs); o = mh_ap(H, o, " stream</span>" as *u8)
352 o = mh_ap(H, o, "<span class='pill'>" as *u8); o = mh_apn(H, o, ca); o = mh_ap(H, o, " audio</span></div>" as *u8)
353 // images + gifs
354 o = mh_ap(H, o, "<h2>Images & GIFs</h2><div class='grid'>" as *u8)
355 var q: i64 = 0
356 while q < cnt { let kk: i64 = kinds[q]; let p: *u8 = ((ub as i64) + offs[q]) as *u8; if kk == MH_KIND_IMAGE { o = mh_card_img(H, o, p, lens[q]) } if kk == MH_KIND_GIF { o = mh_card_img(H, o, p, lens[q]) } q = q + 1 }
357 o = mh_ap(H, o, "</div><h2>Video files</h2><div class='grid'>" as *u8)
358 var q2: i64 = 0
359 while q2 < cnt { if kinds[q2] == MH_KIND_VIDEO { let p2: *u8 = ((ub as i64) + offs[q2]) as *u8; o = mh_card_video(H, o, p2, lens[q2]) } q2 = q2 + 1 }
360 o = mh_ap(H, o, "</div><h2>Stream manifests (HLS / DASH)</h2><div class='grid'>" as *u8)
361 var q3: i64 = 0
362 while q3 < cnt { if kinds[q3] == MH_KIND_STREAM { let p3: *u8 = ((ub as i64) + offs[q3]) as *u8; o = mh_card_link(H, o, p3, lens[q3], "manifest \xe2\x80\x94 open in a player" as *u8) } q3 = q3 + 1 }
363 o = mh_ap(H, o, "</div><h2>Audio files</h2><div class='grid'>" as *u8)
364 var q4: i64 = 0
365 while q4 < cnt { if kinds[q4] == MH_KIND_AUDIO { let p4: *u8 = ((ub as i64) + offs[q4]) as *u8; o = mh_card_audio(H, o, p4, lens[q4]) } q4 = q4 + 1 }
366 o = mh_ap(H, o, "</div></div></body></html>" as *u8)
367 return o
368}