code wiki / (root) / nx_yt_captions_lib.nx

nx_yt_captions_lib.nx source

↩ module page · 482 lines · 29357 B

1// nx_yt_captions_lib.nx -- the YouTube TIMED-TEXT (caption/transcript) path, composed on the estate's 2// proven InnerTube + sovereign-TLS stack. It does NOT duplicate nx_yt_innertube: it COMPOSES yt_innertube 3// (the ANDROID_VR /youtubei/v1/player POST that self-heals against key rotation) to obtain the player 4// response, reads the captionTracks[] baseUrl out of THAT SAME response, and fetches the timedtext body 5// with nx_https_fetch_follow (the shared gzip-decoding, redirect-following GET). Parsers turn json3 and 6// srv1/XML timedtext into rows `startMs \t durMs \t text`, entities decoded through nx_html_entities. 7// 8// WHY A SIBLING LIB, NOT AN EDIT TO nx_yt_innertube: that lib's header declares its subject as the stream 9// path (streamingData.formats[].url); captions are a distinct output read from the same response, so this 10// stays a NEW file that imports it (blast radius: zero -- nothing that imports innertube is touched). 11// 12// WHY THE WEB baseUrl DOES NOT WORK (measured 2026-09-18 from the NAS): the WEB captionTracks baseUrl 13// scraped from a watch page answers status=200 with an EMPTY body -- it is IP-locked (ip=0.0.0.0) and 14// requires a proof-of-origin (pot) token the WEB client mints via BotGuard. The ANDROID_VR client's 15// baseUrl carries no such requirement, which is the whole reason this composes yt_innertube. 16// license_tier: ORIGINAL No hw writes (Rule 26). LIB (no main). 17import "nx_syscalls.nx" 18import "nx_yt_innertube.nx" // yt_innertube (ANDROID_VR player POST); brings TrustStore + TLS closure 19import "nx_media_state.nx" // ms_find, ms_extract_json_value 20import "nx_https_fetch_follow.nx" // nx_https_fetch_follow: gzip-decoding, redirect-following GET; nx_csprng_fill 21import "nx_html_entities.nx" // nx_html_entity_lookup: HTML5 named-entity decode for srv1/XML text 22 23// ---- caps (named for their purpose) ------------------------------------------------------------ 24// Mozilla certdata.txt measures ~1MB; this is ITS reserve alone (matches nx_research_fetch/nx_yt_probe). 25const YTC_CERTDATA_CAP: i64 = 4194304 26// THE PLAYER-RESPONSE RESERVE: an ANDROID_VR /player JSON is ~100-300 KB for a normal video; 8 MiB is 27// generous headroom. A network body's exact size is unknowable in advance, so this is a NAMED bound for 28// this one purpose (buffer-cap law); mmap commits pages lazily so headroom costs address space only. 29const YTC_PLAYER_CAP: i64 = 8388608 30// THE CAPTION-BODY RESERVE: a timedtext response for a multi-hour lecture is at most a few hundred KB of 31// text; 64 MiB is far past any real transcript. Same named-network-body bound; the caller ANNOUNCES and 32// REFUSES if a body ever fills it rather than saving a maybe-truncated transcript. 33const YTC_BODY_CAP: i64 = 67108864 34// A signed timedtext baseUrl measured at ~350 B (WEB) / ~600 B (translation params); 8 KiB is generous. 35const YTC_URL_CAP: i64 = 8192 36 37// ---- small byte helpers -------------------------------------------------------------------------- 38func ytc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 39func ytc_catn(dst: *u8, o: i64, v: i64, cap: i64) -> i64 { 40 if v == 0 { if o < cap - 1 { dst[o] = 48 as u8; o = o + 1 } return o } 41 var m: i64 = v 42 if m < 0 { if o < cap - 1 { dst[o] = 45 as u8; o = o + 1 } m = 0 - m } 43 let t: *u8 = sys_mmap(24); var k: i64 = 0 44 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 45 var j: i64 = 0 46 while j < k { if o < cap - 1 { dst[o] = t[k - 1 - j]; o = o + 1 } j = j + 1 } 47 return o 48} 49func ytc_hexval(c: i64) -> i64 { 50 if c >= 48 { if c <= 57 { return c - 48 } } 51 if c >= 97 { if c <= 102 { return c - 87 } } 52 if c >= 65 { if c <= 70 { return c - 55 } } 53 return 0 54} 55// emit codepoint cp as UTF-8 at dst[o..]; strip=1 maps ASCII control (<32) to space so a row stays 1 line. 56func ytc_utf8_emit(dst: *u8, o: i64, cp0: i64, cap: i64, strip: i64) -> i64 { 57 var cp: i64 = cp0 58 if strip == 1 { if cp < 32 { cp = 32 } } 59 if cp < 128 { if o < cap - 1 { dst[o] = cp as u8; o = o + 1 } return o } 60 if cp < 2048 { if o < cap - 2 { dst[o] = (192 + (cp >> 6)) as u8; dst[o+1] = (128 + (cp & 63)) as u8; o = o + 2 } return o } 61 if cp < 65536 { if o < cap - 3 { dst[o] = (224 + (cp >> 12)) as u8; dst[o+1] = (128 + ((cp >> 6) & 63)) as u8; dst[o+2] = (128 + (cp & 63)) as u8; o = o + 3 } return o } 62 if o < cap - 4 { dst[o] = (240 + (cp >> 18)) as u8; dst[o+1] = (128 + ((cp >> 12) & 63)) as u8; dst[o+2] = (128 + ((cp >> 6) & 63)) as u8; dst[o+3] = (128 + (cp & 63)) as u8; o = o + 4 } 63 return o 64} 65 66// Copy a JSON string value (src[start..] is the FIRST content byte AFTER the opening quote) into dst[o..], 67// decoding \/ \" \\ \n \t \r and \uXXXX (incl surrogate pairs). Stops at the unescaped closing quote. 68// strip=1 sanitises control bytes to space. Returns new o; out_end[0] = index of the closing quote. 69func ytc_copy_jsonstr(src: *u8, start: i64, slen: i64, dst: *u8, o0: i64, cap: i64, strip: i64, out_end: *i64) -> i64 { 70 var i: i64 = start 71 var o: i64 = o0 72 var go: i64 = 1 73 while go == 1 { 74 if i >= slen { go = 0 } 75 else { 76 let c: i64 = src[i] & 0xff 77 if c == 34 { out_end[0] = i; return o } // unescaped closing quote 78 if c == 92 { // backslash 79 if i + 1 >= slen { out_end[0] = i; return o } 80 let n: i64 = src[i+1] & 0xff 81 if n == 117 { // \u 82 if i + 5 < slen { 83 var cp: i64 = (ytc_hexval(src[i+2]&0xff) << 12) | (ytc_hexval(src[i+3]&0xff) << 8) | (ytc_hexval(src[i+4]&0xff) << 4) | ytc_hexval(src[i+5]&0xff) 84 i = i + 6 85 if cp >= 55296 { if cp <= 56319 { // high surrogate: look for the low half 86 if i + 5 < slen { if (src[i]&0xff) == 92 { if (src[i+1]&0xff) == 117 { 87 let lo: i64 = (ytc_hexval(src[i+2]&0xff) << 12) | (ytc_hexval(src[i+3]&0xff) << 8) | (ytc_hexval(src[i+4]&0xff) << 4) | ytc_hexval(src[i+5]&0xff) 88 if lo >= 56320 { if lo <= 57343 { cp = 65536 + ((cp - 55296) << 10) + (lo - 56320); i = i + 6 } } 89 } } } 90 } } 91 o = ytc_utf8_emit(dst, o, cp, cap, strip) 92 } else { i = i + 2 } 93 } else { 94 var m: i64 = n 95 if n == 110 { m = 10 } // \n 96 if n == 116 { m = 9 } // \t 97 if n == 114 { m = 13 } // \r 98 if n == 47 { m = 47 } // \/ 99 if n == 92 { m = 92 } // \\ 100 if n == 34 { m = 34 } // \" 101 o = ytc_utf8_emit(dst, o, m, cap, strip) 102 i = i + 2 103 } 104 } else { 105 o = ytc_utf8_emit(dst, o, c, cap, strip) 106 i = i + 1 107 } 108 } 109 } 110 out_end[0] = slen 111 return o 112} 113 114// exact compare of src[s..s+len) against a NUL-terminated literal. 115func ytc_named_eq(src: *u8, s: i64, len: i64, lit: *u8) -> i64 { 116 var i: i64 = 0 117 while i < len { if lit[i] == (0 as u8) { return 0 } if (src[s+i]&0xff) != (lit[i]&0xff) { return 0 } i = i + 1 } 118 if lit[i] != (0 as u8) { return 0 } 119 return 1 120} 121// Decode an HTML/XML text run src[s..e) (entities &amp; &lt; &gt; &quot; &#NN; &#xHH; and any HTML5 named 122// reference) into dst[o..]; strip=1 -> control bytes become space. Returns new o. 123func ytc_html_decode(src: *u8, s: i64, e: i64, dst: *u8, o0: i64, cap: i64, strip: i64) -> i64 { 124 var i: i64 = s 125 var o: i64 = o0 126 while i < e { 127 let c: i64 = src[i] & 0xff 128 if c == 38 { // '&' 129 var j: i64 = i + 1 130 var semi: i64 = 0 - 1 131 while j < e { if j - i > 33 { break } if (src[j]&0xff) == 59 { semi = j; break } j = j + 1 } 132 if semi < 0 { o = ytc_utf8_emit(dst, o, c, cap, strip); i = i + 1 } 133 else { 134 if (src[i+1]&0xff) == 35 { // '#': numeric 135 var cp: i64 = 0 136 if (src[i+2]&0xff) == 120 { var k: i64 = i + 3; while k < semi { cp = cp * 16 + ytc_hexval(src[k]&0xff); k = k + 1 } } 137 else { var k: i64 = i + 2; while k < semi { let d: i64 = src[k]&0xff; if d >= 48 { if d <= 57 { cp = cp * 10 + (d - 48) } } k = k + 1 } } 138 o = ytc_utf8_emit(dst, o, cp, cap, strip) 139 } else { 140 let elen: i64 = semi - (i + 1) // entity name WITHOUT the ';' 141 var done: i64 = 0 142 if ytc_named_eq(src, i+1, elen, "amp" as *u8) == 1 { o = ytc_utf8_emit(dst, o, 38, cap, strip); done = 1 } 143 if done == 0 { if ytc_named_eq(src, i+1, elen, "lt" as *u8) == 1 { o = ytc_utf8_emit(dst, o, 60, cap, strip); done = 1 } } 144 if done == 0 { if ytc_named_eq(src, i+1, elen, "gt" as *u8) == 1 { o = ytc_utf8_emit(dst, o, 62, cap, strip); done = 1 } } 145 if done == 0 { if ytc_named_eq(src, i+1, elen, "quot" as *u8) == 1 { o = ytc_utf8_emit(dst, o, 34, cap, strip); done = 1 } } 146 if done == 0 { if ytc_named_eq(src, i+1, elen, "apos" as *u8) == 1 { o = ytc_utf8_emit(dst, o, 39, cap, strip); done = 1 } } 147 if done == 0 { if ytc_named_eq(src, i+1, elen, "nbsp" as *u8) == 1 { o = ytc_utf8_emit(dst, o, 160, cap, strip); done = 1 } } 148 if done == 0 { 149 let namelen: i64 = elen + 1 // include the ';' the table expects 150 let cpbuf: *i64 = sys_mmap(16) as *i64 151 let r: i64 = nx_html_entity_lookup(((src as i64) + (i + 1)) as *u8, namelen, cpbuf) 152 if r >= 1 { var q: i64 = 0; while q < r { o = ytc_utf8_emit(dst, o, cpbuf[q], cap, strip); q = q + 1 } done = 1 } 153 } 154 if done == 0 { var k: i64 = i; while k <= semi { o = ytc_utf8_emit(dst, o, src[k]&0xff, cap, strip); k = k + 1 } } 155 } 156 i = semi + 1 157 } 158 } else { o = ytc_utf8_emit(dst, o, c, cap, strip); i = i + 1 } 159 } 160 return o 161} 162 163// ---- caption-track selection from the player response -------------------------------------------- 164// Extract the balanced captionTracks[...] array into arr. Returns array bytes, or -1 if the response has 165// no caption tracks at all (a video with captions disabled). 166func ytc_ct_array(pj: *u8, jlen: i64, arr: *u8, cap: i64) -> i64 { 167 let ct: i64 = ms_find(pj, jlen, "\"captionTracks\":[" as *u8, 17, 0) 168 if ct < 0 { return 0 - 1 } 169 return ms_extract_json_value(pj, ct + 16, jlen, arr, cap) // ct+16 is the '[' 170} 171// Pick a track by languageCode (wlen==0 -> first track). Writes the JSON-unescaped baseUrl (NUL-terminated) 172// and the kind ("asr" or "manual"). Returns 1 ok, 0 no-tracks, -2 requested-lang-absent. 173func ytc_pick(arr: *u8, alen: i64, want: *u8, wlen: i64, out_url: *u8, ucap: i64, out_kind: *u8, kcap: i64) -> i64 { 174 if alen <= 2 { return 0 } 175 let eidx: *i64 = sys_mmap(16) as *i64 176 var pos: i64 = 0 177 var go: i64 = 1 178 while go == 1 { 179 let bu: i64 = ms_find(arr, alen, "\"baseUrl\":\"" as *u8, 11, pos) 180 if bu < 0 { go = 0 } 181 else { 182 let nextbu: i64 = ms_find(arr, alen, "\"baseUrl\":\"" as *u8, 11, bu + 11) 183 var objend: i64 = alen 184 if nextbu >= 0 { objend = nextbu } 185 var mtch: i64 = 0 186 if wlen == 0 { mtch = 1 } 187 else { 188 let lc: i64 = ms_find(arr, objend, "\"languageCode\":\"" as *u8, 16, bu) 189 if lc >= 0 { 190 var i: i64 = lc + 16; var j: i64 = 0; var eq: i64 = 1 191 while j < wlen { if (arr[i+j]&0xff) != (want[j]&0xff) { eq = 0; break } j = j + 1 } 192 if eq == 1 { if (arr[i+wlen]&0xff) == 34 { mtch = 1 } } 193 } 194 } 195 if mtch == 1 { 196 let ulen: i64 = ytc_copy_jsonstr(arr, bu + 11, objend, out_url, 0, ucap, 0, eidx) 197 out_url[ulen] = 0 as u8 198 let kd: i64 = ms_find(arr, objend, "\"kind\":\"" as *u8, 8, bu) 199 if kd >= 0 { 200 var i2: i64 = kd + 8; var o2: i64 = 0 201 while i2 < objend { let c: i64 = arr[i2]&0xff; if c == 34 { break } if o2 < kcap - 1 { out_kind[o2] = c as u8; o2 = o2 + 1 } i2 = i2 + 1 } 202 out_kind[o2] = 0 as u8 203 } else { 204 out_kind[0]=109 as u8; out_kind[1]=97 as u8; out_kind[2]=110 as u8; out_kind[3]=117 as u8; out_kind[4]=97 as u8; out_kind[5]=108 as u8; out_kind[6]=0 as u8 // "manual" 205 } 206 return 1 207 } 208 pos = bu + 11 209 } 210 } 211 if wlen == 0 { return 0 } 212 return 0 - 2 213} 214// List the languageCodes present (comma-separated), for the LANG-ABSENT refusal. Returns count. 215func ytc_langs(arr: *u8, alen: i64, out: *u8, cap: i64) -> i64 { 216 var pos: i64 = 0; var o: i64 = 0; var n: i64 = 0 217 var go: i64 = 1 218 while go == 1 { 219 let lc: i64 = ms_find(arr, alen, "\"languageCode\":\"" as *u8, 16, pos) 220 if lc < 0 { go = 0 } 221 else { 222 var i: i64 = lc + 16 223 if n > 0 { if o < cap - 1 { out[o] = 44 as u8; o = o + 1 } } 224 while i < alen { let c: i64 = arr[i]&0xff; if c == 34 { break } if o < cap - 1 { out[o] = c as u8; o = o + 1 } i = i + 1 } 225 n = n + 1 226 pos = lc + 16 227 } 228 } 229 out[o] = 0 as u8 230 return n 231} 232 233// ---- transports (compose the shipped stack; no new TLS) ------------------------------------------ 234func ytc_load_store() -> i64 { 235 return nx_trust_store_load_from_certdata("data/mozilla_certdata.txt" as *u8, 512, YTC_CERTDATA_CAP) 236} 237func ytc_cat(dst: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } return o } 238// THE visionOS InnerTube client (X-Youtube-Client-Name 101), captured VERBATIM from yt-dlp's own /youtubei 239// traffic 2026-09-18: ANDROID_VR now answers playabilityStatus=LOGIN_REQUIRED ("Sign in to confirm you're 240// not a bot"), but the visionOS player response returns a captionTracks baseUrl that serves a body with NO 241// proof-of-origin token -- the one client context measured to work today. Facts, not memory. 242const YTC_VOS_UA: *u8 = "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15" 243func ytc_vos_body(vid: *u8, vidlen: i64, vd: *u8, vdlen: i64, out: *u8, cap: i64) -> i64 { 244 var o: i64 = ytc_cat(out, 0, "{\"context\":{\"client\":{\"clientName\":\"VISIONOS\",\"clientVersion\":\"1.02\",\"deviceMake\":\"Apple\",\"deviceModel\":\"RealityDevice17,1\",\"userAgent\":\"" as *u8) 245 o = ytc_cat(out, o, YTC_VOS_UA) 246 o = ytc_cat(out, o, "\",\"osName\":\"visionOS\",\"osVersion\":\"26.5.23O471\",\"hl\":\"en\",\"timeZone\":\"UTC\",\"utcOffsetMinutes\":0" as *u8) 247 if vdlen > 0 { o = ytc_cat(out, o, ",\"visitorData\":\"" as *u8); var d: i64 = 0; while d < vdlen { out[o] = vd[d]; o = o + 1; d = d + 1 } o = ytc_cat(out, o, "\"" as *u8) } 248 o = ytc_cat(out, o, "}},\"videoId\":\"" as *u8) 249 var i: i64 = 0; while i < vidlen { out[o] = vid[i]; o = o + 1; i = i + 1 } 250 o = ytc_cat(out, o, "\",\"playbackContext\":{\"contentPlaybackContext\":{\"html5Preference\":\"HTML5_PREF_WANTS\"}},\"contentCheckOk\":true,\"racyCheckOk\":true}" as *u8) 251 return o 252} 253func ytc_vos_req(vd: *u8, vdlen: i64, body: *u8, bl: i64, out: *u8) -> i64 { 254 var o: i64 = ytc_cat(out, 0, "POST /youtubei/v1/player?prettyPrint=false HTTP/1.1\r\nHost: www.youtube.com\r\nUser-Agent: " as *u8) 255 o = ytc_cat(out, o, YTC_VOS_UA) 256 o = ytc_cat(out, o, "\r\nContent-Type: application/json\r\nX-Youtube-Client-Name: 101\r\nX-Youtube-Client-Version: 1.02\r\nOrigin: https://www.youtube.com\r\n" as *u8) 257 if vdlen > 0 { o = ytc_cat(out, o, "X-Goog-Visitor-Id: " as *u8); var d: i64 = 0; while d < vdlen { out[o] = vd[d]; o = o + 1; d = d + 1 } o = ytc_cat(out, o, "\r\n" as *u8) } 258 o = ytc_cat(out, o, "Accept: */*\r\nAccept-Encoding: identity\r\nContent-Length: " as *u8) 259 o = ytc_catn(out, o, bl, 16384) 260 o = ytc_cat(out, o, "\r\nConnection: close\r\n\r\n" as *u8) 261 var i: i64 = 0; while i < bl { out[o] = body[i]; o = o + 1; i = i + 1 } 262 return o 263} 264// Strip HTTP headers and de-chunk a raw response into out (identity encoding requested, so no gzip). 265func ytc_dechunk(raw: *u8, n: i64, out: *u8, cap: i64) -> i64 { 266 let he: i64 = ms_find(raw, n, "\r\n\r\n" as *u8, 4, 0) 267 if he < 0 { var i: i64 = 0; while i < n { if i < cap - 1 { out[i] = raw[i] } i = i + 1 } out[n] = 0 as u8; return n } 268 let bodystart: i64 = he + 4 269 let chunked: i64 = ms_find(raw, bodystart, "chunked" as *u8, 7, 0) 270 if chunked < 0 { var o: i64 = 0; var i: i64 = bodystart; while i < n { if o < cap - 1 { out[o] = raw[i]; o = o + 1 } i = i + 1 } out[o] = 0 as u8; return o } 271 var i: i64 = bodystart; var o: i64 = 0; var go: i64 = 1 272 while go == 1 { 273 var sz: i64 = 0; var got: i64 = 0 274 while i < n { let c: i64 = raw[i]&0xff; if c == 13 { break } sz = sz * 16 + ytc_hexval(c); got = 1; i = i + 1 } 275 if i + 1 < n { if (raw[i]&0xff) == 13 { i = i + 2 } } 276 if got == 0 { go = 0 } else { if sz == 0 { go = 0 } else { 277 var k: i64 = 0 278 while k < sz { if i < n { if o < cap - 1 { out[o] = raw[i]; o = o + 1 } i = i + 1 } else { k = sz } k = k + 1 } 279 if i + 1 < n { if (raw[i]&0xff) == 13 { i = i + 2 } } 280 } } 281 } 282 out[o] = 0 as u8; return o 283} 284// POST the visionOS player request to www.youtube.com; returns RAW response bytes (headers + chunked body). 285func ytc_vos_post(vid: *u8, vidlen: i64, vd: *u8, vdlen: i64, store: *TrustStore, cr: *u8, pk: *u8, now: i64, out: *u8, cap: i64) -> i64 { 286 let host_url: *u8 = "https://www.youtube.com/youtubei/v1/player?prettyPrint=false" as *u8 287 let url_p: *NxUrl = nx_url_new() 288 let target_raw: *u8 = sys_mmap(32); let target: *NxHttpsTarget = target_raw as *NxHttpsTarget 289 target.url = url_p; target.port = 0 290 if nx_https_url_for_fetch(host_url, target) != NX_HTTPS_URL_OK { return 0 - 1 } 291 let fd_p: *i64 = sys_mmap(16) as *i64 292 if nx_https_url_connect(target, host_url, now, fd_p) != NX_HTTPS_CONNECT_OK { return 0 - 2 } 293 let fd: i64 = *fd_p 294 let val_ctx_raw: *u8 = sys_mmap(64); let val_ctx: *TlsValidationContext = val_ctx_raw as *TlsValidationContext 295 val_ctx.store = store; val_ctx.sni_host = host_url + target.url.host_off; val_ctx.sni_host_len = target.url.host_len; val_ctx.now_epoch = now 296 let session_r: i64 = nx_tls13_client_session_run(fd, host_url + target.url.host_off, target.url.host_len, cr, pk, val_ctx) 297 if session_r < 0 { sys_close(fd); return 0 - 3 } 298 let session: *Tls13ClientSession = session_r as *Tls13ClientSession 299 let body: *u8 = sys_mmap(8192); let bl: i64 = ytc_vos_body(vid, vidlen, vd, vdlen, body, 8192) 300 let req: *u8 = sys_mmap(16384); let req_len: i64 = ytc_vos_req(vd, vdlen, body, bl, req) 301 let nresp: i64 = nx_https_req_complete(session, fd, req, req_len, out, cap) 302 sys_close(fd) 303 return nresp 304} 305// Player response via the visionOS client: fetch the watch page for visitorData, POST visionOS, de-chunk. 306func ytc_player(url: *u8, ulen: i64, store: *TrustStore, out: *u8, cap: i64) -> i64 { 307 let vid: *u8 = sys_mmap(64); let idl: i64 = yt_video_id(url, ulen, vid, 64) 308 if idl == 0 { return 0 - 11 } 309 let cr: *u8 = sys_mmap(32); nx_csprng_fill(cr, 32) 310 let pk: *u8 = sys_mmap(32); nx_csprng_fill(pk, 32) 311 let now: i64 = sys_now_realtime_sec() 312 let html: *u8 = sys_mmap(YTC_PLAYER_CAP); let status: *i64 = sys_mmap(8) as *i64 313 let hn: i64 = nx_https_fetch_follow(url, store, html, YTC_PLAYER_CAP, 6, status) 314 let vd: *u8 = sys_mmap(4096); var vdlen: i64 = 0 315 if hn > 0 { vdlen = yt_scrape(html, hn, "\"visitorData\":\"" as *u8, vd, 4096) } 316 let raw: *u8 = sys_mmap(YTC_PLAYER_CAP) 317 let rn: i64 = ytc_vos_post(vid, idl, vd, vdlen, store, cr, pk, now, raw, YTC_PLAYER_CAP) 318 if rn <= 0 { return rn } 319 return ytc_dechunk(raw, rn, out, cap) 320} 321// GET the (NUL-terminated) timedtext baseUrl. status[0] = final HTTP status. Returns body bytes (dechunked 322// and gunzipped by the shared chokepoint). 323func ytc_fetch(url: *u8, store: *TrustStore, out: *u8, cap: i64, status: *i64) -> i64 { 324 return nx_https_fetch_follow(url, store, out, cap, 6, status) 325} 326 327// ---- body classification + parsers --------------------------------------------------------------- 328// 0 json3 | 1 srv1/xml | 2 EMPTY (the pot-token defect: status 200, no bytes) | 3 present-but-not-caption. 329func ytc_classify_body(body: *u8, blen: i64) -> i64 { 330 if blen <= 0 { return 2 } 331 if ms_find(body, blen, "\"wireMagic\"" as *u8, 11, 0) >= 0 { return 0 } // json3 signature (ws-tolerant) 332 if ms_find(body, blen, "\"events\"" as *u8, 8, 0) >= 0 { return 0 } 333 if ms_find(body, blen, "<text " as *u8, 6, 0) >= 0 { return 1 } 334 if ms_find(body, blen, "<transcript" as *u8, 11, 0) >= 0 { return 1 } 335 return 3 336} 337// skip a ':' and any following JSON whitespace, returning the index of the value's first byte. 338func ytc_after_colon(b: *u8, i0: i64, end: i64) -> i64 { 339 var i: i64 = i0 340 while i < end { let c: i64 = b[i]&0xff; if c == 58 { i = i + 1 } else { if c == 32 { i = i + 1 } else { if c == 10 { i = i + 1 } else { if c == 9 { i = i + 1 } else { if c == 13 { i = i + 1 } else { return i } } } } } } 341 return end 342} 343// json3 -> rows `startMs \t durMs \t text \n`. WHITESPACE-TOLERANT: the timedtext json3 is pretty-printed 344// ("tStartMs": 0, "utf8": "..."), so keys are matched alone and the value is read after colon+whitespace. 345// Events with no utf8 seg (window/format-only events) are SKIPPED so the count matches the visible lines. 346func ytc_json3_to_tsv(body: *u8, blen: i64, out: *u8, cap: i64, out_nseg: *i64) -> i64 { 347 var o: i64 = 0; var nseg: i64 = 0 348 var pos: i64 = ms_find(body, blen, "\"events\"" as *u8, 8, 0) 349 if pos < 0 { out[0] = 0 as u8; out_nseg[0] = 0; return 0 } 350 pos = pos + 8 351 let eidx: *i64 = sys_mmap(16) as *i64 352 var go: i64 = 1 353 while go == 1 { 354 let ts: i64 = ms_find(body, blen, "\"tStartMs\"" as *u8, 10, pos) 355 if ts < 0 { go = 0 } 356 else { 357 var i: i64 = ytc_after_colon(body, ts + 10, blen); var startv: i64 = 0 358 while i < blen { let c: i64 = body[i]&0xff; if c < 48 { break } if c > 57 { break } startv = startv * 10 + (c - 48); i = i + 1 } 359 let nextev: i64 = ms_find(body, blen, "\"tStartMs\"" as *u8, 10, i) 360 var evend: i64 = blen 361 if nextev >= 0 { evend = nextev } 362 let firstseg: i64 = ms_find(body, evend, "\"utf8\"" as *u8, 6, ts) 363 if firstseg < 0 { pos = evend } // no text in this event -> skip 364 else { 365 var durv: i64 = 0 366 let dp: i64 = ms_find(body, evend, "\"dDurationMs\"" as *u8, 13, ts) 367 if dp >= 0 { var di: i64 = ytc_after_colon(body, dp + 13, evend); while di < evend { let c: i64 = body[di]&0xff; if c < 48 { break } if c > 57 { break } durv = durv * 10 + (c - 48); di = di + 1 } } 368 o = ytc_catn(out, o, startv, cap) 369 if o < cap - 1 { out[o] = 9 as u8; o = o + 1 } 370 o = ytc_catn(out, o, durv, cap) 371 if o < cap - 1 { out[o] = 9 as u8; o = o + 1 } 372 var seg: i64 = firstseg 373 while seg >= 0 { 374 let q: i64 = ytc_after_colon(body, seg + 6, evend) 375 var nxt: i64 = q + 1 376 if q < evend { if (body[q]&0xff) == 34 { o = ytc_copy_jsonstr(body, q + 1, evend, out, o, cap, 1, eidx); nxt = eidx[0] + 1 } } 377 seg = ms_find(body, evend, "\"utf8\"" as *u8, 6, nxt) 378 } 379 if o < cap - 1 { out[o] = 10 as u8; o = o + 1 } 380 nseg = nseg + 1 381 pos = evend 382 } 383 } 384 } 385 out[o] = 0 as u8; out_nseg[0] = nseg; return o 386} 387// seconds float "12.34" -> milliseconds integer (3 decimal places, padded). 388func ytc_secs_to_ms(src: *u8, s: i64, e: i64) -> i64 { 389 var i: i64 = s; var whole: i64 = 0 390 while i < e { let c: i64 = src[i]&0xff; if c == 46 { break } if c >= 48 { if c <= 57 { whole = whole * 10 + (c - 48) } } i = i + 1 } 391 var frac: i64 = 0; var digits: i64 = 0 392 if i < e { if (src[i]&0xff) == 46 { i = i + 1; while i < e { if digits >= 3 { break } let c: i64 = src[i]&0xff; if c < 48 { break } if c > 57 { break } frac = frac * 10 + (c - 48); digits = digits + 1; i = i + 1 } } } 393 while digits < 3 { frac = frac * 10; digits = digits + 1 } 394 return whole * 1000 + frac 395} 396// srv1/XML `<text start="s" dur="s">TEXT</text>` -> rows. out_nseg[0] = rows. Returns bytes. 397func ytc_xml_to_tsv(body: *u8, blen: i64, out: *u8, cap: i64, out_nseg: *i64) -> i64 { 398 var o: i64 = 0; var nseg: i64 = 0; var pos: i64 = 0 399 var go: i64 = 1 400 while go == 1 { 401 let tp: i64 = ms_find(body, blen, "<text " as *u8, 6, pos) 402 if tp < 0 { go = 0 } 403 else { 404 var tg: i64 = tp 405 while tg < blen { if (body[tg]&0xff) == 62 { break } tg = tg + 1 } // '>' ends the open tag 406 var startv: i64 = 0 - 1 407 let sp: i64 = ms_find(body, blen, "start=\"" as *u8, 7, tp) 408 if sp >= 0 { if sp < tg { var se: i64 = sp + 7; var e2: i64 = se; while e2 < tg { if (body[e2]&0xff) == 34 { break } e2 = e2 + 1 } startv = ytc_secs_to_ms(body, se, e2) } } 409 var durv: i64 = 0 410 let dpp: i64 = ms_find(body, blen, "dur=\"" as *u8, 5, tp) 411 if dpp >= 0 { if dpp < tg { var de: i64 = dpp + 5; var e3: i64 = de; while e3 < tg { if (body[e3]&0xff) == 34 { break } e3 = e3 + 1 } durv = ytc_secs_to_ms(body, de, e3) } } 412 if startv < 0 { startv = 0 } 413 let te: i64 = ms_find(body, blen, "</text>" as *u8, 7, tg) 414 var txend: i64 = blen 415 if te >= 0 { txend = te } 416 o = ytc_catn(out, o, startv, cap) 417 if o < cap - 1 { out[o] = 9 as u8; o = o + 1 } 418 o = ytc_catn(out, o, durv, cap) 419 if o < cap - 1 { out[o] = 9 as u8; o = o + 1 } 420 o = ytc_html_decode(body, tg + 1, txend, out, o, cap, 1) 421 if o < cap - 1 { out[o] = 10 as u8; o = o + 1 } 422 nseg = nseg + 1 423 if te >= 0 { pos = te + 7 } else { pos = tg + 1 } 424 } 425 } 426 out[o] = 0 as u8; out_nseg[0] = nseg; return o 427} 428 429// ---- gate + referee helpers ---------------------------------------------------------------------- 430// count rows (newline-terminated) in a tsv buffer. 431func ytc_tsv_nseg(tsv: *u8, len: i64) -> i64 { var n: i64 = 0; var i: i64 = 0; while i < len { if (tsv[i]&0xff) == 10 { n = n + 1 } i = i + 1 } return n } 432// non-decreasing startMs across rows? 1 yes, 0 no. Each row: startMs is the leading integer. 433func ytc_tsv_monotonic(tsv: *u8, len: i64) -> i64 { 434 var i: i64 = 0; var prev: i64 = 0 - 1 435 while i < len { 436 var v: i64 = 0 437 while i < len { let c: i64 = tsv[i]&0xff; if c < 48 { break } if c > 57 { break } v = v * 10 + (c - 48); i = i + 1 } 438 if v < prev { return 0 } 439 prev = v 440 while i < len { if (tsv[i]&0xff) == 10 { break } i = i + 1 } // skip to end of row 441 i = i + 1 442 } 443 return 1 444} 445// normalise whitespace: collapse any run of space/tab/CR/LF to a single space, trim ends. Returns len. 446func ytc_norm_ws(src: *u8, slen: i64, dst: *u8, cap: i64) -> i64 { 447 var i: i64 = 0; var o: i64 = 0; var pend: i64 = 0; var seen: i64 = 0 448 while i < slen { 449 let c: i64 = src[i]&0xff 450 if c == 32 { pend = 1 } else { if c == 9 { pend = 1 } else { if c == 10 { pend = 1 } else { if c == 13 { pend = 1 } else { 451 if seen == 1 { if pend == 1 { if o < cap - 1 { dst[o] = 32 as u8; o = o + 1 } } } 452 pend = 0; seen = 1 453 if o < cap - 1 { dst[o] = c as u8; o = o + 1 } 454 } } } } 455 i = i + 1 456 } 457 dst[o] = 0 as u8; return o 458} 459// text of the Nth data row (0-based; a data row starts with a digit) into dst; returns len, -1 if absent. 460func ytc_row_text(tsv: *u8, len: i64, idx: i64, dst: *u8, cap: i64) -> i64 { 461 var i: i64 = 0; var row: i64 = 0 462 while i < len { 463 let ls: i64 = i 464 var le: i64 = i 465 while le < len { if (tsv[le]&0xff) == 10 { break } le = le + 1 } 466 let c0: i64 = tsv[ls]&0xff 467 var isdata: i64 = 0 468 if c0 >= 48 { if c0 <= 57 { isdata = 1 } } 469 if isdata == 1 { 470 if row == idx { 471 var t: i64 = ls; var tabs: i64 = 0 472 while t < le { if (tsv[t]&0xff) == 9 { tabs = tabs + 1; if tabs == 2 { t = t + 1; break } } t = t + 1 } 473 var o: i64 = 0 474 while t < le { if o < cap - 1 { dst[o] = tsv[t]; o = o + 1 } t = t + 1 } 475 dst[o] = 0 as u8; return o 476 } 477 row = row + 1 478 } 479 i = le + 1 480 } 481 return 0 - 1 482}