Spaces:
Sleeping
Sleeping
| const express = require('express'); | |
| const cors = require('cors'); | |
| const app = express(); | |
| const PORT = 7860; | |
| app.use(cors()); | |
| app.use(express.json()); | |
| function extractVideoId(url) { | |
| const match = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/); | |
| return match ? match[1] : null; | |
| } | |
| function parseCaptionXml(xml) { | |
| const matches = xml.match(/<text[^>]*>([^<]*)<\/text>/g); | |
| if (!matches) return ""; | |
| return matches | |
| .map(m => m.replace(/<[^>]+>/g, '')) | |
| .join(" ") | |
| .replace(/&#39;/g, "'") | |
| .replace(/&quot;/g, '"') | |
| .replace(/&amp;/g, "&") | |
| .replace(/'/g, "'") | |
| .replace(/\s+/g, ' ') | |
| .trim(); | |
| } | |
| // ======================================================== | |
| // KEEP-ALIVE ENDPOINT | |
| // ======================================================== | |
| app.get('/api/health', (req, res) => { | |
| res.json({ status: 'ok', message: 'Transcript API is running!' }); | |
| }); | |
| // ======================================================== | |
| // TRANSCRIPT API | |
| // ======================================================== | |
| app.post('/api/transcript', async (req, res) => { | |
| try { | |
| const { url } = req.body; | |
| const videoId = extractVideoId(url); | |
| if (!videoId) { | |
| return res.status(400).json({ error: "Invalid YouTube URL" }); | |
| } | |
| console.log(`Fetching InnerTube data for video_id: ${videoId}`); | |
| // Anti-bot headers and cookies to bypass YouTube blocks on Cloud IPs | |
| const headers = { | |
| "Content-Type": "application/json", | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", | |
| "Origin": "https://www.youtube.com", | |
| "Referer": "https://www.youtube.com/", | |
| "Cookie": "CONSENT=YES+1; SOCS=CAESHAgBEhJnd3NfMjAyMzEwMjUtMF9SQzIaAmVuIAEaBgiA-LaoBg" | |
| }; | |
| // 1. Fetch video player data from InnerTube API | |
| const playerRes = await fetch("https://www.youtube.com/youtubei/v1/player?prettyPrint=false", { | |
| method: "POST", | |
| headers: headers, | |
| body: JSON.stringify({ | |
| context: { | |
| client: { | |
| clientName: "WEB", | |
| clientVersion: "2.20241210.00.00", | |
| hl: "en", | |
| gl: "US" | |
| } | |
| }, | |
| videoId: videoId | |
| }) | |
| }); | |
| if (!playerRes.ok) { | |
| throw new Error(`YouTube API returned status ${playerRes.status}`); | |
| } | |
| const playerData = await playerRes.json(); | |
| // Check if video is playable | |
| if (playerData.playabilityStatus?.status === "ERROR") { | |
| return res.status(404).json({ error: playerData.playabilityStatus?.reason || "Video unavailable" }); | |
| } | |
| // 2. Extract caption tracks | |
| const captionTracks = playerData?.captions?.playerCaptionsTracklistRenderer?.captionTracks; | |
| if (!captionTracks || captionTracks.length === 0) { | |
| return res.status(404).json({ error: "No captions found for this video." }); | |
| } | |
| // Prefer English, fallback to first available | |
| let selectedTrack = captionTracks.find(t => t.languageCode === 'en') || captionTracks[0]; | |
| let captionUrl = selectedTrack.baseUrl; | |
| console.log(`Fetching captions from URL...`); | |
| // 3. Fetch the actual caption XML | |
| const captionRes = await fetch(captionUrl, { | |
| headers: { "User-Agent": headers["User-Agent"] } | |
| }); | |
| if (!captionRes.ok) { | |
| throw new Error(`Caption fetch failed with status ${captionRes.status}`); | |
| } | |
| const captionXml = await captionRes.text(); | |
| // 4. Parse the XML into plain text | |
| const fullText = parseCaptionXml(captionXml); | |
| if (!fullText) { | |
| return res.status(404).json({ error: "Caption XML was empty." }); | |
| } | |
| console.log(`Successfully fetched transcript. Length: ${fullText.length} chars`); | |
| res.json({ text: fullText }); | |
| } catch (error) { | |
| console.error("Transcript Error:", error.message); | |
| res.status(500).json({ error: "Could not fetch transcript. " + error.message }); | |
| } | |
| }); | |
| // ======================================================== | |
| // TRANSLATE API | |
| // ======================================================== | |
| app.post('/api/translate', async (req, res) => { | |
| try { | |
| const { text, targetLang } = req.body; | |
| if (!text || !targetLang) { | |
| return res.status(400).json({ error: "Missing text or target language" }); | |
| } | |
| const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t`; | |
| const fetchRes = await fetch(url, { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/x-www-form-urlencoded", | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", | |
| }, | |
| body: `q=${encodeURIComponent(text)}`, | |
| }); | |
| if (!fetchRes.ok) { | |
| const errText = await fetchRes.text(); | |
| console.error("Google API Error:", errText); | |
| throw new Error(`Translation service responded with status ${fetchRes.status}`); | |
| } | |
| const data = await fetchRes.json(); | |
| const translated = data[0].map((item) => item[0]).join(""); | |
| res.json({ translatedText: translated }); | |
| } catch (error) { | |
| console.error("Translation API Error:", error.message); | |
| res.status(500).json({ error: "Failed to translate text. " + error.message }); | |
| } | |
| }); | |
| // Start server | |
| app.listen(PORT, '0.0.0.0', () => { | |
| console.log(`Server running on http://0.0.0.0:${PORT}`); | |
| }); |