Spaces:
Sleeping
Sleeping
File size: 5,447 Bytes
26f3ad0 d5aefbd 26f3ad0 d5aefbd 26f3ad0 d5aefbd 26f3ad0 d5aefbd 26f3ad0 d5aefbd 26f3ad0 d5aefbd 26f3ad0 d5aefbd 26f3ad0 d5aefbd 26f3ad0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | 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}`);
}); |