ztoolx's picture
up
d5aefbd verified
Raw
History Blame Contribute Delete
5.45 kB
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(/&amp;#39;/g, "'")
.replace(/&amp;quot;/g, '"')
.replace(/&amp;amp;/g, "&")
.replace(/&#39;/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}`);
});