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(/&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}`);
});