// credit: @ShanMolvyr / @Shann import https from "https"; import { randomUUID } from "crypto"; const BASE = "metube.io"; const UA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Mobile Safari/537.36"; const _log = (msg) => process.stderr.write(`\r\x1b[K[metube] ${msg}`); const _done = () => process.stderr.write("\n"); const _req = (method, path, body, extra = {}) => new Promise((resolve, reject) => { const payload = body ? JSON.stringify(body) : null; const opts = { hostname: BASE, path, method, headers: { "User-Agent": UA, Accept: "application/json, text/plain, */*", "Accept-Language": "en-US,en;q=0.9", "Content-Type": "application/json", Referer: `https://${BASE}/`, Origin: `https://${BASE}`, ...extra, }, }; if (payload) opts.headers["Content-Length"] = Buffer.byteLength(payload); const req = https.request(opts, (res) => { let raw = ""; res.on("data", (c) => (raw += c)); res.on("end", () => { try { resolve(JSON.parse(raw)); } catch { resolve(raw); } }); }); req.on("error", reject); if (payload) req.write(payload); req.end(); }); const _sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const _headers = (visitorId) => ({ "x-visitor-id": visitorId, Cookie: `Next-Locale=en; sy_visitor_id=${visitorId}`, }); const _get = (path, vid) => _req("GET", path, null, _headers(vid)); const _post = (path, body, vid) => _req("POST", path, body, _headers(vid)); const _pollTask = async (taskId, vid) => { let i = 0; while (true) { const r = await _get(`/api/v1/tasks/${taskId}`, vid).then((x) => x?.data); if (r?.status === "SUCCESS") return r; if (r?.status === "FAILED") throw new Error("Task gagal: " + r.error_message); _log(`Memproses video... (${++i * 4}s)`); await _sleep(4000); } }; const _pollNotes = async (videoId, language, vid) => { let i = 0; while (true) { const r = await _get( `/api/v1/videos/${videoId}/generated-notes?output_language=${language}&limit=20`, vid ).then((x) => x?.data); const pct = r?.active_task?.processing_percent ?? 0; const msg = r?.active_task?.processing_message ?? "generating..."; if (r?.items?.length > 0 && r?.active_task === null) return r; _log(`Generating summary... ${pct}% — ${msg}`); await _sleep(5000); if (++i > 60) throw new Error("Polling notes timeout"); } }; const summarize = async (url, language = "en") => { const vid = randomUUID(); _log("Mengirim request..."); const processRes = await _post( "/api/v1/videos/process", { url, language }, vid ); if (!processRes?.data?.id) { _done(); throw new Error("Gagal memproses video: " + JSON.stringify(processRes)); } const { id: taskId, related_id: videoId, status, parameters } = processRes.data; if (status !== "SUCCESS") { _log("Memproses video..."); await _pollTask(taskId, vid); } const notes = await _pollNotes(videoId, language, vid); _log("Mengambil transcript & summary..."); const [chunksRes, summaryRes] = await Promise.all([ _get(`/api/v1/videos/${videoId}/chunks?language=${language}`, vid), _get(`/api/v1/videos/${videoId}/summary?language=${language}&limit=20`, vid), ]); _done(); const note = notes.items?.[0]; const transcript = chunksRes?.data?.video_chunks?.[0]?.chunks?.map((c) => ({ id: c.id, time_range: c.time_range, start_ms: c.start_ms, end_ms: c.end_ms, text: c.chunk_text, })) ?? []; return { by: "ShanMolvyr", video_id: videoId, youtube_video_id: parameters?.youtube_video_id ?? null, title: summaryRes?.data?.title ?? null, language, summary_type: note?.prompt_slug ?? null, summary_name: note?.prompt_name ?? null, model_used: note?.model_used ?? null, content_markdown: note?.content_markdown ?? null, transcript, created_at: note?.created_at ?? null, }; }; const main = async () => { const [url, lang = "en"] = process.argv.slice(2); if (!url) { console.error("Usage: node metube.js [language]"); process.exit(1); } try { const result = await summarize(url, lang); console.log(JSON.stringify(result, null, 2)); } catch (err) { _done(); console.error(JSON.stringify({ error: err.message }, null, 2)); process.exit(1); } }; main();