shivam413 commited on
Commit
afc50e4
·
verified ·
1 Parent(s): e534a94

Update pages/api/search.ts

Browse files
Files changed (1) hide show
  1. pages/api/search.ts +89 -52
pages/api/search.ts CHANGED
@@ -1,82 +1,119 @@
1
  import type { NextApiRequest, NextApiResponse } from "next"
2
- import { execFile } from "node:child_process"
3
 
4
  type YtResult = {
5
  id: string
6
  title: string
7
  url: string
 
 
8
  }
9
 
10
- function run(cmd: string, args: string[]) {
11
- return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
12
- execFile(cmd, args, { maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
13
- if (err) return reject(Object.assign(err, { stdout, stderr }))
14
- resolve({ stdout, stderr })
15
- })
16
- })
17
  }
18
 
19
- // Extract the first YouTube watch URL from yt-dlp verbose output.
20
- function extractFirstYoutubeUrl(text: string): string | null {
21
- // Look for explicit "Extracting URL: <url>" first
22
- const explicit = text.match(/Extracting URL:\s*(https?:\/\/[^\s]+)/i)
23
- if (explicit?.[1]) return explicit[1]
 
 
24
 
25
- // Fallback: find any YouTube watch URL in output
26
- const anyWatch = text.match(/https?:\/\/(?:www\.)?youtube\.com\/watch\?v=[\w\-]{6,}/i)
27
- if (anyWatch?.[0]) return anyWatch[0]
 
 
 
 
28
 
29
- return null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
 
32
- // Pull the 11-char YouTube video id from a watch URL
33
- function videoIdFromUrl(u: string): string | null {
34
- try {
35
- const url = new URL(u)
36
- const id = url.searchParams.get("v")
37
- if (id) return id
38
- } catch {
39
- // ignore
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
- // Fallback quick parse
42
- const m = u.match(/[?&]v=([\w\-]{6,})/)
43
- return m?.[1] || null
44
  }
45
 
46
  export default async function handler(req: NextApiRequest, res: NextApiResponse) {
47
  const q = (req.query.q || "").toString().trim()
48
  if (!q) return res.status(400).json({ error: "Missing q" })
 
49
 
50
- try {
51
- // Use exactly the approach that works in your environment:
52
- // yt-dlp "ytsearch:<query>"
53
- // We also add --skip-download to avoid writing files to disk.
54
- const term = `ytsearch:${q}`
55
- const { stdout, stderr } = await run("yt-dlp", [term, "--no-warnings", "--skip-download"])
56
 
57
- const url = extractFirstYoutubeUrl(stdout + "\n" + stderr)
58
- if (!url) {
59
- return res.status(502).json({
60
- error: "no_url_found",
61
- detail: "yt-dlp did not print a watch URL. Full logs are suppressed in production.",
62
- })
 
 
 
63
  }
64
 
65
- const id = videoIdFromUrl(url) || ""
66
- const results: YtResult[] = [
67
- {
68
- id,
69
- url,
70
- // Use the query as a placeholder title; client can show this or just use Play.
71
- title: q,
72
- },
73
- ]
74
- return res.json({ results, source: "yt-dlp-ytsearch" })
75
  } catch (err: any) {
76
- console.error("yt-dlp search failed", err)
77
  return res.status(500).json({
78
  error: "search_failed",
79
- detail: err?.stderr || err?.message || "unknown_error",
80
  })
81
  }
82
  }
 
1
  import type { NextApiRequest, NextApiResponse } from "next"
 
2
 
3
  type YtResult = {
4
  id: string
5
  title: string
6
  url: string
7
+ duration?: number
8
+ thumbnails?: { url: string; width?: number; height?: number }[]
9
  }
10
 
11
+ function limitInt(v: string | number | undefined, def = 8, min = 1, max = 20) {
12
+ const n = Number(v)
13
+ if (!Number.isFinite(n)) return def
14
+ return Math.max(min, Math.min(max, Math.floor(n)))
 
 
 
15
  }
16
 
17
+ function thumbsFromSnippet(thumbs: any): { url: string; width?: number; height?: number }[] | undefined {
18
+ const out: { url: string; width?: number; height?: number }[] = []
19
+ for (const v of Object.values<any>(thumbs || {})) {
20
+ if (v?.url) out.push({ url: v.url, width: v.width, height: v.height })
21
+ }
22
+ return out.length ? out : undefined
23
+ }
24
 
25
+ async function searchYouTubeAPI(q: string, limit: number, key: string): Promise<YtResult[]> {
26
+ const url = new URL("https://www.googleapis.com/youtube/v3/search")
27
+ url.searchParams.set("part", "snippet")
28
+ url.searchParams.set("type", "video")
29
+ url.searchParams.set("maxResults", String(limit))
30
+ url.searchParams.set("q", q)
31
+ url.searchParams.set("key", key)
32
 
33
+ const r = await fetch(url.toString(), { next: { revalidate: 300 } })
34
+ if (!r.ok) throw new Error(`ytapi_http_${r.status}`)
35
+ const data = await r.json()
36
+ const items: any[] = Array.isArray(data?.items) ? data.items : []
37
+ return items
38
+ .map((it: any) => {
39
+ const id = it?.id?.videoId
40
+ const sn = it?.snippet
41
+ if (!id || !sn?.title) return null
42
+ return {
43
+ id,
44
+ title: sn.title,
45
+ url: `https://www.youtube.com/watch?v=${id}`,
46
+ thumbnails: thumbsFromSnippet(sn.thumbnails),
47
+ } as YtResult
48
+ })
49
+ .filter(Boolean) as YtResult[]
50
  }
51
 
52
+ async function searchPipedServer(q: string, limit: number): Promise<YtResult[]> {
53
+ // Try a few public instances; server-side DNS might still fail depending on your host.
54
+ const instances = [
55
+ "https://pipedapi.kavin.rocks",
56
+ "https://piped.video",
57
+ "https://piped.mha.fi",
58
+ "https://piped-api.garudalinux.org",
59
+ ]
60
+ for (const base of instances) {
61
+ try {
62
+ const url = new URL("/search", base)
63
+ url.searchParams.set("q", q)
64
+ const r = await fetch(url.toString(), { cache: "no-store" })
65
+ if (!r.ok) continue
66
+ const data = await r.json()
67
+ const items: any[] = Array.isArray(data?.items) ? data.items : []
68
+ const results = items
69
+ .filter((it) => it?.type?.toLowerCase() === "video" && it?.id && it?.title)
70
+ .slice(0, limit)
71
+ .map((it) => {
72
+ const id = it.id
73
+ return {
74
+ id,
75
+ title: it.title,
76
+ url: `https://www.youtube.com/watch?v=${id}`,
77
+ duration: typeof it.duration === "number" ? it.duration : undefined,
78
+ thumbnails: it.thumbnail ? [{ url: it.thumbnail }] : undefined,
79
+ } as YtResult
80
+ })
81
+ if (results.length) return results
82
+ } catch {
83
+ // try next instance
84
+ }
85
  }
86
+ return []
 
 
87
  }
88
 
89
  export default async function handler(req: NextApiRequest, res: NextApiResponse) {
90
  const q = (req.query.q || "").toString().trim()
91
  if (!q) return res.status(400).json({ error: "Missing q" })
92
+ const limit = limitInt(req.query.limit, 8, 1, 20)
93
 
94
+ // Edge cache (if available)
95
+ res.setHeader("Cache-Control", "s-maxage=300, stale-while-revalidate")
 
 
 
 
96
 
97
+ try {
98
+ const ytKey = process.env.YT_API_KEY || process.env.YOUTUBE_API_KEY
99
+ if (ytKey) {
100
+ try {
101
+ const results = await searchYouTubeAPI(q, limit, ytKey)
102
+ if (results.length) return res.json({ results, source: "youtube_api" })
103
+ } catch {
104
+ // fall back to piped
105
+ }
106
  }
107
 
108
+ const piped = await searchPipedServer(q, limit)
109
+ if (piped.length) return res.json({ results: piped, source: "piped" })
110
+
111
+ return res.status(502).json({ error: "no_results", detail: "All server-side search methods failed or returned empty." })
 
 
 
 
 
 
112
  } catch (err: any) {
113
+ console.error("search endpoint failed", err)
114
  return res.status(500).json({
115
  error: "search_failed",
116
+ detail: err?.message || "unknown_error",
117
  })
118
  }
119
  }