Allow returning page if transcript extraction failed

777b2518bdb0153f194db79fb651c558e196fc3d

Cohee <18619528+Cohee1207@users.noreply.github.com>

1 files changed, +75 -57Ignore whitespace
src/endpoints/search.js+75 -57
@@ -22,6 +22,72 @@ const visitHeaders = {
22 'Sec-Fetch-User': '?1',22 'Sec-Fetch-User': '?1',
23};23};
2424
25/**
26 * Extract the transcript of a YouTube video
27 * @param {string} videoPageBody HTML of the video page
28 * @param {string} lang Language code
29 * @returns {Promise<string>} Transcript text
30 */
31async function extractTranscript(videoPageBody, lang) {
32 const he = require('he');
33 const RE_XML_TRANSCRIPT = /<text start="([^"]*)" dur="([^"]*)">([^<]*)<\/text>/g;
34 const splittedHTML = videoPageBody.split('"captions":');
35
36 if (splittedHTML.length <= 1) {
37 if (videoPageBody.includes('class="g-recaptcha"')) {
38 throw new Error('Too many requests');
39 }
40 if (!videoPageBody.includes('"playabilityStatus":')) {
41 throw new Error('Video is not available');
42 }
43 throw new Error('Transcript not available');
44 }
45
46 const captions = (() => {
47 try {
48 return JSON.parse(splittedHTML[1].split(',"videoDetails')[0].replace('\n', ''));
49 } catch (e) {
50 return undefined;
51 }
52 })()?.['playerCaptionsTracklistRenderer'];
53
54 if (!captions) {
55 throw new Error('Transcript disabled');
56 }
57
58 if (!('captionTracks' in captions)) {
59 throw new Error('Transcript not available');
60 }
61
62 if (lang && !captions.captionTracks.some(track => track.languageCode === lang)) {
63 throw new Error('Transcript not available in this language');
64 }
65
66 const transcriptURL = (lang ? captions.captionTracks.find(track => track.languageCode === lang) : captions.captionTracks[0]).baseUrl;
67 const transcriptResponse = await fetch(transcriptURL, {
68 headers: {
69 ...(lang && { 'Accept-Language': lang }),
70 'User-Agent': visitHeaders['User-Agent'],
71 },
72 });
73
74 if (!transcriptResponse.ok) {
75 throw new Error('Transcript request failed');
76 }
77
78 const transcriptBody = await transcriptResponse.text();
79 const results = [...transcriptBody.matchAll(RE_XML_TRANSCRIPT)];
80 const transcript = results.map((result) => ({
81 text: result[3],
82 duration: parseFloat(result[2]),
83 offset: parseFloat(result[1]),
84 lang: lang ?? captions.captionTracks[0].languageCode,
85 }));
86 // The text is double-encoded
87 const transcriptText = transcript.map((line) => he.decode(he.decode(line.text))).join(' ');
88 return transcriptText;
89}
90
25router.post('/serpapi', jsonParser, async (request, response) => {91router.post('/serpapi', jsonParser, async (request, response) => {
26 try {92 try {
27 const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);93 const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);
@@ -56,8 +122,6 @@ router.post('/serpapi', jsonParser, async (request, response) => {
56 */122 */
57router.post('/transcript', jsonParser, async (request, response) => {123router.post('/transcript', jsonParser, async (request, response) => {
58 try {124 try {
59 const he = require('he');
60 const RE_XML_TRANSCRIPT = /<text start="([^"]*)" dur="([^"]*)">([^<]*)<\/text>/g;
61 const id = request.body.id;125 const id = request.body.id;
62 const lang = request.body.lang;126 const lang = request.body.lang;
63 const json = request.body.json;127 const json = request.body.json;
@@ -75,64 +139,18 @@ router.post('/transcript', jsonParser, async (request, response) => {
75 });139 });
76140
77 const videoPageBody = await videoPageResponse.text();141 const videoPageBody = await videoPageResponse.text();
78 const splittedHTML = videoPageBody.split('"captions":');
79142
80 if (splittedHTML.length <= 1) {143 try {
81 if (videoPageBody.includes('class="g-recaptcha"')) {144 const transcriptText = await extractTranscript(videoPageBody, lang);
82 throw new Error('Too many requests');145 return json
83 }146 ? response.json({ transcript: transcriptText, html: videoPageBody })
84 if (!videoPageBody.includes('"playabilityStatus":')) {147 : response.send(transcriptText);
85 throw new Error('Video is not available');148 } catch (error) {
86 }149 if (json) {
87 throw new Error('Transcript not available');150 return response.json({ html: videoPageBody, transcript: '' });
88 }
89
90 const captions = (() => {
91 try {
92 return JSON.parse(splittedHTML[1].split(',"videoDetails')[0].replace('\n', ''));
93 } catch (e) {
94 return undefined;
95 }151 }
96 })()?.['playerCaptionsTracklistRenderer'];152 throw error;
97
98 if (!captions) {
99 throw new Error('Transcript disabled');
100 }
101
102 if (!('captionTracks' in captions)) {
103 throw new Error('Transcript not available');
104 }
105
106 if (lang && !captions.captionTracks.some(track => track.languageCode === lang)) {
107 throw new Error('Transcript not available in this language');
108 }153 }
109
110 const transcriptURL = (lang ? captions.captionTracks.find(track => track.languageCode === lang) : captions.captionTracks[0]).baseUrl;
111 const transcriptResponse = await fetch(transcriptURL, {
112 headers: {
113 ...(lang && { 'Accept-Language': lang }),
114 'User-Agent': visitHeaders['User-Agent'],
115 },
116 });
117
118 if (!transcriptResponse.ok) {
119 throw new Error('Transcript request failed');
120 }
121
122 const transcriptBody = await transcriptResponse.text();
123 const results = [...transcriptBody.matchAll(RE_XML_TRANSCRIPT)];
124 const transcript = results.map((result) => ({
125 text: result[3],
126 duration: parseFloat(result[2]),
127 offset: parseFloat(result[1]),
128 lang: lang ?? captions.captionTracks[0].languageCode,
129 }));
130 // The text is double-encoded
131 const transcriptText = transcript.map((line) => he.decode(he.decode(line.text))).join(' ');
132
133 return json
134 ? response.json({ transcript: transcriptText, html: videoPageBody })
135 : response.send(transcriptText);
136 } catch (error) {154 } catch (error) {
137 console.log(error);155 console.log(error);
138 return response.sendStatus(500);156 return response.sendStatus(500);