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}