UNPKG

yt-dlx

Version:

Effortless Audio-Video Downloader And Streamer!

236 lines 11.9 kB
import * as fs from "fs"; import colors from "colors"; import * as path from "path"; import { z, ZodError } from "zod"; import ffmpeg from "fluent-ffmpeg"; import ytdlx from "../../utils/Agent"; import { locator } from "../../utils/locator"; import { PassThrough } from "stream"; function formatTime(seconds) { if (!isFinite(seconds) || isNaN(seconds)) return "00h 00m 00s"; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); return `${hours.toString().padStart(2, "0")}h ${minutes.toString().padStart(2, "0")}m ${secs.toString().padStart(2, "0")}s`; } function calculateETA(startTime, percent) { const currentTime = new Date(); const elapsedTime = (currentTime.getTime() - startTime.getTime()) / 1000; if (percent <= 0) return NaN; const totalTimeEstimate = (elapsedTime / percent) * 100; const remainingTime = totalTimeEstimate - elapsedTime; return remainingTime; } function progbar({ percent, timemark, startTime }) { let displayPercent = isNaN(percent || 0) ? 0 : percent || 0; displayPercent = Math.min(Math.max(displayPercent, 0), 100); const colorFn = displayPercent < 25 ? colors.red : displayPercent < 50 ? colors.yellow : colors.green; const width = Math.floor((process.stdout.columns || 80) / 4); const scomp = Math.round((width * displayPercent) / 100); const progb = colorFn("━").repeat(scomp) + colorFn(" ").repeat(width - scomp); const etaSeconds = calculateETA(startTime, displayPercent); const etaFormatted = formatTime(etaSeconds); process.stdout.write(`\r${colorFn("@prog:")} ${progb} ${colorFn("| @percent:")} ${displayPercent.toFixed(2)}% ${colorFn("| @timemark:")} ${timemark} ${colorFn("| @eta:")} ${etaFormatted}`); } var ZodSchema = z.object({ query: z.string().min(2), output: z.string().optional(), useTor: z.boolean().optional(), stream: z.boolean().optional(), verbose: z.boolean().optional(), metadata: z.boolean().optional(), filter: z.enum(["invert", "rotate90", "rotate270", "grayscale", "rotate180", "flipVertical", "flipHorizontal"]).optional(), resolution: z.string().regex(/^\d+p(\d+)?$/), showProgress: z.boolean().optional(), }); export default async function AudioVideoCustom({ query, stream, output, useTor, filter, metadata, verbose, resolution, showProgress, }) { try { ZodSchema.parse({ query, stream, output, useTor, filter, metadata, verbose, resolution, showProgress }); if (metadata && (stream || output || filter || showProgress)) { throw new Error(`${colors.red("@error:")} The 'metadata' parameter cannot be used with 'stream', 'output', 'filter', or 'showProgress'.`); } if (stream && output) throw new Error(`${colors.red("@error:")} The 'stream' parameter cannot be used with 'output'.`); if (metadata && showProgress) throw new Error(`${colors.red("@error:")} The 'showProgress' parameter cannot be used when 'metadata' is true.`); const engineData = await ytdlx({ query, verbose, useTor }); if (!engineData) throw new Error(`${colors.red("@error:")} Unable to retrieve a response from the engine.`); if (!engineData.metaData) throw new Error(`${colors.red("@error:")} Metadata was not found in the engine response.`); if (metadata) { return { metadata: { metaData: engineData.metaData, BestAudioLow: engineData.BestAudioLow, BestAudioHigh: engineData.BestAudioHigh, AudioLowDRC: engineData.AudioLowDRC, AudioHighDRC: engineData.AudioHighDRC, BestVideoLow: engineData.BestVideoLow, BestVideoHigh: engineData.BestVideoHigh, VideoLowHDR: engineData.VideoLowHDR, VideoHighHDR: engineData.VideoHighHDR, ManifestLow: engineData.ManifestLow, ManifestHigh: engineData.ManifestHigh, filename: `yt-dlx_AudioVideoCustom_${resolution}_${filter ? filter + "_" : ""}${engineData.metaData.title?.replace(/[^a-zA-Z0-9_]+/g, "_") || "video"}.mkv`, }, }; } const title = engineData.metaData.title?.replace(/[^a-zA-Z0-9_]+/g, "_") || "video"; const folder = output ? output : process.cwd(); if (!stream && !fs.existsSync(folder)) { try { fs.mkdirSync(folder, { recursive: true }); } catch (mkdirError) { throw new Error(`${colors.red("@error:")} Failed to create the output directory: ${mkdirError?.message}`); } } const instance = ffmpeg(); try { const paths = await locator(); if (!paths.ffmpeg) throw new Error(`${colors.red("@error:")} ffmpeg executable not found.`); if (!paths.ffprobe) throw new Error(`${colors.red("@error:")} ffprobe executable not found.`); instance.setFfmpegPath(paths.ffmpeg); instance.setFfprobePath(paths.ffprobe); } catch (locatorError) { throw new Error(`${colors.red("@error:")} Failed to locate ffmpeg or ffprobe: ${locatorError?.message}`); } if (!engineData.BestAudioHigh?.url) throw new Error(`${colors.red("@error:")} Highest quality audio URL was not found.`); instance.addInput(engineData.BestAudioHigh.url); instance.withOutputFormat("matroska"); const resolutionRegex = /(\d+)p(\d+)?/; const resolutionMatch = resolution.match(resolutionRegex); const targetHeight = resolutionMatch ? parseInt(resolutionMatch[1], 10) : null; const targetFps = resolutionMatch && resolutionMatch[2] ? parseInt(resolutionMatch[2], 10) : null; const vdata = engineData.allFormats?.find((i) => { const height = i.height; const fps = i.fps; const vcodec = i.vcodec; let heightMatches = height === targetHeight; let fpsMatches = targetFps === null || fps === targetFps; return heightMatches && fpsMatches && vcodec !== "none"; }); if (vdata) { if (!vdata.url) throw new Error(`${colors.red("@error:")} Video URL not found for resolution: ${resolution}.`); instance.addInput(vdata.url.toString()); } else { throw new Error(`${colors.red("@error:")} No video data found for resolution: ${resolution}. Use list_formats() maybe?`); } const filterMap = { invert: ["negate"], flipVertical: ["vflip"], rotate180: ["rotate=PI"], flipHorizontal: ["hflip"], rotate90: ["rotate=PI/2"], rotate270: ["rotate=3*PI/2"], grayscale: ["colorchannelmixer=.3:.4:.3:0:.3:.4:.3:0:.3:.4:.3"], }; if (filter && filterMap[filter]) instance.withVideoFilter(filterMap[filter]); else instance.outputOptions("-c copy"); let processStartTime; if (showProgress) { instance.on("start", () => { processStartTime = new Date(); }); instance.on("progress", progress => { if (processStartTime) progbar({ ...progress, percent: progress.percent !== undefined ? progress.percent : 0, startTime: processStartTime }); }); } if (stream) { const passthroughStream = new PassThrough(); const filenameBase = `yt-dlx_AudioVideoCustom_${resolution}_`; let filename = `${filenameBase}${filter ? filter + "_" : ""}${title}.mkv`; passthroughStream.filename = filename; instance.on("start", command => { if (verbose) console.log(colors.green("@info:"), "FFmpeg stream started:", command); }); instance.pipe(passthroughStream, { end: true }); instance.on("end", () => { if (verbose) console.log(colors.green("@info:"), "FFmpeg streaming finished."); if (showProgress) process.stdout.write("\n"); }); instance.on("error", (error, stdout, stderr) => { const errorMessage = `${colors.red("@error:")} FFmpeg stream error: ${error?.message}`; console.error(errorMessage, "\nstdout:", stdout, "\nstderr:", stderr); passthroughStream.emit("error", new Error(errorMessage)); passthroughStream.destroy(new Error(errorMessage)); if (showProgress) process.stdout.write("\n"); }); instance.run(); if (verbose) console.log(colors.green("@info:"), "❣️ Thank you for using yt-dlx. Consider 🌟starring the GitHub repo https://github.com/yt-dlx."); return { stream: passthroughStream, filename: filename }; } else { const filenameBase = `yt-dlx_AudioVideoCustom_${resolution}_`; let filename = `${filenameBase}${filter ? filter + "_" : ""}${title}.mkv`; const outputPath = path.join(folder, filename); instance.output(outputPath); await new Promise((resolve, reject) => { instance.on("start", command => { if (verbose) console.log(colors.green("@info:"), "FFmpeg download started:", command); if (showProgress) processStartTime = new Date(); }); instance.on("progress", progress => { if (showProgress && processStartTime) progbar({ ...progress, percent: progress.percent !== undefined ? progress.percent : 0, startTime: processStartTime }); }); instance.on("end", () => { if (verbose) console.log(colors.green("@info:"), "FFmpeg download finished."); if (showProgress) process.stdout.write("\n"); resolve(); }); instance.on("error", (error, stdout, stderr) => { const errorMessage = `${colors.red("@error:")} FFmpeg download error: ${error?.message}`; console.error(errorMessage, "\nstdout:", stdout, "\nstderr:", stderr); if (showProgress) process.stdout.write("\n"); reject(new Error(errorMessage)); }); instance.run(); }); if (verbose) console.log(colors.green("@info:"), "❣️ Thank you for using yt-dlx. Consider 🌟starring the GitHub repo https://github.com/yt-dlx."); return { outputPath: outputPath }; } } catch (error) { if (error instanceof ZodError) { const errorMessage = `${colors.red("@error:")} Argument validation failed: ${error.errors.map(e => `${e.path.join(".")}: ${e.message}`).join(", ")}`; console.error(errorMessage); throw new Error(errorMessage); } else if (error instanceof Error) { console.error(error.message); throw error; } else { const unexpectedError = `${colors.red("@error:")} An unexpected error occurred: ${String(error)}`; console.error(unexpectedError); throw new Error(unexpectedError); } } finally { } } //# sourceMappingURL=Custom.js.map