#!/usr/bin/env node
/*
 * sabr-drain-capture.mjs — final PoC step: drive the FULL SABR loop to EOF
 * using the harvested WEB credentials, proving the whole file drains (not
 * just one chunk like sabr-replay).
 *
 * Reads tools/captured.json (from harvest-pot-console.js). The captured WEB
 * SABR request carries everything bound to one session:
 *   - url   = serverAbrStreamingUrl (signed, sabr=1, c=WEB)
 *   - body  = VideoPlaybackAbrRequest; field 5 = videoPlaybackUstreamerConfig
 *   - pot   = StreamerContext.poToken
 * We feed those (all WEB-bound, no cross-client mismatch) into googlevideo's
 * SabrStream and drain video+audio to completion. Format metadata is pulled
 * from an iOS player POST (lastModified/contentLength are per-transcode, the
 * same across clients for one video).
 *
 *   node tools/sabr-drain-capture.mjs [tools/captured.json] [--video=itag] [--audio=itag]
 */

import { readFile } from 'node:fs/promises';
import { SabrStream } from 'googlevideo/sabr-stream';
import { buildSabrFormat, EnabledTrackTypes, u8ToBase64 } from 'googlevideo/utils';

const flags = Object.fromEntries(process.argv.slice(2).filter(a => a.startsWith('--')).map(a => { const [k, v] = a.replace(/^--/, '').split('='); return [k, v ?? true]; }));
const path = process.argv.slice(2).find(a => !a.startsWith('--')) || new URL('./captured.json', import.meta.url).pathname;
const WANT_VIDEO = flags.video ? Number(flags.video) : 134;
const WANT_AUDIO = flags.audio ? Number(flags.audio) : 140;

const cap = JSON.parse(await readFile(path, 'utf8'));
const body = Uint8Array.from(atob(cap.bodyB64), c => c.charCodeAt(0));
const visitor = decodeURIComponent(cap.visitor);

// --- pull field 5 (videoPlaybackUstreamerConfig, tag 42) out of the body ---
const readVarint = (buf, pos) => { let r = 0, s = 0, b; do { b = buf[pos++]; r += (b & 0x7f) * Math.pow(2, s); s += 7; } while (b & 0x80); return [r, pos]; };
const field = (buf, target) => { let pos = 0; while (pos < buf.length) { let tag; [tag, pos] = readVarint(buf, pos); const f = Math.floor(tag / 8), wt = tag & 7; if (wt === 2) { let len; [len, pos] = readVarint(buf, pos); if (f === target) return buf.subarray(pos, pos + len); pos += len; } else if (wt === 0) [, pos] = readVarint(buf, pos); else if (wt === 1) pos += 8; else if (wt === 5) pos += 4; else return null; } return null; };
const ustreamer = field(body, 5);
if (!ustreamer) { console.error('could not extract ustreamerConfig (field 5) from captured body'); process.exit(1); }

// --- iOS player POST just for format metadata (clen/duration/lastModified) ---
async function iosFormats() {
  const b = { videoId: '', racyCheckOk: true, contentCheckOk: true, context: { client: { clientName: 'iOS', clientVersion: '20.11.6', deviceMake: 'Apple', deviceModel: 'iPhone10,4', osName: 'iOS', osVersion: '16.7.7.20H330', hl: 'en', gl: 'US', visitorData: visitor }, user: {}, request: { useSsl: true } } };
  // recover videoId from the capture's player? not in capture — read from arg or default to known
  b.videoId = flags.id ? String(flags.id) : 'BRfjeH366EY';
  const r = await fetch('https://www.youtube.com/youtubei/v1/player?prettyPrint=false', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Youtube-Client-Name': '5', 'X-Youtube-Client-Version': '20.11.6', 'X-Goog-Visitor-Id': visitor, 'User-Agent': 'com.google.ios.youtube/20.11.6 (iPhone10,4; U; CPU iOS 16_7_7 like Mac OS X)' }, body: JSON.stringify(b) });
  const j = await r.json();
  return j.streamingData?.adaptiveFormats || [];
}

const adaptive = await iosFormats();
const formats = adaptive.map(buildSabrFormat);
const v = adaptive.find(f => f.itag === WANT_VIDEO), a = adaptive.find(f => f.itag === WANT_AUDIO);
console.log(`\n=== sabr-drain-capture  ustreamer=${ustreamer.length}B  pot=${cap.pot.slice(0, 12)}…  formats=${formats.length} ===`);
console.log(`target: video itag ${v?.itag} (clen ${v?.contentLength})  audio itag ${a?.itag} (clen ${a?.contentLength})`);

const headers = { 'Referer': 'https://www.youtube.com/', 'Origin': 'https://www.youtube.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36' };
const stream = new SabrStream({
  fetch: (url, init = {}) => fetch(url, { ...init, headers: { ...(init.headers || {}), ...headers } }),
  serverAbrStreamingUrl: cap.url,
  videoPlaybackUstreamerConfig: u8ToBase64(ustreamer),
  clientInfo: { clientName: 1, clientVersion: '2.20260529.01.00', osName: 'Windows', osVersion: '10.0' },
  poToken: cap.pot,
  formats
});
stream.on('streamProtectionStatusUpdate', s => { if (s.status !== 1) console.log(`  » protection=${s.status}`); });
stream.on('formatInitialization', f => console.log(`  » formatInit itag=${f.formatInitializationMetadata?.formatId?.itag}`));

async function drain(s) { const r = s.getReader(); let b = 0, n = 0; for (;;) { const { done, value } = await r.read(); if (done) break; b += value.byteLength; n++; if (n % 20 === 0) process.stdout.write(`\r  draining… ${b} bytes`); } return b; }

try {
  const { videoStream, audioStream, selectedFormats } = await stream.start({ videoFormat: WANT_VIDEO, audioFormat: WANT_AUDIO, enabledTrackTypes: EnabledTrackTypes.VIDEO_AND_AUDIO });
  const [vb, ab] = await Promise.all([drain(videoStream), drain(audioStream)]);
  const vc = Number(selectedFormats.videoFormat?.contentLength || v?.contentLength || 0);
  const ac = Number(selectedFormats.audioFormat?.contentLength || a?.contentLength || 0);
  console.log(`\n\n=== RESULT ===`);
  console.log(`video itag ${selectedFormats.videoFormat?.itag}: ${vb} / ${vc} bytes  ${vc && vb >= vc * 0.99 ? '✓ FULL' : '✗ SHORT'}`);
  console.log(`audio itag ${selectedFormats.audioFormat?.itag}: ${ab} / ${ac} bytes  ${ac && ab >= ac * 0.99 ? '✓ FULL' : '✗ SHORT'}`);
  const ok = vc && vb >= vc * 0.99 && ac && ab >= ac * 0.99;
  console.log(`\nFULL-FILE SABR DRAIN with harvested creds: ${ok ? '✓✓ WORKS — entire file pulled, 1MB cap defeated' : '✗ incomplete'}\n`);
} catch (e) {
  console.log(`\n*** drain threw: ${e.message} ***\n`);
  process.exitCode = 1;
}
