Spaces:
No application file
No application file
| import type { APIRoute } from 'astro'; | |
| import { YtDlp } from 'ytdlp-nodejs'; | |
| import { extractionCache, CACHE_TTL } from '../../lib/cache'; | |
| export const POST: APIRoute = async ({ request }) => { | |
| try { | |
| const { url } = await request.json(); | |
| if (!url) { | |
| return new Response(JSON.stringify({ error: 'URL is required' }), { status: 400 }); | |
| } | |
| // Check cache | |
| const cacheKey = `transcript-info-${url}`; | |
| if (extractionCache.has(cacheKey)) { | |
| const cached = extractionCache.get(cacheKey); | |
| if (Date.now() - cached.timestamp < CACHE_TTL) { | |
| return new Response(JSON.stringify(cached.data), { | |
| status: 200, | |
| headers: { 'Content-Type': 'application/json', 'X-Cache': 'HIT' } | |
| }); | |
| } | |
| extractionCache.delete(cacheKey); | |
| } | |
| // Regex for Video ID | |
| const videoIdMatch = url.match(/(?:v=|\/|embed\/|shorts\/)([a-zA-Z0-9_-]{11})/); | |
| const videoId = videoIdMatch ? videoIdMatch[1] : null; | |
| let info: any = null; | |
| try { | |
| const binaryPath = './yt-dlp'; | |
| const ytdl = new YtDlp(binaryPath); | |
| info = await ytdl.getInfoAsync(url, { | |
| args: ['--no-check-certificates', '--no-playlist', '--flat-playlist', '--no-warnings', '--ignore-config'] | |
| }); | |
| } catch (e: any) { | |
| try { | |
| const ytdl = new YtDlp(); | |
| info = await ytdl.getInfoAsync(url, { | |
| args: ['--no-check-certificates', '--no-playlist', '--flat-playlist', '--no-warnings', '--ignore-config'] | |
| }); | |
| } catch (e2: any) { | |
| console.warn('yt-dlp transcript info failed:', e2.message); | |
| } | |
| } | |
| if (!info && videoId && API_KEY) { | |
| const res = await youtube.videos.list({ | |
| key: API_KEY, | |
| part: ['snippet', 'contentDetails'], | |
| id: [videoId] | |
| }); | |
| const item = res.data.items?.[0]; | |
| if (item) { | |
| info = { | |
| title: item.snippet?.title, | |
| uploader: item.snippet?.channelTitle, | |
| thumbnail: item.snippet?.thumbnails?.high?.url, | |
| duration_string: item.contentDetails?.duration, | |
| subtitles: {}, | |
| automatic_captions: {}, | |
| isFallback: true | |
| }; | |
| } | |
| } | |
| if (!info) return new Response(JSON.stringify({ error: 'Video not found or extraction not supported on this node.' }), { status: 404 }); | |
| const subtitles = info.subtitles || {}; | |
| const autoCaptions = info.automatic_captions || {}; | |
| // Merge subtitles and auto-captions for display | |
| const languages: Record<string, { name: string, isAuto: boolean }> = {}; | |
| Object.keys(subtitles).forEach(lang => { | |
| languages[lang] = { | |
| name: subtitles[lang][0]?.name || lang, | |
| isAuto: false | |
| }; | |
| }); | |
| Object.keys(autoCaptions).forEach(lang => { | |
| if (!languages[lang]) { | |
| languages[lang] = { | |
| name: autoCaptions[lang][0]?.name || lang, | |
| isAuto: true | |
| }; | |
| } | |
| }); | |
| const responseData = { | |
| title: info.title, | |
| uploader: info.uploader, | |
| thumbnail: info.thumbnail, | |
| duration: info.duration_string, | |
| languages, | |
| // Store raw subtitle data to avoid re-fetching in the next step (if small enough) | |
| // Actually, we only need the URLs for the selected language | |
| rawSubtitles: subtitles, | |
| rawAutoCaptions: autoCaptions | |
| }; | |
| extractionCache.set(cacheKey, { | |
| timestamp: Date.now(), | |
| data: responseData | |
| }); | |
| return new Response(JSON.stringify(responseData), { | |
| status: 200, | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| } catch (error: any) { | |
| return new Response(JSON.stringify({ error: error.message }), { status: 500 }); | |
| } | |
| }; | |