nr8local
Version:
Personal yt-dlp helper CLI for downloading audio/video and opening music convert links
379 lines (330 loc) • 10.6 kB
JavaScript
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);
}