-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Feature/video download #600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,355 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // apps/web/src/app/api/download-videos/route.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from "zod"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { spawn } from "child_process"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Validates the request body for video URL. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const infoRequestSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url: z.string().url("Invalid URL provided"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Validates the request body for search keyword. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const searchRequestSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| keyword: z.string().min(1, "Keyword is required"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| page: z.number().int().positive().optional().default(1), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageSize: z.number().int().min(1).max(50).optional().default(20), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Handles POST requests to /api/download-videos with action parameter. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Supports: search, info, download | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(request: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { searchParams } = new URL(request.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const action = searchParams.get("action"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (action) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "search": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await handleSearch(request); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "info": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await handleVideoInfo(request); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "download": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await handleDownload(request); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: "Invalid action. Use 'search', 'info', or 'download'." }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Video API error:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Internal server error", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: "An unexpected error occurred", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Handles video search using yt-dlp. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Searches videos based on keyword and pagination. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleSearch(request: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rawBody = await request.json().catch(() => null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!rawBody) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: "Invalid JSON in request body" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validationResult = searchRequestSchema.safeParse(rawBody); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!validationResult.success) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Invalid request parameters", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: validationResult.error.flatten().fieldErrors, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { keyword, page, pageSize } = validationResult.data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const startIndex = (page - 1) * pageSize + 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const endIndex = page * pageSize; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Promise((resolve) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ytDlp = spawn("yt-dlp", [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "--dump-single-json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "--flat-playlist", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "--no-warnings", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "--playlist-start", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| startIndex.toString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "--playlist-end", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| endIndex.toString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `ytsearch${pageSize}:${keyword}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The pagination logic is broken - pages beyond the first will return empty results because yt-dlp is only searching for View DetailsAnalysisPagination bug in handleSearch() returns empty results for pages beyond firstWhat fails: How to reproduce: # Page 2 with pageSize=20 tries to get results 21-40 from only 20 total results
yt-dlp --playlist-start 21 --playlist-end 40 "ytsearch20:python tutorial"Result: Returns empty array: Expected: Should return 20 results (items 21-40) like when using Root cause: Line 95 searches for |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+82
to
+96
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Search pagination bug: ytsearch limit must reflect endIndex. With page > 1, - const { keyword, page, pageSize } = validationResult.data;
- const startIndex = (page - 1) * pageSize + 1;
- const endIndex = page * pageSize;
+ const { keyword, page, pageSize } = validationResult.data;
+ const startIndex = (page - 1) * pageSize + 1;
+ const endIndex = page * pageSize;
@@
- const ytDlp = spawn("yt-dlp", [
+ const ytDlp = spawn("yt-dlp", [
"--dump-single-json",
"--flat-playlist",
"--no-warnings",
"--playlist-start",
startIndex.toString(),
"--playlist-end",
endIndex.toString(),
- `ytsearch${pageSize}:${keyword}`,
+ `ytsearch${endIndex}:${keyword}`,
]);📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let stdoutData = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let stderrData = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ytDlp.stdout.on("data", (data) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stdoutData += data.toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ytDlp.stderr.on("data", (data) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stderrData += data.toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ytDlp.on("close", (code) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (code !== 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: `yt-dlp process exited with code ${code}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: stderrData, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!stdoutData.trim()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "No output from yt-dlp", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: stderrData || "Process completed with no output", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = JSON.parse(stdoutData); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: Array.isArray(result) ? result : [result], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (parseError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+137
to
+144
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parse yt-dlp search output via entries array.
- const result = JSON.parse(stdoutData);
- resolve(
- NextResponse.json({
- success: true,
- data: Array.isArray(result) ? result : [result],
- })
- );
+ const parsed = JSON.parse(stdoutData);
+ const entries = Array.isArray(parsed?.entries) ? parsed.entries : [];
+ resolve(
+ NextResponse.json({
+ success: true,
+ data: entries,
+ pagination: { page, pageSize },
+ })
+ );📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Error parsing yt-dlp output", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: parseError instanceof Error ? parseError.message : "Unknown error", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rawOutput: stdoutData, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ytDlp.on("error", (error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Failed to start yt-dlp process", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: error.message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : "Unknown error occurred", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Fetches video metadata using yt-dlp. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns title, duration, formats, thumbnails, etc. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleVideoInfo(request: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rawBody = await request.json().catch(() => null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!rawBody) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: "Invalid JSON in request body" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validationResult = infoRequestSchema.safeParse(rawBody); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!validationResult.success) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Invalid request parameters", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: validationResult.error.flatten().fieldErrors, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { url } = validationResult.data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Promise((resolve) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ytDlp = spawn("yt-dlp", [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "--dump-single-json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "--no-warnings", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "--compat-options", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "no-youtube-channel-redirect", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let stdoutData = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let stderrData = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ytDlp.stdout.on("data", (data) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stdoutData += data.toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ytDlp.stderr.on("data", (data) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stderrData += data.toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ytDlp.on("close", (code) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (code !== 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: `yt-dlp process exited with code ${code}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: stderrData, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!stdoutData.trim()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "No output from yt-dlp", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: stderrData || "Process completed with no output", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = JSON.parse(stdoutData); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: result, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (parseError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Error parsing yt-dlp output", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: parseError instanceof Error ? parseError.message : "Unknown error", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rawOutput: stdoutData, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ytDlp.on("error", (error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Failed to start yt-dlp process", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: error.message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : "Unknown error occurred", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Initiates video download process (placeholder). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * In the future, this will spawn yt-dlp to download the video. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param request - Incoming NextRequest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns NextResponse with download status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleDownload(request: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rawBody = await request.json().catch(() => null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!rawBody) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: "Invalid JSON in request body" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validationResult = infoRequestSchema.safeParse(rawBody); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!validationResult.success) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Invalid request parameters", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: validationResult.error.flatten().fieldErrors, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { url } = validationResult.data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: 实际调用 yt-dlp 下载视频文件 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 当前返回模拟响应 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: "Download started (placeholder)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: "queued", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| downloadId: crypto.randomUUID(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| startedAt: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : "Unknown error occurred", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use node: protocol for built-ins and import types correctly. Also import randomUUID.
Aligns with guidelines and avoids relying on a possibly-missing global crypto.
📝 Committable suggestion
🤖 Prompt for AI Agents