UNPKG

nr8local

Version:

Personal yt-dlp helper CLI for downloading audio/video and opening music convert links

379 lines (330 loc) 10.6 kB
#!/usr/bin/env node const { execSync, spawnSync } = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); const args = process.argv.slice(2); function printHelp() { console.log(` nr8local - Personal yt-dlp helper tool Usage: nr8local -D3 <video_ids> [resolution/bitrate] [(silent)] Download audio (default mp3, supports other audio formats) nr8local -D4 <video_ids> [resolution] [(silent)] Download video (default mp4) nr8local -D3P <playlist_id> [resolution/bitrate] [(silent)] Download audio playlist nr8local -D4P <playlist_id> [resolution] [(silent)] Download video playlist nr8local -L <service> Open music convert links or specific sites in browser Options: -D3 Download audio by video id(s), separated by commas, defaults to mp3 and highest kbps -D4 Download video by video id(s), separated by commas, defaults to 1080p mp4 -D3P Download audio playlist by playlist id -D4P Download video playlist by playlist id -L Open specific links or services (convert, soupcan, etc.) Resolution options (for video): 240p 360p 480p 720p 1080p 1440p 2160p 4320p Bitrate options (for audio kbps): 128 192 256 320 Add (silent) at the end to suppress command output. Examples: nr8local -D3 abc123,def456 320 (silent) nr8local -D4 xyz789 720p nr8local -D3P PLabcdef1234567890 256 nr8local -L convert `); } function runCommand(cmd, args = [], silent = false) { try { if (silent) { spawnSync(cmd, args, { stdio: 'ignore' }); } else { const result = spawnSync(cmd, args, { stdio: 'inherit' }); if (result.error || result.status !== 0) { return false; } } return true; } catch { return false; } } function getLinuxDistro() { try { const osRelease = fs.readFileSync('/etc/os-release', 'utf8'); const lines = osRelease.split('\n'); for (const line of lines) { if (line.startsWith('ID=')) { return line.replace('ID=', '').replace(/"/g, '').trim().toLowerCase(); } } } catch { return null; } return null; } function installPackageIfMissing(pkgName, checkCmd, installCmds) { try { execSync(checkCmd, { stdio: 'ignore' }); return; } catch { console.log(`${pkgName} not found, attempting to install...`); } for (const cmd of installCmds) { if (runCommand('sh', ['-c', cmd], false)) { console.log(`${pkgName} installed successfully.`); return; } } console.error(`Failed to install ${pkgName}. Please install it manually.`); process.exit(1); } function installYtDlpIfMissing() { const platform = os.platform(); try { execSync('yt-dlp --version', { stdio: 'ignore' }); return; } catch {} if (platform === 'linux') { const distro = getLinuxDistro(); if (distro === 'ubuntu' || distro === 'debian' || distro === 'linuxmint') { installPackageIfMissing('yt-dlp', 'yt-dlp --version', [ 'sudo apt-get update && sudo apt-get install -y yt-dlp', 'pip3 install --user yt-dlp', ]); } else if (distro === 'arch' || distro === 'manjaro') { installPackageIfMissing('yt-dlp', 'yt-dlp --version', [ 'sudo pacman -Sy --noconfirm yt-dlp', 'pip3 install --user yt-dlp', ]); } else if (distro === 'fedora') { installPackageIfMissing('yt-dlp', 'yt-dlp --version', [ 'sudo dnf install -y yt-dlp', 'pip3 install --user yt-dlp', ]); } else { // fallback to pip install user local installPackageIfMissing('yt-dlp', 'yt-dlp --version', ['pip3 install --user yt-dlp']); } } else if (platform === 'darwin') { installPackageIfMissing('yt-dlp', 'yt-dlp --version', ['brew install yt-dlp']); } else if (platform === 'win32') { console.error('Please install yt-dlp manually from https://github.com/yt-dlp/yt-dlp/releases'); process.exit(1); } else { console.error('Unsupported platform. Please install yt-dlp manually.'); process.exit(1); } } function installOpenIfMissing() { const platform = os.platform(); try { execSync('open --version', { stdio: 'ignore' }); return; } catch {} if (platform === 'linux') { const distro = getLinuxDistro(); if (distro === 'ubuntu' || distro === 'debian' || distro === 'linuxmint') { installPackageIfMissing('xdg-open', 'xdg-open --version', [ 'sudo apt-get update && sudo apt-get install -y xdg-utils', ]); } else if (distro === 'arch' || distro === 'manjaro') { installPackageIfMissing('xdg-open', 'xdg-open --version', [ 'sudo pacman -Sy --noconfirm xdg-utils', ]); } else if (distro === 'fedora') { installPackageIfMissing('xdg-open', 'xdg-open --version', [ 'sudo dnf install -y xdg-utils', ]); } else { console.error('xdg-open not found and your distro is unsupported. Please install it manually.'); process.exit(1); } } else if (platform === 'darwin') { // macOS open always exists return; } else if (platform === 'win32') { console.error('Please open the URL manually on Windows.'); process.exit(1); } else { console.error('Unsupported platform. Please open the URL manually.'); process.exit(1); } } function openLink(url) { const platform = os.platform(); try { if (platform === 'darwin') { execSync(`open "${url}"`); } else if (platform === 'win32') { execSync(`start "" "${url}"`, { shell: 'cmd.exe' }); } else { execSync(`xdg-open "${url}"`); } } catch (err) { console.error('Failed to open link:', err.message); } } function buildYtDlpArgs({ ids, isPlaylist, isAudio, resolution = '1080p', bitrate = '320', silent = false, }) { const args = []; if (isPlaylist) { args.push('-o', '"%(playlist_title)s/%(title)s.%(ext)s"'); args.push('-i'); } else { args.push('-o', '"%(title)s.%(ext)s"'); } if (isAudio) { // Audio format & quality args.push('-f', `bestaudio`); args.push('--extract-audio'); args.push('--audio-format', 'mp3'); args.push('--audio-quality', '0'); // best quality if (bitrate) { args.push('--audio-quality', '0'); // already best quality, but bitrate param can be used if needed } } else { // Video format & quality // Set resolution filter args.push('-f', `bestvideo[height<=${resolution.replace('p', '')}]+bestaudio/best[height<=${resolution.replace('p', '')}]`); } // Add ids or playlist URL(s) if (Array.isArray(ids)) { args.push(...ids); } else { args.push(ids); } if (silent) args.push('-q', '--no-warnings'); return args; } function parseIds(input) { // split by commas, no spaces allowed return input.split(',').filter(Boolean); } // Main execution if (args.length === 0 || args.includes('--help') || args.includes('-h')) { printHelp(); process.exit(0); } installYtDlpIfMissing(); installOpenIfMissing(); const cmd = args[0]; const rest = args.slice(1); switch (cmd) { case '-D3': case '-D4': case '-D3P': case '-D4P': { const isAudio = cmd.startsWith('-D3'); const isPlaylist = cmd.endsWith('P'); if (rest.length === 0) { console.error(`Error: You must provide video or playlist ID(s).`); process.exit(1); } // Check for silent mode (last arg) const silentArgIndex = rest.findIndex((a) => a.toLowerCase() === '(silent)'); const silent = silentArgIndex !== -1; // Extract IDs (video or playlist) // For multiple IDs separated by commas (no spaces), parse them let idsRaw = rest[0]; if (silent) { rest.splice(silentArgIndex, 1); if (silentArgIndex === 0) { console.error('No IDs provided.'); process.exit(1); } idsRaw = rest[0]; } const ids = parseIds(idsRaw); // Resolution or bitrate option if provided let quality = null; if (rest.length > 1) { // Use the second argument as quality/resolution if not silent or not last let idx = silent ? 1 : 1; if (rest.length > idx) { quality = rest[idx]; } } // Defaults let resolution = '1080p'; let bitrate = '320'; if (isAudio) { // quality can be bitrate number like 128, 256, 320 if (quality && /^[0-9]+$/.test(quality)) { bitrate = quality; } } else { // video resolution like 240p, 720p etc if (quality && /^[0-9]+p$/.test(quality)) { resolution = quality; } } // Build yt-dlp args const ytArgs = []; if (isPlaylist) { ytArgs.push(`https://www.youtube.com/playlist?list=${idsRaw}`); } else { ids.forEach((id) => ytArgs.push(`https://www.youtube.com/watch?v=${id}`)); } let execArgs = []; if (isAudio) { execArgs = ['-x', '--audio-format', 'mp3']; if (bitrate) { // Note: yt-dlp's audio-quality for mp3 does not support bitrate in kbps directly, // so we leave it best quality. } } else { execArgs = ['-f', `bestvideo[height<=${resolution.replace('p', '')}]+bestaudio/best[height<=${resolution.replace('p', '')}]`]; } if (silent) { execArgs.push('-q', '--no-warnings'); } execArgs = execArgs.concat(ytArgs); try { const res = spawnSync('yt-dlp', execArgs, { stdio: silent ? 'ignore' : 'inherit' }); if (res.error) { console.error('Error running yt-dlp:', res.error); process.exit(1); } process.exit(res.status); } catch (e) { console.error('Failed to execute yt-dlp:', e); process.exit(1); } break; } case '-L': { if (rest.length === 0) { console.error('Please provide a service or link name after -L.'); process.exit(1); } const service = rest[0].toLowerCase(); let url; switch (service) { case 'convert': url = 'https://www.tunemymusic.com/transfer'; break; case 'soupcan': url = 'https://soupcan.xyz'; break; default: if (service.startsWith('http')) { url = service; } else { console.error('Unknown service. Supported: convert, soupcan, or provide full URL.'); process.exit(1); } } try { openLink(url); } catch (e) { console.error('Failed to open link:', e); process.exit(1); } break; } default: console.error('No valid options provided. Use --help for usage.'); process.exit(1); }