import { Config } from "./config.ts"; const ws = new WebSocket(Deno.args[0]) let config: Config = {} as unknown as Config export class TextLineStream extends TransformStream { line = ""; constructor() { super({ transform: (chunk, controller) => { this.line += chunk; while (true) { const newline = this.line.indexOf("\n"); if (newline === -1) break; controller.enqueue(this.line.slice(0, newline)); this.line = this.line.slice(newline + 1); } }, flush: (controller) => { if (this.line === "") return; controller.enqueue(this.line); }, }); } } const supported = [ "youtube", "bilibili", "niconico", ] function key_to_url(key: string): string { const [kind, id] = key.split(":", 2) if (kind == "youtube") return `https://www.youtube.com/watch?v=${id}` if (kind == "bilibili") return `https://www.bilibili.com/video/${id}` if (kind == "niconico") return `https://www.nicovideo.jp/watch/${id}` throw new Error("unknown kind"); } async function do_download(key: string, data: { [key: string]: string }) { const url = key_to_url(key) if (!data.output) throw new Error("no output"); const output = data.output as string; if (!data.profile) throw new Error("no profile"); const args = config.ytdlp_download.profiles[data.profile as string] if (!args) throw new Error(`unknown profile ${data.profile}`); await Deno.mkdir(output, { recursive: true }) const child = new Deno.Command("yt-dlp", { args: [ "--quiet", "--progress", "--progress-template", "%(progress)j", "--newline", ...args, url ], stdout: "piped", stderr: "piped", cwd: output }).spawn() let fail_reason = "unknown" Promise.all([ (async () => { const lines = child.stderr.pipeThrough(new TextDecoderStream()).pipeThrough(new TextLineStream()) for await (const line of lines) { if (!line.length) continue if (line.includes("members-only content")) fail_reason = "members_only" if (line.includes("Sign in to confirm") && line.includes(" not a bot")) fail_reason = "bot" if (line.includes("Sign in to confirm your age")) fail_reason = "age_restricted" console.error("ytdlp: " + line); } })(), (async () => { const lines = child.stdout.pipeThrough(new TextDecoderStream()).pipeThrough(new TextLineStream()) for await (const line of lines) { if (!line.length) continue const k = JSON.parse(line) ws.send(JSON.stringify({ t: "metadata", key, data: { progress: (k._percent ?? 0) / 100, status: k._default_template?.trim() ?? "" } })) } })() ]) const status = await child.status if (!status.success) ws.send(JSON.stringify({ t: "metadata", key, data: { failed: fail_reason } })) ws.send(JSON.stringify({ t: "metadata", key, data: { progress: null, status: null } })) ws.send(JSON.stringify({ t: "complete", key })) ws.send(JSON.stringify({ t: "save" })) } ws.onerror = () => console.error("ws error") ws.onclose = () => { console.error("ws closed") Deno.exit(1) } ws.onopen = () => { console.log("ws open"); ws.send(JSON.stringify({ t: "register", name: "yt-dlp video downloader", task_kinds: supported })) ws.send(JSON.stringify({ t: "accept" })) } ws.onmessage = async ev => { if (typeof ev.data != "string") return const p = JSON.parse(ev.data) if (p.t == "config") config = p.config if (p.t == "error") console.error(`error: ${p.message}`); if (p.t == "work") { console.log("work"); await do_download(p.key, p.data) console.log("accept"); ws.send(JSON.stringify({ t: "accept" })) } }