#!/usr/bin/env bun import { execFileSync, spawn, spawnSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; import minimist from "minimist"; // URL validation to prevent command injection const VALID_URL_PATTERN = /^https?:\/\/[^\s;|&`$()'"\\]+$/i; const ALLOWED_HOSTS = [ "youtube.com", "youtu.be", "www.youtube.com", "m.youtube.com", "vimeo.com", "www.vimeo.com", "player.vimeo.com", "tiktok.com", "www.tiktok.com", "vm.tiktok.com", "twitter.com", "x.com", "www.twitter.com", "facebook.com", "www.facebook.com", "fb.watch", "instagram.com", "www.instagram.com", "dailymotion.com", "www.dailymotion.com", "twitch.tv", "www.twitch.tv", "clips.twitch.tv", "reddit.com", "www.reddit.com", "v.redd.it", "soundcloud.com", "www.soundcloud.com", "bandcamp.com", "vk.com", "www.vk.com", "bilibili.com", "www.bilibili.com", ]; function validateUrl(url: string): boolean { // Check basic URL pattern (no shell metacharacters) if (!VALID_URL_PATTERN.test(url)) { return false; } try { const parsed = new URL(url); const hostname = parsed.hostname.toLowerCase(); // Check against allowlist (allow subdomains) return ALLOWED_HOSTS.some(allowed => hostname === allowed || hostname.endsWith(`.${allowed}`) ); } catch { return false; } } // Types interface DownloadOptions { quality: string; format: string; codec: string; audioOnly: boolean; audioFormat: string; audioQuality: number; output: string; dir: string; filename: string; subtitles: boolean; subLang: string; subFormat: string; subOnly: boolean; autoSub: boolean; playlist: boolean; playlistStart: number; playlistEnd: number | null; batch: string | null; limit: number | null; thumbnail: boolean; metadata: boolean; chapters: boolean; description: boolean; info: boolean; listFormats: boolean; cookies: string | null; rateLimit: string | null; } // Parse command line arguments const args = minimist(process.argv.slice(2), { string: ["quality", "format", "codec", "audio-format", "output", "dir", "filename", "sub-lang", "sub-format", "batch", "cookies", "rate-limit"], boolean: ["audio-only", "subtitles", "sub-only", "auto-sub", "playlist", "thumbnail", "metadata", "chapters", "description", "info", "list-formats", "help"], default: { quality: "best", format: "mp4", "audio-format": "mp3", "audio-quality": 192, dir: ".skills/exports", filename: "%(title)s", "sub-lang": "en", "sub-format": "srt", "playlist-start": 1, }, alias: { o: "output", h: "help", }, }); // Show help if (args.help) { console.log(` Video Downloader - Download videos from YouTube, Vimeo, TikTok, and 1000+ sites Usage: skills run video-downloader -- [options] skills run video-downloader -- --batch [options] Quality Options: --quality Resolution: best, 4k, 2k, 1080, 720, 480, 360, worst --format Video format: mp4, webm, mkv (default: mp4) --codec Video codec: h264, h265, vp9, av1 Audio Options: --audio-only Download audio only --audio-format Audio format: mp3, m4a, wav, flac, opus (default: mp3) --audio-quality Audio bitrate: 128, 192, 256, 320 (default: 192) Output Options: -o, --output Output file path --dir Output directory (default: .skills/exports) --filename