Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 19 additions & 33 deletions api/download.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,10 @@
import ytdl from '@distube/ytdl-core';
import { Writable } from 'stream';
import ffmpeg from 'fluent-ffmpeg';
import ffmpegPath from 'ffmpeg-static';
import fs from 'fs';

// Ensure ffmpeg binary is available – fallback to system ffmpeg if static build is missing
let resolvedFfmpegPath = ffmpegPath;
try {
const exists = resolvedFfmpegPath && fs.existsSync(resolvedFfmpegPath);
console.log('[ffmpeg-debug] static path:', resolvedFfmpegPath, 'exists:', exists);
if (!exists) {
console.warn('ffmpeg-static binary not found; falling back to system ffmpeg');
resolvedFfmpegPath = 'ffmpeg';
}
} catch (err) {
console.warn('[ffmpeg-debug] access check failed:', err);
resolvedFfmpegPath = 'ffmpeg';
}

ffmpeg.setFfmpegPath(resolvedFfmpegPath);
// On Vercel serverless, ffmpeg-static postinstall scripts don't run,
// so we always use the system ffmpeg binary.
ffmpeg.setFfmpegPath('ffmpeg');
Comment on lines +5 to +7
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Vercel Node.js serverless ffmpeg availability PATH 2026

💡 Result:

On Vercel Node.js Serverless / Vercel Functions (Fluid compute) in 2026, FFmpeg is not something you can assume is preinstalled or on PATH (so calling spawn("ffmpeg", ...) is not reliable). The supported approach is to bundle an FFmpeg binary with your function and execute it via an explicit path. [1]

Working pattern (recommended):

  • Add an FFmpeg binary dependency like ffmpeg-static.
  • Ensure the binary is included in the function bundle (Next.js file tracing does not always include native binaries unless you declare them). Vercel’s reference example uses outputFileTracingIncludes to include ./node_modules/ffmpeg-static/ffmpeg. [2]
  • Spawn FFmpeg by pointing to the bundled binary (example spawns ./node_modules/ffmpeg-static/ffmpeg, though you can also use the path exported by ffmpeg-static). [3]

Operational constraints to keep in mind:

  • Vercel Functions have a read-only filesystem, with only /tmp writable (up to 500 MB). [4]
  • Installing/bundling FFmpeg can be large; Vercel staff caution it can push you over function size limits (the community thread specifically mentions the 50 MB limit concern). [5]

So, for “PATH availability”: treat it as not available; always ship the binary and invoke it by explicit path. [1][3]

Sources: [1] [2] [3] [4] [5]


🏁 Script executed:

# Search for ffmpeg references in the codebase
rg -i "ffmpeg" --type js -B 2 -A 2

# Check for environment variable usage related to ffmpeg
rg "FFMPEG" --type js -B 2 -A 2

# Look for error handling around downloads
rg -i "download|merged|ffmpeg" api/download.js -B 2 -A 2

# Check if there's package.json with ffmpeg-static dependency
cat package.json | grep -i ffmpeg

Repository: creatorcluster/renderdragon.org

Length of output: 5083


🏁 Script executed:

# Check git history to understand the context of this change
git log --oneline -n 20 -- api/download.js

Repository: creatorcluster/renderdragon.org

Length of output: 127


Revert to bundled ffmpeg-static or ffmpeg will not be available on Vercel.

The code hard-codes ffmpeg.setFfmpegPath('ffmpeg') assuming system-level ffmpeg availability, but the comment's premise is flawed: ffmpeg-static dependency was removed and Vercel Node.js Serverless does not include ffmpeg in PATH by default. When users attempt merged downloads (video + audio), the ffmpeg command will fail at runtime with a generic error.

Solution: Restore ffmpeg-static as a dependency and use the bundled binary path. Ensure the binary is included in function tracing (via outputFileTracingIncludes in Vercel config if needed). Alternatively, if ffmpeg-static was intentionally removed, the merged download path must be disabled on Vercel.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/download.js` around lines 5 - 7, Replace the hard-coded system ffmpeg
path in the download handler by restoring and using the ffmpeg-static bundled
binary instead of ffmpeg.setFfmpegPath('ffmpeg'); add ffmpeg-static back as a
dependency and call ffmpeg.setFfmpegPath with the ffmpeg-static provided path
(or disable merged download on Vercel if you intentionally removed
ffmpeg-static), and ensure the bundled binary is included in Vercel function
tracing (configure outputFileTracingIncludes) so the merged video+audio flow in
api/download.js can reliably find ffmpeg at runtime.


// Enhanced browser headers (same as info.js)
const USER_AGENTS = [
Expand Down Expand Up @@ -61,7 +47,7 @@ function randomDelay(min = 1000, max = 3000) {
// Enhanced getInfo with retry logic
async function getInfoWithRetry(url, tries = 3, delayMs = 2000) {
await randomDelay(500, 1500);

try {
const info = await ytdl.getInfo(url, {
requestOptions: {
Expand All @@ -72,30 +58,30 @@ async function getInfoWithRetry(url, tries = 3, delayMs = 2000) {
quality: 'highestvideo',
filter: 'audioandvideo',
});

return info;
} catch (err) {
console.log(`[ytdl-download] Attempt ${4 - tries} failed:`, err.message);

const status = typeof err === 'object' && err && ('statusCode' in err ? err.statusCode : err.status);
const isRetryable =
status === 429 ||
status === 403 ||
status === 502 ||
status === 503 ||
const isRetryable =
status === 429 ||
status === 403 ||
status === 502 ||
status === 503 ||
status === 504 ||
err.message?.includes('Sign in to confirm') ||
err.message?.includes('bot') ||
err.message?.includes('captcha') ||
err.message?.includes('rate limit') ||
err.message?.includes('timeout');

if (isRetryable && tries > 1) {
console.log(`[ytdl-download] Retrying in ${delayMs}ms... (${tries - 1} attempts left)`);
await new Promise(resolve => setTimeout(resolve, delayMs));
return getInfoWithRetry(url, tries - 1, delayMs * 1.5);
}

throw err;
}
}
Expand Down Expand Up @@ -137,12 +123,12 @@ export default async function handler(req) {
}

console.log('[ytdl-download] Processing download request for:', url);

Comment on lines 125 to +126
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid logging raw user-provided URLs.

This logs full query strings, which can leak user-specific tokens/identifiers into logs.

Proposed fix
-    console.log('[ytdl-download] Processing download request for:', url);
+    let safeUrlForLog = url;
+    try {
+      const parsed = new URL(url);
+      parsed.search = '';
+      safeUrlForLog = parsed.toString();
+    } catch {}
+    console.log('[ytdl-download] Processing download request for:', safeUrlForLog);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/download.js` around lines 125 - 126, The current console.log in
api/download.js prints the raw user-provided url variable and can leak sensitive
query tokens; replace that logging with a sanitized form (e.g., parse the url
via the URL API and log only origin+pathname or a redacted query string, or log
a hash/fingerprint of the full URL) so no raw query parameters are emitted;
update the call that references url (the console.log('[ytdl-download] Processing
download request for:', url) statement) to use the sanitizedUrl/hashed value
instead and ensure any future logs follow the same redaction approach.

// Add delay before processing
await randomDelay(500, 1000);

const info = await getInfoWithRetry(url, 4, 2000);

Comment on lines 130 to +131
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Retry attempt logging is now inconsistent with 4 attempts.

getInfoWithRetry is called with 4 tries, but the attempt counter inside the helper is hardcoded around 4 and can emit misleading attempt numbers.

Proposed fix
-async function getInfoWithRetry(url, tries = 3, delayMs = 2000) {
+async function getInfoWithRetry(url, tries = 3, delayMs = 2000, maxTries = tries) {
@@
-    console.log(`[ytdl-download] Attempt ${4 - tries} failed:`, err.message);
+    console.log(`[ytdl-download] Attempt ${maxTries - tries + 1} failed:`, err.message);
@@
-      return getInfoWithRetry(url, tries - 1, delayMs * 1.5);
+      return getInfoWithRetry(url, tries - 1, delayMs * 1.5, maxTries);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/download.js` around lines 130 - 131, The retry logging in
getInfoWithRetry is misleading because it hardcodes the max attempts instead of
using the passed-in tries parameter; update the helper (getInfoWithRetry) to
reference the tries argument for max-attempt messages and compute the current
attempt from its loop/index/counter variable (e.g., attempt or i) so logs show
"attempt X of N" consistent with the tries argument passed from callers like the
call site that uses 4; also ensure any error messages or final failure log use
tries rather than the previous hardcoded value.

if (!info || !info.videoDetails) {
throw new Error('Failed to fetch video information');
}
Expand Down Expand Up @@ -228,7 +214,7 @@ export default async function handler(req) {

} catch (err) {
console.error('[ytdl-download] Error:', err.message);

// Return more user-friendly error messages
let errorMessage = 'Failed to download video';
if (err.message?.includes('Video unavailable')) {
Expand All @@ -240,8 +226,8 @@ export default async function handler(req) {
} else if (err.message?.includes('timeout')) {
errorMessage = 'Request timed out. Please try again.';
}
return new Response(JSON.stringify({

return new Response(JSON.stringify({
error: errorMessage,
message: errorMessage
}), {
Expand Down
Loading