ztoolx commited on
Commit
d5aefbd
·
verified ·
1 Parent(s): 3a0160b
Files changed (1) hide show
  1. server.js +91 -33
server.js CHANGED
@@ -1,14 +1,31 @@
1
  const express = require('express');
2
  const cors = require('cors');
3
- const { YoutubeTranscript } = require('youtube-transcript');
4
 
5
  const app = express();
6
  const PORT = 7860;
7
 
8
- // Enable CORS for your Vercel frontend
9
  app.use(cors());
10
  app.use(express.json());
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  // ========================================================
13
  // KEEP-ALIVE ENDPOINT
14
  // ========================================================
@@ -22,47 +39,88 @@ app.get('/api/health', (req, res) => {
22
  app.post('/api/transcript', async (req, res) => {
23
  try {
24
  const { url } = req.body;
25
-
26
- const videoIdMatch = url.match(
27
- /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/
28
- );
29
- if (!videoIdMatch) {
30
  return res.status(400).json({ error: "Invalid YouTube URL" });
31
  }
32
- const videoId = videoIdMatch[1];
33
-
34
- let transcriptData;
35
- let retries = 3;
36
- let lastError;
37
-
38
- while (retries > 0) {
39
- try {
40
- transcriptData = await YoutubeTranscript.fetchTranscript(videoId);
41
- break; // success
42
- } catch (err) {
43
- lastError = err;
44
- retries--;
45
- if (retries > 0) {
46
- await new Promise((resolve) => setTimeout(resolve, 2000));
47
- }
48
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  }
50
 
51
- if (!transcriptData) {
52
- throw lastError;
 
 
 
53
  }
54
 
55
- const fullText = transcriptData
56
- .map((t) => t.text)
57
- .join(" ")
58
- .replace(/'|"/g, "'");
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  res.json({ text: fullText });
 
61
  } catch (error) {
62
  console.error("Transcript Error:", error.message);
63
- res.status(500).json({
64
- error: "Could not fetch transcript. The video might not have captions enabled, or the request timed out."
65
- });
66
  }
67
  });
68
 
 
1
  const express = require('express');
2
  const cors = require('cors');
 
3
 
4
  const app = express();
5
  const PORT = 7860;
6
 
 
7
  app.use(cors());
8
  app.use(express.json());
9
 
10
+ function extractVideoId(url) {
11
+ const match = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/);
12
+ return match ? match[1] : null;
13
+ }
14
+
15
+ function parseCaptionXml(xml) {
16
+ const matches = xml.match(/<text[^>]*>([^<]*)<\/text>/g);
17
+ if (!matches) return "";
18
+ return matches
19
+ .map(m => m.replace(/<[^>]+>/g, ''))
20
+ .join(" ")
21
+ .replace(/&amp;#39;/g, "'")
22
+ .replace(/&amp;quot;/g, '"')
23
+ .replace(/&amp;amp;/g, "&")
24
+ .replace(/&#39;/g, "'")
25
+ .replace(/\s+/g, ' ')
26
+ .trim();
27
+ }
28
+
29
  // ========================================================
30
  // KEEP-ALIVE ENDPOINT
31
  // ========================================================
 
39
  app.post('/api/transcript', async (req, res) => {
40
  try {
41
  const { url } = req.body;
42
+ const videoId = extractVideoId(url);
43
+
44
+ if (!videoId) {
 
 
45
  return res.status(400).json({ error: "Invalid YouTube URL" });
46
  }
47
+
48
+ console.log(`Fetching InnerTube data for video_id: ${videoId}`);
49
+
50
+ // Anti-bot headers and cookies to bypass YouTube blocks on Cloud IPs
51
+ const headers = {
52
+ "Content-Type": "application/json",
53
+ "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",
54
+ "Origin": "https://www.youtube.com",
55
+ "Referer": "https://www.youtube.com/",
56
+ "Cookie": "CONSENT=YES+1; SOCS=CAESHAgBEhJnd3NfMjAyMzEwMjUtMF9SQzIaAmVuIAEaBgiA-LaoBg"
57
+ };
58
+
59
+ // 1. Fetch video player data from InnerTube API
60
+ const playerRes = await fetch("https://www.youtube.com/youtubei/v1/player?prettyPrint=false", {
61
+ method: "POST",
62
+ headers: headers,
63
+ body: JSON.stringify({
64
+ context: {
65
+ client: {
66
+ clientName: "WEB",
67
+ clientVersion: "2.20241210.00.00",
68
+ hl: "en",
69
+ gl: "US"
70
+ }
71
+ },
72
+ videoId: videoId
73
+ })
74
+ });
75
+
76
+ if (!playerRes.ok) {
77
+ throw new Error(`YouTube API returned status ${playerRes.status}`);
78
+ }
79
+
80
+ const playerData = await playerRes.json();
81
+
82
+ // Check if video is playable
83
+ if (playerData.playabilityStatus?.status === "ERROR") {
84
+ return res.status(404).json({ error: playerData.playabilityStatus?.reason || "Video unavailable" });
85
  }
86
 
87
+ // 2. Extract caption tracks
88
+ const captionTracks = playerData?.captions?.playerCaptionsTracklistRenderer?.captionTracks;
89
+
90
+ if (!captionTracks || captionTracks.length === 0) {
91
+ return res.status(404).json({ error: "No captions found for this video." });
92
  }
93
 
94
+ // Prefer English, fallback to first available
95
+ let selectedTrack = captionTracks.find(t => t.languageCode === 'en') || captionTracks[0];
96
+ let captionUrl = selectedTrack.baseUrl;
 
97
 
98
+ console.log(`Fetching captions from URL...`);
99
+
100
+ // 3. Fetch the actual caption XML
101
+ const captionRes = await fetch(captionUrl, {
102
+ headers: { "User-Agent": headers["User-Agent"] }
103
+ });
104
+
105
+ if (!captionRes.ok) {
106
+ throw new Error(`Caption fetch failed with status ${captionRes.status}`);
107
+ }
108
+
109
+ const captionXml = await captionRes.text();
110
+
111
+ // 4. Parse the XML into plain text
112
+ const fullText = parseCaptionXml(captionXml);
113
+
114
+ if (!fullText) {
115
+ return res.status(404).json({ error: "Caption XML was empty." });
116
+ }
117
+
118
+ console.log(`Successfully fetched transcript. Length: ${fullText.length} chars`);
119
  res.json({ text: fullText });
120
+
121
  } catch (error) {
122
  console.error("Transcript Error:", error.message);
123
+ res.status(500).json({ error: "Could not fetch transcript. " + error.message });
 
 
124
  }
125
  });
126