code wiki / _hdl_build / nx_yt_innertube.nx
nx_yt_innertube.nx source
↩ module page · 103 lines · 8291 B
1// nx_yt_innertube.nx -- the RELIABLE YouTube path, done yt-dlp's way: SCRAPE the live INNERTUBE_API_KEY +
2// visitorData from the watch page's ytcfg (they rotate -- hardcoding them precondition-fails), then POST the
3// ANDROID-client /youtubei/v1/player request with the scraped key + visitorData, over our own TLS-1.3. Parse
4// streamingData.formats[].url (progressive, directly playable). This self-heals against Google's key rotation --
5// the same technique yt-dlp uses. license_tier: ORIGINAL
6import "nx_syscalls.nx"
7import "nx_https_get_spoof.nx" // watch-page fetch (browser headers) + connect/handshake machinery + trust types
8import "nx_https_get_complete.nx" // nx_https_req_complete (send a prebuilt request over the TLS session)
9import "nx_media_state.nx" // ms_find (substring)
10
11func yt_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
12func yt_puts(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){dst[off]=s[i];off=off+1;i=i+1} return off }
13func yt_putn(dst: *u8, off: i64, v: i64) -> i64 { var m: i64=v; if m==0 { dst[off]=48 as u8; return off+1 } let t: *u8=sys_mmap(24); var k: i64=0; while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 } var j: i64=0; while j<k { dst[off]=t[k-1-j]; off=off+1; j=j+1 } return off }
14
15func yt_video_id(url: *u8, ulen: i64, out: *u8, cap: i64) -> i64 {
16 var s: i64 = ms_find(url, ulen, "v=" as *u8, 2, 0)
17 if s >= 0 { s = s + 2 } else { s = ms_find(url, ulen, "youtu.be/" as *u8, 9, 0); if s >= 0 { s = s + 9 } }
18 if s < 0 { out[0]=0 as u8; return 0 }
19 var o: i64 = 0; var i: i64 = s; var go: i64 = 1
20 while go == 1 { if i >= ulen { go = 0 } else { let c: i64 = url[i]&0xff; if c==38 { go=0 } else { if c==63 { go=0 } else { if c==47 { go=0 } else { if o<(cap-1) { out[o]=c as u8; o=o+1 } i=i+1 } } } } }
21 out[o]=0 as u8; return o
22}
23// scrape a JSON string field value: find `"<field>":"` then copy until the closing quote (values here -- api key,
24// visitorData -- contain no embedded quotes). Returns len (0 if absent).
25func yt_scrape(html: *u8, hlen: i64, field: *u8, out: *u8, cap: i64) -> i64 {
26 let fl: i64 = yt_slen(field)
27 let p: i64 = ms_find(html, hlen, field, fl, 0)
28 if p < 0 { out[0]=0 as u8; return 0 }
29 var i: i64 = p + fl; var o: i64 = 0; var go: i64 = 1
30 while go == 1 { if i >= hlen { go = 0 } else { let c: i64 = html[i]&0xff; if c == 34 { go = 0 } else { if o < (cap-1) { out[o]=c as u8; o=o+1 } i=i+1 } } }
31 out[o]=0 as u8; return o
32}
33// build the ANDROID player body with the scraped visitorData (a required precondition).
34func yt_build_body(vid: *u8, vidlen: i64, vd: *u8, vdlen: i64, out: *u8, cap: i64) -> i64 {
35 var o: i64 = yt_puts(out, 0, "{\"context\":{\"client\":{\"clientName\":\"ANDROID_VR\",\"clientVersion\":\"1.60.19\",\"deviceMake\":\"Oculus\",\"deviceModel\":\"Quest 3\",\"androidSdkVersion\":32,\"osName\":\"Android\",\"osVersion\":\"12L\",\"hl\":\"en\",\"gl\":\"US\",\"utcOffsetMinutes\":0" as *u8)
36 if vdlen > 0 { o = yt_puts(out, o, ",\"visitorData\":\"" as *u8); var d: i64=0; while d<vdlen { out[o]=vd[d]; o=o+1; d=d+1 } o = yt_puts(out, o, "\"" as *u8) }
37 o = yt_puts(out, o, ",\"userAgent\":\"com.google.android.apps.youtube.vr.oculus/1.60.19 (Linux; U; Android 12L; en_US; Quest 3 Build/SQ3A.220605.009.A1) gzip\"}},\"videoId\":\"" as *u8)
38 var i: i64 = 0; while i < vidlen { out[o]=vid[i]; o=o+1; i=i+1 }
39 o = yt_puts(out, o, "\",\"playbackContext\":{\"contentPlaybackContext\":{\"html5Preference\":\"HTML5_PREF_WANTS\"}},\"contentCheckOk\":true,\"racyCheckOk\":true}" as *u8)
40 out[o]=0 as u8; return o
41}
42// build the raw POST with the scraped api key in the query.
43func yt_build_post(host: *u8, hlen: i64, key: *u8, keylen: i64, vd: *u8, vdlen: i64, body: *u8, blen: i64, out: *u8) -> i64 {
44 var o: i64 = yt_puts(out, 0, "POST /youtubei/v1/player?key=" as *u8)
45 var kk: i64 = 0; while kk < keylen { out[o]=key[kk]; o=o+1; kk=kk+1 }
46 o = yt_puts(out, o, "&prettyPrint=false HTTP/1.1\r\nHost: " as *u8)
47 var i: i64 = 0; while i < hlen { out[o]=host[i]; o=o+1; i=i+1 }
48 o = yt_puts(out, o, "\r\nUser-Agent: com.google.android.apps.youtube.vr.oculus/1.60.19 (Linux; U; Android 12L; en_US; Quest 3 Build/SQ3A.220605.009.A1) gzip\r\n" as *u8)
49 o = yt_puts(out, o, "Content-Type: application/json\r\nX-Goog-Api-Format-Version: 2\r\nX-YouTube-Client-Name: 28\r\nX-YouTube-Client-Version: 1.60.19\r\n" as *u8)
50 if vdlen > 0 { o = yt_puts(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 = yt_puts(out, o, "\r\n" as *u8) }
51 o = yt_puts(out, o, "Accept: */*\r\nAccept-Encoding: identity\r\nContent-Length: " as *u8)
52 o = yt_putn(out, o, blen)
53 o = yt_puts(out, o, "\r\nConnection: close\r\n\r\n" as *u8)
54 i = 0; while i < blen { out[o]=body[i]; o=o+1; i=i+1 }
55 return o
56}
57// POST an InnerTube android player request over a fresh TLS session to `host`. Returns raw response bytes.
58func yt_post(host_url: *u8, key: *u8, keylen: i64, vd: *u8, vdlen: i64, body: *u8, blen: i64, store: *TrustStore, cr: *u8, pk: *u8, now: i64, out: *u8, cap: i64) -> i64 {
59 let url_p: *NxUrl = nx_url_new()
60 let target_raw: *u8 = sys_mmap(32); let target: *NxHttpsTarget = target_raw as *NxHttpsTarget
61 target.url = url_p; target.port = 0
62 if nx_https_url_for_fetch(host_url, target) != NX_HTTPS_URL_OK { return 0 - 1 }
63 let fd_p: *i64 = sys_mmap(16) as *i64
64 if nx_https_url_connect(target, host_url, now, fd_p) != NX_HTTPS_CONNECT_OK { return 0 - 2 }
65 let fd: i64 = *fd_p
66 let val_ctx_raw: *u8 = sys_mmap(64); let val_ctx: *TlsValidationContext = val_ctx_raw as *TlsValidationContext
67 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
68 let session_r: i64 = nx_tls13_client_session_run(fd, host_url + target.url.host_off, target.url.host_len, cr, pk, val_ctx)
69 if session_r < 0 { sys_close(fd); return 0 - 3 }
70 let session: *Tls13ClientSession = session_r as *Tls13ClientSession
71 let req: *u8 = sys_mmap(8192)
72 let req_len: i64 = yt_build_post(host_url + target.url.host_off, target.url.host_len, key, keylen, vd, vdlen, body, blen, req)
73 let n: i64 = nx_https_req_complete(session, fd, req, req_len, out, cap)
74 sys_close(fd)
75 return n
76}
77// FULL: fetch the watch page -> scrape live key + visitorData -> POST android player -> raw response into out.
78// ANDROID_VR client: no cookies, no visitorData, no PoToken, no nsig -- returns directly-playable progressive
79// stream URLs today (confirmed by DownloadHelper's shipped ruleset AND the 15-agent PoToken scope). We only need
80// the video id from the url; no watch-page scrape required. (SABR experiment may cap some videos to 360p/itag18.)
81func yt_innertube(watch_url: *u8, wulen: i64, store: *TrustStore, cr: *u8, pk: *u8, now: i64, out: *u8, cap: i64) -> i64 {
82 let vid: *u8 = sys_mmap(64); let idl: i64 = yt_video_id(watch_url, wulen, vid, 64)
83 if idl == 0 { return 0 - 11 }
84 let key: *u8 = "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w" as *u8; let kl: i64 = yt_slen(key) // ANDROID InnerTube key
85 let body: *u8 = sys_mmap(8192); let bl: i64 = yt_build_body(vid, idl, "" as *u8, 0, body, 8192)
86 return yt_post("https://youtubei.googleapis.com/youtubei/v1/player" as *u8, key, kl, "" as *u8, 0, body, bl, store, cr, pk, now, out, cap)
87}
88// pick a directly-playable PROGRESSIVE url from streamingData.formats[] (JSON-\/-unescaped). 0 if none.
89func yt_pick_stream(json: *u8, jlen: i64, out: *u8, cap: i64) -> i64 {
90 var from: i64 = ms_find(json, jlen, "\"formats\":[" as *u8, 11, 0)
91 if from < 0 { from = 0 }
92 let up: i64 = ms_find(json, jlen, "\"url\":\"" as *u8, 7, from)
93 if up < 0 { out[0]=0 as u8; return 0 }
94 var i: i64 = up + 7; var o: i64 = 0; var go: i64 = 1
95 while go == 1 {
96 if i >= jlen { go = 0 }
97 else { let c: i64 = json[i]&0xff
98 if c == 34 { go = 0 }
99 else { if c == 92 { if (i+1)<jlen { if (json[i+1]&0xff)==47 { if o<(cap-1) { out[o]=47 as u8; o=o+1 } i=i+2 } else { if o<(cap-1){out[o]=c as u8;o=o+1} i=i+1 } } else { i=i+1 } }
100 else { if o<(cap-1) { out[o]=c as u8; o=o+1 } i=i+1 } } }
101 }
102 out[o]=0 as u8; return o
103}