Update pages/api/search.ts
Browse files- pages/api/search.ts +74 -31
pages/api/search.ts
CHANGED
|
@@ -9,41 +9,84 @@ type YtResult = {
|
|
| 9 |
thumbnails?: { url: string; width?: number; height?: number }[]
|
| 10 |
}
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
const q = (req.query.q || "").toString().trim()
|
| 14 |
if (!q) return res.status(400).json({ error: "Missing q" })
|
| 15 |
|
| 16 |
-
const limit = Math.max(1, Math.min(20, parseInt((req.query.limit || "
|
| 17 |
const term = `ytsearch${limit}:${q}`
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
["-
|
| 22 |
-
{
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
}))
|
| 42 |
-
res.json({ results })
|
| 43 |
-
} catch (parseErr) {
|
| 44 |
-
console.error("Failed to parse yt-dlp JSON", parseErr)
|
| 45 |
-
res.status(500).json({ error: "parse_failed" })
|
| 46 |
-
}
|
| 47 |
}
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
}
|
|
|
|
| 9 |
thumbnails?: { url: string; width?: number; height?: number }[]
|
| 10 |
}
|
| 11 |
|
| 12 |
+
function run(cmd: string, args: string[]) {
|
| 13 |
+
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
| 14 |
+
execFile(cmd, args, { maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
|
| 15 |
+
if (err) return reject(Object.assign(err, { stdout, stderr }))
|
| 16 |
+
resolve({ stdout, stderr })
|
| 17 |
+
})
|
| 18 |
+
})
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function mapLineJsonToResult(obj: any): YtResult | null {
|
| 22 |
+
const id = obj?.id
|
| 23 |
+
const title = obj?.title
|
| 24 |
+
if (!id || !title) return null
|
| 25 |
+
const url = obj?.webpage_url || `https://www.youtube.com/watch?v=${id}`
|
| 26 |
+
const duration = typeof obj?.duration === "number" ? obj.duration : undefined
|
| 27 |
+
const thumbs = Array.isArray(obj?.thumbnails)
|
| 28 |
+
? obj.thumbnails.map((t: any) => ({ url: t.url, width: t.width, height: t.height }))
|
| 29 |
+
: obj?.thumbnail
|
| 30 |
+
? [{ url: obj.thumbnail }]
|
| 31 |
+
: undefined
|
| 32 |
+
return { id, title, url, duration, thumbnails: thumbs }
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
| 36 |
const q = (req.query.q || "").toString().trim()
|
| 37 |
if (!q) return res.status(400).json({ error: "Missing q" })
|
| 38 |
|
| 39 |
+
const limit = Math.max(1, Math.min(20, parseInt((req.query.limit || "8").toString(), 10) || 8))
|
| 40 |
const term = `ytsearch${limit}:${q}`
|
| 41 |
|
| 42 |
+
try {
|
| 43 |
+
// Attempt A: Use IPv4 and per-entry JSON (-j). This avoids the “API page” failure mode.
|
| 44 |
+
const argsA = ["--force-ipv4", "-j", term, "--no-warnings"]
|
| 45 |
+
const { stdout } = await run("yt-dlp", argsA)
|
| 46 |
+
|
| 47 |
+
const resultsA: YtResult[] = stdout
|
| 48 |
+
.split("\n")
|
| 49 |
+
.map((line) => line.trim())
|
| 50 |
+
.filter((line) => line.startsWith("{") && line.endsWith("}"))
|
| 51 |
+
.map((line) => {
|
| 52 |
+
try {
|
| 53 |
+
return JSON.parse(line)
|
| 54 |
+
} catch {
|
| 55 |
+
return null
|
| 56 |
+
}
|
| 57 |
+
})
|
| 58 |
+
.filter(Boolean)
|
| 59 |
+
.map((obj) => mapLineJsonToResult(obj))
|
| 60 |
+
.filter((r): r is YtResult => !!r)
|
| 61 |
+
|
| 62 |
+
if (resultsA.length > 0) {
|
| 63 |
+
return res.json({ results: resultsA })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
+
|
| 66 |
+
// Fallback B: Print fields in a simple, parseable format (still forces IPv4).
|
| 67 |
+
const fmt = "%(id)s\t%(title)s\t%(webpage_url)s\t%(duration)s\t%(thumbnail)s"
|
| 68 |
+
const argsB = ["--force-ipv4", "--print", fmt, term, "--no-warnings"]
|
| 69 |
+
const outB = await run("yt-dlp", argsB)
|
| 70 |
+
const resultsB: YtResult[] = outB.stdout
|
| 71 |
+
.split("\n")
|
| 72 |
+
.map((l) => l.trim())
|
| 73 |
+
.filter(Boolean)
|
| 74 |
+
.map((line) => {
|
| 75 |
+
const [id, title, url, durationStr, thumb] = line.split("\t")
|
| 76 |
+
if (!id || !title) return null
|
| 77 |
+
const urlFinal = url || `https://www.youtube.com/watch?v=${id}`
|
| 78 |
+
const duration = Number.isFinite(Number(durationStr)) ? Number(durationStr) : undefined
|
| 79 |
+
const thumbnails = thumb ? [{ url: thumb }] : undefined
|
| 80 |
+
return { id, title, url: urlFinal, duration, thumbnails } as YtResult
|
| 81 |
+
})
|
| 82 |
+
.filter(Boolean) as YtResult[]
|
| 83 |
+
|
| 84 |
+
return res.json({ results: resultsB })
|
| 85 |
+
} catch (err: any) {
|
| 86 |
+
console.error("yt-dlp search failed", err)
|
| 87 |
+
return res.status(500).json({
|
| 88 |
+
error: "search_failed",
|
| 89 |
+
detail: err?.stderr || err?.message || "unknown_error",
|
| 90 |
+
})
|
| 91 |
+
}
|
| 92 |
}
|