tubezero 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -121
- package/README.md +32 -26
- package/dist/index.cjs +1802 -675
- package/dist/index.d.cts +412 -132
- package/dist/index.d.ts +412 -132
- package/dist/index.js +1778 -673
- package/package.json +74 -54
package/dist/index.cjs
CHANGED
|
@@ -21,17 +21,34 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Base: () => Base,
|
|
24
|
+
BaseChannel: () => BaseChannel,
|
|
24
25
|
BaseVideo: () => BaseVideo,
|
|
26
|
+
Caption: () => Caption,
|
|
27
|
+
CaptionLanguage: () => CaptionLanguage,
|
|
28
|
+
Channel: () => Channel,
|
|
25
29
|
ChannelCompact: () => ChannelCompact,
|
|
30
|
+
ChannelLive: () => ChannelLive,
|
|
31
|
+
ChannelPlaylists: () => ChannelPlaylists,
|
|
32
|
+
ChannelPosts: () => ChannelPosts,
|
|
33
|
+
ChannelShorts: () => ChannelShorts,
|
|
34
|
+
ChannelVideos: () => ChannelVideos,
|
|
35
|
+
Chat: () => Chat,
|
|
26
36
|
Client: () => Client,
|
|
37
|
+
Comment: () => Comment,
|
|
38
|
+
CommentReplies: () => CommentReplies,
|
|
27
39
|
Continuable: () => Continuable,
|
|
40
|
+
LiveVideo: () => LiveVideo,
|
|
41
|
+
MixPlaylist: () => MixPlaylist,
|
|
28
42
|
Playlist: () => Playlist,
|
|
29
43
|
PlaylistCompact: () => PlaylistCompact,
|
|
30
44
|
PlaylistVideos: () => PlaylistVideos,
|
|
31
45
|
SearchResult: () => SearchResult,
|
|
32
46
|
Thumbnails: () => Thumbnails,
|
|
33
47
|
Video: () => Video,
|
|
48
|
+
VideoCaptions: () => VideoCaptions,
|
|
49
|
+
VideoComments: () => VideoComments,
|
|
34
50
|
VideoCompact: () => VideoCompact,
|
|
51
|
+
VideoRelated: () => VideoRelated,
|
|
35
52
|
createCommentsApiRequestOptions: () => createCommentsApiRequestOptions,
|
|
36
53
|
extractPlayerResponse: () => extractPlayerResponse,
|
|
37
54
|
extractVideoEntries: () => extractVideoEntries,
|
|
@@ -42,369 +59,17 @@ __export(index_exports, {
|
|
|
42
59
|
findContinuationToken: () => findContinuationToken,
|
|
43
60
|
getInnerTubeConfig: () => getInnerTubeConfig,
|
|
44
61
|
getSApiSidHash: () => getSApiSidHash,
|
|
45
|
-
|
|
62
|
+
getSapisidFromCookieString: () => getSapisidFromCookieString,
|
|
63
|
+
getVideoPlayback: () => getVideoPlayback,
|
|
46
64
|
parseXmlTranscriptRegex: () => parseXmlTranscriptRegex,
|
|
47
|
-
scrapeTasteData: () => scrapeTasteData
|
|
65
|
+
scrapeTasteData: () => scrapeTasteData,
|
|
66
|
+
searchYouTube: () => searchYouTube,
|
|
67
|
+
toPlainText: () => toPlainText,
|
|
68
|
+
toSRT: () => toSRT,
|
|
69
|
+
toVTT: () => toVTT
|
|
48
70
|
});
|
|
49
71
|
module.exports = __toCommonJS(index_exports);
|
|
50
72
|
|
|
51
|
-
// src/scraper.ts
|
|
52
|
-
async function fetchYtInitialData(url) {
|
|
53
|
-
try {
|
|
54
|
-
console.log(`[Scraper] Fetching HTML from ${url}`);
|
|
55
|
-
const response = await fetch(url, { credentials: "include" });
|
|
56
|
-
const text = await response.text();
|
|
57
|
-
const patterns = [
|
|
58
|
-
/var ytInitialData\s*=\s*(\{.*?\});<\/script>/,
|
|
59
|
-
/window\["ytInitialData"\]\s*=\s*(\{.*?\});/
|
|
60
|
-
];
|
|
61
|
-
for (const regex of patterns) {
|
|
62
|
-
const match = text.match(regex);
|
|
63
|
-
if (match && match[1]) {
|
|
64
|
-
return JSON.parse(match[1]);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
console.warn(`[Scraper] Could not find ytInitialData in ${url}`);
|
|
68
|
-
} catch (e) {
|
|
69
|
-
console.error(`[Scraper] Failed to fetch data from ${url}`, e);
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
function getSapisidFromCookie() {
|
|
74
|
-
if (typeof document === "undefined") return null;
|
|
75
|
-
const match = document.cookie.match(/__Secure-3PAPISID=([^;]+)/) || document.cookie.match(/__Secure-1PAPISID=([^;]+)/) || document.cookie.match(/SAPISID=([^;]+)/);
|
|
76
|
-
return match ? match[1] : null;
|
|
77
|
-
}
|
|
78
|
-
async function getSApiSidHash(sapisid, origin = "https://www.youtube.com") {
|
|
79
|
-
if (!sapisid) return null;
|
|
80
|
-
try {
|
|
81
|
-
const timestamp = Math.floor(Date.now() / 1e3);
|
|
82
|
-
const input = `${timestamp} ${sapisid} ${origin}`;
|
|
83
|
-
const encoder = new TextEncoder();
|
|
84
|
-
const data = encoder.encode(input);
|
|
85
|
-
const buffer = await crypto.subtle.digest("SHA-1", data);
|
|
86
|
-
const hash = Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
87
|
-
return `${timestamp}_${hash}`;
|
|
88
|
-
} catch (e) {
|
|
89
|
-
console.error("[Scraper] Failed to generate SAPISIDHASH", e);
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
function extractVideoEntries(data) {
|
|
94
|
-
const entries = [];
|
|
95
|
-
const seenTitles = /* @__PURE__ */ new Set();
|
|
96
|
-
function recurse(obj) {
|
|
97
|
-
if (!obj || typeof obj !== "object") return;
|
|
98
|
-
if (Array.isArray(obj)) {
|
|
99
|
-
for (let i = 0; i < obj.length; i++) {
|
|
100
|
-
recurse(obj[i]);
|
|
101
|
-
}
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (obj.videoId && obj.title) {
|
|
105
|
-
let title = "";
|
|
106
|
-
if (typeof obj.title === "string") {
|
|
107
|
-
title = obj.title;
|
|
108
|
-
} else if (obj.title.runs && obj.title.runs[0] && obj.title.runs[0].text) {
|
|
109
|
-
title = obj.title.runs[0].text;
|
|
110
|
-
} else if (obj.title.simpleText) {
|
|
111
|
-
title = obj.title.simpleText;
|
|
112
|
-
}
|
|
113
|
-
title = title.trim();
|
|
114
|
-
if (title && title.length > 2 && title !== "Skip navigation") {
|
|
115
|
-
if (!seenTitles.has(title)) {
|
|
116
|
-
seenTitles.add(title);
|
|
117
|
-
let channel = "";
|
|
118
|
-
const byline = obj.longBylineText || obj.shortBylineText || obj.ownerText;
|
|
119
|
-
if (byline) {
|
|
120
|
-
if (typeof byline === "string") {
|
|
121
|
-
channel = byline;
|
|
122
|
-
} else if (byline.runs && byline.runs[0] && byline.runs[0].text) {
|
|
123
|
-
channel = byline.runs[0].text;
|
|
124
|
-
} else if (byline.simpleText) {
|
|
125
|
-
channel = byline.simpleText;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
entries.push({
|
|
129
|
-
title,
|
|
130
|
-
channel: channel.split("\n")[0].replace(/•/g, "").trim()
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
|
|
137
|
-
const model = obj.lockupViewModel;
|
|
138
|
-
const meta = model.metadata?.lockupMetadataViewModel;
|
|
139
|
-
if (meta && meta.title && meta.title.content) {
|
|
140
|
-
const title = meta.title.content.trim();
|
|
141
|
-
if (title && !seenTitles.has(title)) {
|
|
142
|
-
seenTitles.add(title);
|
|
143
|
-
let channel = "";
|
|
144
|
-
const rows = meta.metadata?.contentMetadataViewModel?.metadataRows;
|
|
145
|
-
if (rows && rows[0] && rows[0].metadataParts && rows[0].metadataParts[0] && rows[0].metadataParts[0].text) {
|
|
146
|
-
channel = rows[0].metadataParts[0].text.content || "";
|
|
147
|
-
}
|
|
148
|
-
entries.push({
|
|
149
|
-
title,
|
|
150
|
-
channel: channel.split("\n")[0].replace(/•/g, "").trim()
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
for (const key in obj) {
|
|
157
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
158
|
-
recurse(obj[key]);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
try {
|
|
163
|
-
recurse(data);
|
|
164
|
-
} catch (e) {
|
|
165
|
-
console.warn("[Scraper] Error extracting video entries recursively", e);
|
|
166
|
-
}
|
|
167
|
-
return entries;
|
|
168
|
-
}
|
|
169
|
-
function getFallbackClientVersion() {
|
|
170
|
-
const d = /* @__PURE__ */ new Date();
|
|
171
|
-
d.setDate(d.getDate() - 2);
|
|
172
|
-
const yyyymmdd = d.toISOString().split("T")[0].replace(/-/g, "");
|
|
173
|
-
return `2.${yyyymmdd}.00.00`;
|
|
174
|
-
}
|
|
175
|
-
var cachedApiKey = null;
|
|
176
|
-
var cachedClientVersion = null;
|
|
177
|
-
var cachedIdToken = null;
|
|
178
|
-
async function getInnerTubeConfig(injectedConfig) {
|
|
179
|
-
if (injectedConfig && injectedConfig.apiKey) {
|
|
180
|
-
cachedApiKey = injectedConfig.apiKey;
|
|
181
|
-
cachedClientVersion = injectedConfig.clientVersion || getFallbackClientVersion();
|
|
182
|
-
cachedIdToken = injectedConfig.idToken ?? null;
|
|
183
|
-
return { apiKey: cachedApiKey, clientVersion: cachedClientVersion, idToken: cachedIdToken };
|
|
184
|
-
}
|
|
185
|
-
if (cachedApiKey && cachedClientVersion) {
|
|
186
|
-
return { apiKey: cachedApiKey, clientVersion: cachedClientVersion, idToken: cachedIdToken };
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
console.log("[Scraper] Fetching YouTube homepage for config...");
|
|
190
|
-
const response = await fetch("https://www.youtube.com", { credentials: "include" });
|
|
191
|
-
const html = await response.text();
|
|
192
|
-
const apiKeyMatch = html.match(/"INNERTUBE_API_KEY":"([^"]+)"/);
|
|
193
|
-
const clientVersionMatch = html.match(/"INNERTUBE_CLIENT_VERSION":"([^"]+)"/);
|
|
194
|
-
const idTokenMatch = html.match(/"ID_TOKEN":"([^"]+)"/);
|
|
195
|
-
if (apiKeyMatch && apiKeyMatch[1]) cachedApiKey = apiKeyMatch[1];
|
|
196
|
-
if (clientVersionMatch && clientVersionMatch[1]) cachedClientVersion = clientVersionMatch[1];
|
|
197
|
-
if (idTokenMatch && idTokenMatch[1]) cachedIdToken = idTokenMatch[1];
|
|
198
|
-
console.log(`[Scraper] Extracted Config \u2014 Key: ${cachedApiKey ? "OK" : "Failed"}, Version: ${cachedClientVersion}`);
|
|
199
|
-
} catch (e) {
|
|
200
|
-
console.warn("[Scraper] Failed to extract InnerTube config from HTML", e);
|
|
201
|
-
}
|
|
202
|
-
if (!cachedClientVersion) {
|
|
203
|
-
cachedClientVersion = getFallbackClientVersion();
|
|
204
|
-
}
|
|
205
|
-
return { apiKey: cachedApiKey, clientVersion: cachedClientVersion, idToken: cachedIdToken };
|
|
206
|
-
}
|
|
207
|
-
function findContinuationToken(obj) {
|
|
208
|
-
if (!obj || typeof obj !== "object") return null;
|
|
209
|
-
if (obj.continuationCommand && obj.continuationCommand.token) {
|
|
210
|
-
return obj.continuationCommand.token;
|
|
211
|
-
}
|
|
212
|
-
for (const key in obj) {
|
|
213
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
214
|
-
const token = findContinuationToken(obj[key]);
|
|
215
|
-
if (token) return token;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
async function fetchInnerTubeContinuation(apiKey, clientVersion, idToken, initialToken, limit) {
|
|
221
|
-
const entries = [];
|
|
222
|
-
let continuationToken = initialToken;
|
|
223
|
-
try {
|
|
224
|
-
while (continuationToken && entries.length < limit) {
|
|
225
|
-
const headers = {
|
|
226
|
-
"Content-Type": "application/json",
|
|
227
|
-
"X-Youtube-Client-Name": "1",
|
|
228
|
-
"X-Youtube-Client-Version": clientVersion
|
|
229
|
-
};
|
|
230
|
-
if (idToken) {
|
|
231
|
-
headers["X-Youtube-Identity-Token"] = idToken;
|
|
232
|
-
}
|
|
233
|
-
const sapisid = getSapisidFromCookie();
|
|
234
|
-
if (sapisid) {
|
|
235
|
-
const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
|
|
236
|
-
if (authHash) {
|
|
237
|
-
headers["Authorization"] = `SAPISIDHASH ${authHash}`;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const response = await fetch(`https://www.youtube.com/youtubei/v1/browse?key=${apiKey}&prettyPrint=false`, {
|
|
241
|
-
method: "POST",
|
|
242
|
-
headers,
|
|
243
|
-
credentials: "include",
|
|
244
|
-
body: JSON.stringify({
|
|
245
|
-
context: {
|
|
246
|
-
client: {
|
|
247
|
-
clientName: "WEB",
|
|
248
|
-
clientVersion,
|
|
249
|
-
hl: "en",
|
|
250
|
-
gl: "US"
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
continuation: continuationToken
|
|
254
|
-
})
|
|
255
|
-
});
|
|
256
|
-
if (!response.ok) break;
|
|
257
|
-
const data = await response.json();
|
|
258
|
-
const pageEntries = extractVideoEntries(data);
|
|
259
|
-
entries.push(...pageEntries);
|
|
260
|
-
continuationToken = findContinuationToken(data);
|
|
261
|
-
}
|
|
262
|
-
} catch (e) {
|
|
263
|
-
console.error("[Scraper] Error in InnerTube pagination", e);
|
|
264
|
-
}
|
|
265
|
-
return entries;
|
|
266
|
-
}
|
|
267
|
-
async function fetchInnerTubeFeed(apiKey, clientVersion, idToken, browseId, limit = 500) {
|
|
268
|
-
const entries = [];
|
|
269
|
-
try {
|
|
270
|
-
const headers = {
|
|
271
|
-
"Content-Type": "application/json",
|
|
272
|
-
"X-Youtube-Client-Name": "1",
|
|
273
|
-
"X-Youtube-Client-Version": clientVersion
|
|
274
|
-
};
|
|
275
|
-
if (idToken) {
|
|
276
|
-
headers["X-Youtube-Identity-Token"] = idToken;
|
|
277
|
-
}
|
|
278
|
-
const sapisid = getSapisidFromCookie();
|
|
279
|
-
if (sapisid) {
|
|
280
|
-
const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
|
|
281
|
-
if (authHash) {
|
|
282
|
-
headers["Authorization"] = `SAPISIDHASH ${authHash}`;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
const response = await fetch(`https://www.youtube.com/youtubei/v1/browse?key=${apiKey}&prettyPrint=false`, {
|
|
286
|
-
method: "POST",
|
|
287
|
-
headers,
|
|
288
|
-
credentials: "include",
|
|
289
|
-
body: JSON.stringify({
|
|
290
|
-
context: {
|
|
291
|
-
client: {
|
|
292
|
-
clientName: "WEB",
|
|
293
|
-
clientVersion,
|
|
294
|
-
hl: "en",
|
|
295
|
-
gl: "US"
|
|
296
|
-
}
|
|
297
|
-
},
|
|
298
|
-
browseId
|
|
299
|
-
})
|
|
300
|
-
});
|
|
301
|
-
if (!response.ok) return [];
|
|
302
|
-
const data = await response.json();
|
|
303
|
-
const pageEntries = extractVideoEntries(data);
|
|
304
|
-
entries.push(...pageEntries);
|
|
305
|
-
if (entries.length < limit) {
|
|
306
|
-
const continuationToken = findContinuationToken(data);
|
|
307
|
-
if (continuationToken) {
|
|
308
|
-
const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, continuationToken, limit - entries.length);
|
|
309
|
-
entries.push(...more);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
} catch (e) {
|
|
313
|
-
console.error(`[Scraper] InnerTube fetch error for ${browseId}`, e);
|
|
314
|
-
}
|
|
315
|
-
return entries;
|
|
316
|
-
}
|
|
317
|
-
async function scrapeTasteData(injectedConfig, customPlaylists = [], limit = 500) {
|
|
318
|
-
let historyEntries = [];
|
|
319
|
-
let likesEntries = [];
|
|
320
|
-
let wlEntries = [];
|
|
321
|
-
const dislikesEntries = [];
|
|
322
|
-
const config = await getInnerTubeConfig(injectedConfig);
|
|
323
|
-
const apiKey = config.apiKey;
|
|
324
|
-
const clientVersion = config.clientVersion;
|
|
325
|
-
const idToken = config.idToken;
|
|
326
|
-
if (apiKey) {
|
|
327
|
-
historyEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "FEhistory", limit);
|
|
328
|
-
likesEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "VLLL", limit);
|
|
329
|
-
wlEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "VLWL", limit);
|
|
330
|
-
}
|
|
331
|
-
if (historyEntries.length === 0) {
|
|
332
|
-
const historyData = await fetchYtInitialData("https://www.youtube.com/feed/history");
|
|
333
|
-
if (historyData) {
|
|
334
|
-
historyEntries = extractVideoEntries(historyData);
|
|
335
|
-
if (historyEntries.length < limit) {
|
|
336
|
-
const token = findContinuationToken(historyData);
|
|
337
|
-
if (token && apiKey) {
|
|
338
|
-
const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - historyEntries.length);
|
|
339
|
-
historyEntries.push(...more);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (likesEntries.length === 0) {
|
|
345
|
-
const likesData = await fetchYtInitialData("https://www.youtube.com/playlist?list=LL");
|
|
346
|
-
if (likesData) {
|
|
347
|
-
likesEntries = extractVideoEntries(likesData);
|
|
348
|
-
if (likesEntries.length < limit) {
|
|
349
|
-
const token = findContinuationToken(likesData);
|
|
350
|
-
if (token && apiKey) {
|
|
351
|
-
const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - likesEntries.length);
|
|
352
|
-
likesEntries.push(...more);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if (wlEntries.length === 0) {
|
|
358
|
-
const wlData = await fetchYtInitialData("https://www.youtube.com/playlist?list=WL");
|
|
359
|
-
if (wlData) {
|
|
360
|
-
wlEntries = extractVideoEntries(wlData);
|
|
361
|
-
if (wlEntries.length < limit) {
|
|
362
|
-
const token = findContinuationToken(wlData);
|
|
363
|
-
if (token && apiKey) {
|
|
364
|
-
const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - wlEntries.length);
|
|
365
|
-
wlEntries.push(...more);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
const customPlaylistsData = [];
|
|
371
|
-
for (const pl of customPlaylists) {
|
|
372
|
-
if (!pl.url) continue;
|
|
373
|
-
const match = pl.url.match(/[&?]list=([a-zA-Z0-9_-]+)/);
|
|
374
|
-
const playlistId = match ? match[1] : pl.url.trim();
|
|
375
|
-
if (!playlistId || !/^[a-zA-Z0-9_-]+$/.test(playlistId)) continue;
|
|
376
|
-
const browseId = playlistId.startsWith("VL") ? playlistId : "VL" + playlistId;
|
|
377
|
-
let entries = [];
|
|
378
|
-
if (apiKey) {
|
|
379
|
-
entries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, browseId, limit);
|
|
380
|
-
}
|
|
381
|
-
if (entries.length === 0) {
|
|
382
|
-
const data = await fetchYtInitialData(`https://www.youtube.com/playlist?list=${playlistId}`);
|
|
383
|
-
if (data) {
|
|
384
|
-
entries = extractVideoEntries(data);
|
|
385
|
-
if (entries.length < limit) {
|
|
386
|
-
const token = findContinuationToken(data);
|
|
387
|
-
if (token && apiKey) {
|
|
388
|
-
const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - entries.length);
|
|
389
|
-
entries.push(...more);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
customPlaylistsData.push({
|
|
395
|
-
id: playlistId,
|
|
396
|
-
entries: entries.slice(0, limit)
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
return {
|
|
400
|
-
historyEntries: historyEntries.slice(0, limit),
|
|
401
|
-
likesEntries: likesEntries.slice(0, limit),
|
|
402
|
-
wlEntries: wlEntries.slice(0, limit),
|
|
403
|
-
dislikesEntries: dislikesEntries.slice(0, limit),
|
|
404
|
-
customPlaylistsData
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
73
|
// src/base.ts
|
|
409
74
|
var Base = class {
|
|
410
75
|
constructor(client) {
|
|
@@ -463,26 +128,27 @@ var Continuable = class extends Base {
|
|
|
463
128
|
};
|
|
464
129
|
|
|
465
130
|
// src/thumbnails.ts
|
|
466
|
-
var Thumbnails = class {
|
|
467
|
-
list
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (!this.list || this.list.length === 0) {
|
|
473
|
-
return void 0;
|
|
474
|
-
}
|
|
475
|
-
let best = this.list[0];
|
|
476
|
-
let maxResolution = (best.width || 0) * (best.height || 0);
|
|
477
|
-
for (let i = 1; i < this.list.length; i++) {
|
|
478
|
-
const thumb = this.list[i];
|
|
479
|
-
const resolution = (thumb.width || 0) * (thumb.height || 0);
|
|
480
|
-
if (resolution > maxResolution) {
|
|
481
|
-
maxResolution = resolution;
|
|
482
|
-
best = thumb;
|
|
483
|
-
}
|
|
131
|
+
var Thumbnails = class extends Array {
|
|
132
|
+
constructor(list = []) {
|
|
133
|
+
if (typeof list === "number") {
|
|
134
|
+
super(list);
|
|
135
|
+
} else {
|
|
136
|
+
super(...list);
|
|
484
137
|
}
|
|
485
|
-
|
|
138
|
+
}
|
|
139
|
+
get min() {
|
|
140
|
+
return this[0]?.url;
|
|
141
|
+
}
|
|
142
|
+
get best() {
|
|
143
|
+
return this.reduce((prev, curr) => {
|
|
144
|
+
if (!prev) return curr;
|
|
145
|
+
return prev.width * prev.height > curr.width * curr.height ? prev : curr;
|
|
146
|
+
}, void 0)?.url;
|
|
147
|
+
}
|
|
148
|
+
load(thumbnails) {
|
|
149
|
+
this.length = 0;
|
|
150
|
+
this.push(...thumbnails);
|
|
151
|
+
return this;
|
|
486
152
|
}
|
|
487
153
|
};
|
|
488
154
|
|
|
@@ -506,51 +172,142 @@ var VideoCompact = class extends Base {
|
|
|
506
172
|
let pubAt = null;
|
|
507
173
|
let thumbnails = [];
|
|
508
174
|
let channelObj = void 0;
|
|
509
|
-
if (data.videoId) {
|
|
510
|
-
videoId = data.videoId;
|
|
175
|
+
if (data.videoId) {
|
|
176
|
+
videoId = data.videoId;
|
|
177
|
+
titleText = data.title?.simpleText || data.title?.runs?.[0]?.text || "";
|
|
178
|
+
thumbnails = data.thumbnail?.thumbnails || [];
|
|
179
|
+
if (data.lengthText) {
|
|
180
|
+
const text = data.lengthText.simpleText || data.lengthText.runs?.[0]?.text || "";
|
|
181
|
+
durationSec = this.parseDuration(text);
|
|
182
|
+
}
|
|
183
|
+
if (data.badges?.some((b) => b.metadataBadgeRenderer?.style === "BADGE_STYLE_TYPE_LIVE_NOW")) {
|
|
184
|
+
isLive = true;
|
|
185
|
+
}
|
|
186
|
+
if (data.viewCountText) {
|
|
187
|
+
const text = data.viewCountText.simpleText || data.viewCountText.runs?.[0]?.text || "";
|
|
188
|
+
viewCountNum = this.parseViewCount(text);
|
|
189
|
+
}
|
|
190
|
+
if (data.publishedTimeText) {
|
|
191
|
+
pubAt = data.publishedTimeText.simpleText || data.publishedTimeText.runs?.[0]?.text || null;
|
|
192
|
+
}
|
|
193
|
+
const byline = data.shortBylineText || data.longBylineText || data.ownerText;
|
|
194
|
+
if (byline && byline.runs && byline.runs[0]) {
|
|
195
|
+
const run = byline.runs[0];
|
|
196
|
+
channelObj = {
|
|
197
|
+
id: run.navigationEndpoint?.browseEndpoint?.browseId || void 0,
|
|
198
|
+
name: run.text,
|
|
199
|
+
thumbnails: data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails ? new Thumbnails(data.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails) : void 0
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
} else if (data.lockupViewModel && data.lockupViewModel.contentId && data.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
|
|
203
|
+
const model = data.lockupViewModel;
|
|
204
|
+
videoId = model.contentId;
|
|
205
|
+
const meta = model.metadata?.lockupMetadataViewModel;
|
|
206
|
+
titleText = meta?.title?.content || "";
|
|
207
|
+
const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || [];
|
|
208
|
+
let cName = "";
|
|
209
|
+
let vCount = null;
|
|
210
|
+
let pAt = null;
|
|
211
|
+
for (const row of rows) {
|
|
212
|
+
for (const part of row.metadataParts || []) {
|
|
213
|
+
const text = part.text?.content || "";
|
|
214
|
+
if (text.includes("views") || text.includes("watching")) {
|
|
215
|
+
if (text.includes("watching")) isLive = true;
|
|
216
|
+
vCount = this.parseViewCount(text);
|
|
217
|
+
} else if (text.includes("ago")) {
|
|
218
|
+
pAt = text;
|
|
219
|
+
} else {
|
|
220
|
+
cName = text;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (cName) {
|
|
225
|
+
channelObj = { name: cName.replace(/•/g, "").trim() };
|
|
226
|
+
}
|
|
227
|
+
viewCountNum = vCount;
|
|
228
|
+
pubAt = pAt;
|
|
229
|
+
}
|
|
230
|
+
this.id = videoId;
|
|
231
|
+
this.title = titleText;
|
|
232
|
+
this.thumbnails = new Thumbnails(thumbnails);
|
|
233
|
+
this.duration = durationSec;
|
|
234
|
+
this.isLive = isLive;
|
|
235
|
+
this.channel = channelObj;
|
|
236
|
+
this.viewCount = viewCountNum;
|
|
237
|
+
this.publishedAt = pubAt;
|
|
238
|
+
}
|
|
239
|
+
parseDuration(text) {
|
|
240
|
+
const parts = text.split(":").map(Number);
|
|
241
|
+
if (parts.some(isNaN)) return 0;
|
|
242
|
+
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
243
|
+
if (parts.length === 2) return parts[0] * 60 + parts[1];
|
|
244
|
+
return parts[0] || 0;
|
|
245
|
+
}
|
|
246
|
+
parseViewCount(text) {
|
|
247
|
+
const cleaned = text.trim().replace(/,/g, "");
|
|
248
|
+
const match = cleaned.match(/([\d.]+)\s*([KMB])?/i);
|
|
249
|
+
if (!match) return null;
|
|
250
|
+
const num = parseFloat(match[1]);
|
|
251
|
+
if (isNaN(num)) return null;
|
|
252
|
+
const suffix = match[2]?.toUpperCase();
|
|
253
|
+
if (suffix === "K") return Math.round(num * 1e3);
|
|
254
|
+
if (suffix === "M") return Math.round(num * 1e6);
|
|
255
|
+
if (suffix === "B") return Math.round(num * 1e9);
|
|
256
|
+
const result = Math.round(num);
|
|
257
|
+
return isNaN(result) ? null : result;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/playlist-compact.ts
|
|
262
|
+
var PlaylistCompact = class extends Base {
|
|
263
|
+
id;
|
|
264
|
+
title;
|
|
265
|
+
thumbnails;
|
|
266
|
+
videoCount;
|
|
267
|
+
channel;
|
|
268
|
+
constructor(client, data) {
|
|
269
|
+
super(client);
|
|
270
|
+
let playlistId = "";
|
|
271
|
+
let titleText = "";
|
|
272
|
+
let thumbnails = [];
|
|
273
|
+
let videoCountNum = null;
|
|
274
|
+
let channelObj = void 0;
|
|
275
|
+
if (data.playlistId) {
|
|
276
|
+
playlistId = data.playlistId;
|
|
511
277
|
titleText = data.title?.simpleText || data.title?.runs?.[0]?.text || "";
|
|
512
|
-
thumbnails = data.thumbnail?.thumbnails || [];
|
|
513
|
-
if (data.
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (data.viewCountText) {
|
|
521
|
-
const text = data.viewCountText.simpleText || data.viewCountText.runs?.[0]?.text || "";
|
|
522
|
-
viewCountNum = this.parseViewCount(text);
|
|
523
|
-
}
|
|
524
|
-
if (data.publishedTimeText) {
|
|
525
|
-
pubAt = data.publishedTimeText.simpleText || data.publishedTimeText.runs?.[0]?.text || null;
|
|
278
|
+
thumbnails = data.thumbnails?.[0]?.thumbnails || data.thumbnail?.thumbnails || [];
|
|
279
|
+
if (data.videoCount) {
|
|
280
|
+
const parsed = parseInt(data.videoCount, 10);
|
|
281
|
+
videoCountNum = isNaN(parsed) ? null : parsed;
|
|
282
|
+
} else if (data.videoCountText) {
|
|
283
|
+
const text = data.videoCountText.runs?.[0]?.text || data.videoCountText.simpleText || "";
|
|
284
|
+
const cleaned = text.replace(/[^0-9]/g, "");
|
|
285
|
+
if (cleaned) videoCountNum = parseInt(cleaned, 10);
|
|
526
286
|
}
|
|
527
287
|
const byline = data.shortBylineText || data.longBylineText || data.ownerText;
|
|
528
288
|
if (byline && byline.runs && byline.runs[0]) {
|
|
529
289
|
const run = byline.runs[0];
|
|
530
290
|
channelObj = {
|
|
531
291
|
id: run.navigationEndpoint?.browseEndpoint?.browseId || void 0,
|
|
532
|
-
name: run.text
|
|
533
|
-
thumbnails: data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails ? new Thumbnails(data.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails) : void 0
|
|
292
|
+
name: run.text
|
|
534
293
|
};
|
|
535
294
|
}
|
|
536
|
-
} else if (data.lockupViewModel && data.lockupViewModel.contentId && data.lockupViewModel.contentType === "
|
|
295
|
+
} else if (data.lockupViewModel && data.lockupViewModel.contentId && data.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_PLAYLIST") {
|
|
537
296
|
const model = data.lockupViewModel;
|
|
538
|
-
|
|
297
|
+
playlistId = model.contentId;
|
|
539
298
|
const meta = model.metadata?.lockupMetadataViewModel;
|
|
540
299
|
titleText = meta?.title?.content || "";
|
|
300
|
+
const img = model.image?.lockupImageViewModel?.image;
|
|
301
|
+
if (img && img.sources) thumbnails = img.sources;
|
|
541
302
|
const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || [];
|
|
542
303
|
let cName = "";
|
|
543
|
-
let vCount = null;
|
|
544
|
-
let pAt = null;
|
|
545
304
|
for (const row of rows) {
|
|
546
305
|
for (const part of row.metadataParts || []) {
|
|
547
306
|
const text = part.text?.content || "";
|
|
548
|
-
if (text.includes("
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
} else if (
|
|
552
|
-
pAt = text;
|
|
553
|
-
} else {
|
|
307
|
+
if (text.includes("video")) {
|
|
308
|
+
const cleaned = text.replace(/[^0-9]/g, "");
|
|
309
|
+
if (cleaned) videoCountNum = parseInt(cleaned, 10);
|
|
310
|
+
} else if (!cName) {
|
|
554
311
|
cName = text;
|
|
555
312
|
}
|
|
556
313
|
}
|
|
@@ -558,206 +315,761 @@ var VideoCompact = class extends Base {
|
|
|
558
315
|
if (cName) {
|
|
559
316
|
channelObj = { name: cName.replace(/•/g, "").trim() };
|
|
560
317
|
}
|
|
561
|
-
viewCountNum = vCount;
|
|
562
|
-
pubAt = pAt;
|
|
563
318
|
}
|
|
564
|
-
this.id =
|
|
319
|
+
this.id = playlistId;
|
|
565
320
|
this.title = titleText;
|
|
566
321
|
this.thumbnails = new Thumbnails(thumbnails);
|
|
567
|
-
this.
|
|
568
|
-
this.isLive = isLive;
|
|
322
|
+
this.videoCount = videoCountNum;
|
|
569
323
|
this.channel = channelObj;
|
|
570
|
-
this.viewCount = viewCountNum;
|
|
571
|
-
this.publishedAt = pubAt;
|
|
572
324
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// src/channel-compact.ts
|
|
328
|
+
var ChannelCompact = class extends Base {
|
|
329
|
+
id;
|
|
330
|
+
name;
|
|
331
|
+
thumbnails;
|
|
332
|
+
subscriberCount;
|
|
333
|
+
constructor(client, data) {
|
|
334
|
+
super(client);
|
|
335
|
+
let channelId = "";
|
|
336
|
+
let nameText = "";
|
|
337
|
+
let thumbnails = [];
|
|
338
|
+
let subCount = null;
|
|
339
|
+
if (data.channelId) {
|
|
340
|
+
channelId = data.channelId;
|
|
341
|
+
nameText = data.title?.simpleText || data.title?.runs?.[0]?.text || "";
|
|
342
|
+
thumbnails = data.thumbnail?.thumbnails || [];
|
|
343
|
+
if (data.subscriberCountText) {
|
|
344
|
+
subCount = data.subscriberCountText.simpleText || data.subscriberCountText.runs?.[0]?.text || null;
|
|
345
|
+
} else if (data.videoSubscriberCountText) {
|
|
346
|
+
subCount = data.videoSubscriberCountText.simpleText || data.videoSubscriberCountText.runs?.[0]?.text || null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
this.id = channelId;
|
|
350
|
+
this.name = nameText;
|
|
351
|
+
this.thumbnails = new Thumbnails(thumbnails);
|
|
352
|
+
this.subscriberCount = subCount;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/search-result.ts
|
|
357
|
+
var SearchResult = class _SearchResult extends Continuable {
|
|
358
|
+
constructor(client, initialData) {
|
|
359
|
+
super(client);
|
|
360
|
+
const { items, continuation } = _SearchResult.parseData(client, initialData);
|
|
361
|
+
this.items = items;
|
|
362
|
+
this.continuation = continuation;
|
|
363
|
+
}
|
|
364
|
+
async fetch() {
|
|
365
|
+
if (!this.continuation) {
|
|
366
|
+
return { items: [], continuation: null };
|
|
367
|
+
}
|
|
368
|
+
const data = await this.client.request("search", {
|
|
369
|
+
continuation: this.continuation
|
|
370
|
+
});
|
|
371
|
+
return _SearchResult.parseData(this.client, data);
|
|
372
|
+
}
|
|
373
|
+
static parseData(client, data) {
|
|
374
|
+
const items = [];
|
|
375
|
+
let continuation = null;
|
|
376
|
+
function traverse(obj) {
|
|
377
|
+
if (!obj || typeof obj !== "object") return;
|
|
378
|
+
if (Array.isArray(obj)) {
|
|
379
|
+
for (const item of obj) traverse(item);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (obj.videoRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
|
|
383
|
+
items.push(new VideoCompact(client, obj.videoRenderer || obj));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (obj.playlistRenderer) {
|
|
387
|
+
items.push(new PlaylistCompact(client, obj.playlistRenderer));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (obj.channelRenderer) {
|
|
391
|
+
items.push(new ChannelCompact(client, obj.channelRenderer));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (obj.continuationCommand && obj.continuationCommand.token) {
|
|
395
|
+
continuation = obj.continuationCommand.token;
|
|
396
|
+
} else if (obj.continuationItemRenderer?.continuationEndpoint) {
|
|
397
|
+
continuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
|
|
398
|
+
}
|
|
399
|
+
for (const key of Object.keys(obj)) {
|
|
400
|
+
traverse(obj[key]);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
traverse(data);
|
|
404
|
+
return { items, continuation };
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// src/base-channel.ts
|
|
409
|
+
var TAB_PARAMS = {
|
|
410
|
+
videos: "EgZ2aWRlb3PyBgQKAjoA",
|
|
411
|
+
shorts: "EgZzaG9ydHPyBgUKA5oBAA%3D%3D",
|
|
412
|
+
live: "EgdzdHJlYW1z8gYECgJ6AA%3D%3D",
|
|
413
|
+
playlists: "EglwbGF5bGlzdHPyBgQKAkIA",
|
|
414
|
+
posts: "EgVwb3N0c_IGBAoCSgA%3D"
|
|
415
|
+
};
|
|
416
|
+
var ChannelVideos = class extends Continuable {
|
|
417
|
+
constructor(client, channel) {
|
|
418
|
+
super(client);
|
|
419
|
+
this.channel = channel;
|
|
420
|
+
}
|
|
421
|
+
channel;
|
|
422
|
+
async fetch() {
|
|
423
|
+
const data = await this.client.request("browse", {
|
|
424
|
+
browseId: this.channel.id,
|
|
425
|
+
params: TAB_PARAMS.videos,
|
|
426
|
+
continuation: this.continuation || void 0
|
|
427
|
+
});
|
|
428
|
+
return parseTabItems(this.client, data, "videos");
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
var ChannelShorts = class extends Continuable {
|
|
432
|
+
constructor(client, channel) {
|
|
433
|
+
super(client);
|
|
434
|
+
this.channel = channel;
|
|
435
|
+
}
|
|
436
|
+
channel;
|
|
437
|
+
async fetch() {
|
|
438
|
+
const data = await this.client.request("browse", {
|
|
439
|
+
browseId: this.channel.id,
|
|
440
|
+
params: TAB_PARAMS.shorts,
|
|
441
|
+
continuation: this.continuation || void 0
|
|
442
|
+
});
|
|
443
|
+
return parseTabItems(this.client, data, "shorts");
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var ChannelLive = class extends Continuable {
|
|
447
|
+
constructor(client, channel) {
|
|
448
|
+
super(client);
|
|
449
|
+
this.channel = channel;
|
|
450
|
+
}
|
|
451
|
+
channel;
|
|
452
|
+
async fetch() {
|
|
453
|
+
const data = await this.client.request("browse", {
|
|
454
|
+
browseId: this.channel.id,
|
|
455
|
+
params: TAB_PARAMS.live,
|
|
456
|
+
continuation: this.continuation || void 0
|
|
457
|
+
});
|
|
458
|
+
return parseTabItems(this.client, data, "live");
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
var ChannelPlaylists = class extends Continuable {
|
|
462
|
+
constructor(client, channel) {
|
|
463
|
+
super(client);
|
|
464
|
+
this.channel = channel;
|
|
465
|
+
}
|
|
466
|
+
channel;
|
|
467
|
+
async fetch() {
|
|
468
|
+
const data = await this.client.request("browse", {
|
|
469
|
+
browseId: this.channel.id,
|
|
470
|
+
params: TAB_PARAMS.playlists,
|
|
471
|
+
continuation: this.continuation || void 0
|
|
472
|
+
});
|
|
473
|
+
return parseTabItems(this.client, data, "playlists");
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
var ChannelPosts = class extends Continuable {
|
|
477
|
+
constructor(client, channel) {
|
|
478
|
+
super(client);
|
|
479
|
+
this.channel = channel;
|
|
480
|
+
}
|
|
481
|
+
channel;
|
|
482
|
+
async fetch() {
|
|
483
|
+
const data = await this.client.request("browse", {
|
|
484
|
+
browseId: this.channel.id,
|
|
485
|
+
params: TAB_PARAMS.posts,
|
|
486
|
+
continuation: this.continuation || void 0
|
|
487
|
+
});
|
|
488
|
+
return parseTabItems(this.client, data, "posts");
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
var BaseChannel = class extends Base {
|
|
492
|
+
id;
|
|
493
|
+
name;
|
|
494
|
+
handle;
|
|
495
|
+
description;
|
|
496
|
+
thumbnails;
|
|
497
|
+
subscriberCount;
|
|
498
|
+
videos;
|
|
499
|
+
shorts;
|
|
500
|
+
live;
|
|
501
|
+
playlists;
|
|
502
|
+
posts;
|
|
503
|
+
constructor(client, data) {
|
|
504
|
+
super(client);
|
|
505
|
+
this.id = "";
|
|
506
|
+
this.name = "";
|
|
507
|
+
this.videos = new ChannelVideos(client, this);
|
|
508
|
+
this.shorts = new ChannelShorts(client, this);
|
|
509
|
+
this.live = new ChannelLive(client, this);
|
|
510
|
+
this.playlists = new ChannelPlaylists(client, this);
|
|
511
|
+
this.posts = new ChannelPosts(client, this);
|
|
512
|
+
if (data) {
|
|
513
|
+
this.load(data);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
get url() {
|
|
517
|
+
return `https://www.youtube.com/channel/${this.id}`;
|
|
518
|
+
}
|
|
519
|
+
load(data) {
|
|
520
|
+
const headerObj = data.header?.c4TabbedHeaderRenderer || data.header?.pageHeaderRenderer || data.c4TabbedHeaderRenderer || data;
|
|
521
|
+
if (headerObj.pageTitle) {
|
|
522
|
+
this.name = headerObj.pageTitle;
|
|
523
|
+
}
|
|
524
|
+
const viewModel = headerObj.content?.pageHeaderViewModel;
|
|
525
|
+
if (viewModel) {
|
|
526
|
+
this.name = viewModel.title?.dynamicTextViewModel?.text?.content || this.name;
|
|
527
|
+
const thumbs = viewModel.image?.decoratedAvatarViewModel?.avatar?.avatarViewModel?.image?.sources || [];
|
|
528
|
+
this.thumbnails = new Thumbnails(thumbs);
|
|
529
|
+
const rows = viewModel.metadata?.contentMetadataViewModel?.metadataRows || [];
|
|
530
|
+
for (const row of rows) {
|
|
531
|
+
for (const part of row.metadataParts || []) {
|
|
532
|
+
const txt = part.text?.content || "";
|
|
533
|
+
if (txt.includes("subscriber")) {
|
|
534
|
+
this.subscriberCount = txt;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (headerObj.channelId) {
|
|
540
|
+
this.id = headerObj.channelId;
|
|
541
|
+
}
|
|
542
|
+
if (headerObj.title) {
|
|
543
|
+
this.name = headerObj.title.simpleText || headerObj.title.runs?.[0]?.text || this.name;
|
|
544
|
+
}
|
|
545
|
+
if (headerObj.subscriberCountText) {
|
|
546
|
+
this.subscriberCount = headerObj.subscriberCountText.simpleText || headerObj.subscriberCountText.runs?.[0]?.text || this.subscriberCount;
|
|
547
|
+
}
|
|
548
|
+
if (headerObj.thumbnail) {
|
|
549
|
+
const thumbs = headerObj.thumbnail.thumbnails || [];
|
|
550
|
+
this.thumbnails = new Thumbnails(thumbs);
|
|
551
|
+
}
|
|
552
|
+
this.id = this.id || data.id || data.channelId || "";
|
|
553
|
+
this.name = this.name || data.name || "";
|
|
554
|
+
if (!this.thumbnails || this.thumbnails.length === 0) {
|
|
555
|
+
const thumbs = data.thumbnails || data.thumbnail?.thumbnails || [];
|
|
556
|
+
this.thumbnails = new Thumbnails(thumbs);
|
|
557
|
+
}
|
|
558
|
+
if (!this.id) {
|
|
559
|
+
const serviceParams = data.responseContext?.serviceTrackingParams || [];
|
|
560
|
+
for (const sp of serviceParams) {
|
|
561
|
+
const browseIdParam = sp.params?.find((p) => p.key === "browse_id");
|
|
562
|
+
if (browseIdParam) {
|
|
563
|
+
this.id = browseIdParam.value;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return this;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
function parseTabItems(client, data, tabName) {
|
|
572
|
+
const items = [];
|
|
573
|
+
let continuation = null;
|
|
574
|
+
function traverse(obj) {
|
|
575
|
+
if (!obj || typeof obj !== "object") return;
|
|
576
|
+
if (Array.isArray(obj)) {
|
|
577
|
+
for (const item of obj) traverse(item);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (obj.videoRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
|
|
581
|
+
if (tabName !== "playlists") {
|
|
582
|
+
items.push(new VideoCompact(client, obj.videoRenderer || obj));
|
|
583
|
+
}
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (obj.playlistRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_PLAYLIST") {
|
|
587
|
+
if (tabName === "playlists") {
|
|
588
|
+
items.push(new PlaylistCompact(client, obj.playlistRenderer || obj));
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (obj.backstagePostRenderer || obj.sharedPostRenderer) {
|
|
593
|
+
if (tabName === "posts") {
|
|
594
|
+
items.push(obj);
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (obj.continuationCommand && obj.continuationCommand.token) {
|
|
599
|
+
continuation = obj.continuationCommand.token;
|
|
600
|
+
} else if (obj.continuationItemRenderer?.continuationEndpoint) {
|
|
601
|
+
continuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
|
|
602
|
+
}
|
|
603
|
+
for (const key of Object.keys(obj)) {
|
|
604
|
+
traverse(obj[key]);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
traverse(data);
|
|
608
|
+
return { items, continuation };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/video-related.ts
|
|
612
|
+
var VideoRelated = class extends Continuable {
|
|
613
|
+
constructor(client, video) {
|
|
614
|
+
super(client);
|
|
615
|
+
this.video = video;
|
|
616
|
+
}
|
|
617
|
+
video;
|
|
618
|
+
async fetch() {
|
|
619
|
+
if (!this.continuation) {
|
|
620
|
+
return { items: [], continuation: null };
|
|
621
|
+
}
|
|
622
|
+
const data = await this.client.request("next", {
|
|
623
|
+
continuation: this.continuation
|
|
624
|
+
});
|
|
625
|
+
return parseRelatedData(this.client, data);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
function parseRelatedData(client, data) {
|
|
629
|
+
const items = [];
|
|
630
|
+
let continuation = null;
|
|
631
|
+
function traverse(obj) {
|
|
632
|
+
if (!obj || typeof obj !== "object") return;
|
|
633
|
+
if (Array.isArray(obj)) {
|
|
634
|
+
for (const item of obj) traverse(item);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (obj.compactVideoRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
|
|
638
|
+
items.push(new VideoCompact(client, obj.compactVideoRenderer || obj));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (obj.compactRadioRenderer || obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_PLAYLIST") {
|
|
642
|
+
items.push(new PlaylistCompact(client, obj.compactRadioRenderer || obj));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (obj.continuationCommand && obj.continuationCommand.token) {
|
|
646
|
+
continuation = obj.continuationCommand.token;
|
|
647
|
+
} else if (obj.continuationItemRenderer?.continuationEndpoint) {
|
|
648
|
+
continuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
|
|
649
|
+
}
|
|
650
|
+
for (const key of Object.keys(obj)) {
|
|
651
|
+
traverse(obj[key]);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const secondaryContents = data.contents?.twoColumnWatchNextResults?.secondaryResults?.secondaryResults?.results || data.onResponseReceivedEndpoints?.[0]?.appendContinuationItemsAction?.continuationItems || [];
|
|
655
|
+
if (secondaryContents.length > 0) {
|
|
656
|
+
traverse(secondaryContents);
|
|
657
|
+
} else {
|
|
658
|
+
traverse(data);
|
|
659
|
+
}
|
|
660
|
+
return { items, continuation };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/caption.ts
|
|
664
|
+
var CaptionLanguage = class {
|
|
665
|
+
captions;
|
|
666
|
+
name;
|
|
667
|
+
code;
|
|
668
|
+
isTranslatable;
|
|
669
|
+
url;
|
|
670
|
+
constructor(attr) {
|
|
671
|
+
this.captions = attr.captions;
|
|
672
|
+
this.name = attr.name;
|
|
673
|
+
this.code = attr.code;
|
|
674
|
+
this.isTranslatable = attr.isTranslatable;
|
|
675
|
+
this.url = attr.url;
|
|
676
|
+
}
|
|
677
|
+
async get(translationLanguageCode) {
|
|
678
|
+
return this.captions.get(this.code, translationLanguageCode);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
var Caption = class {
|
|
682
|
+
duration;
|
|
683
|
+
start;
|
|
684
|
+
text;
|
|
685
|
+
segments;
|
|
686
|
+
constructor(attr) {
|
|
687
|
+
Object.assign(this, attr);
|
|
688
|
+
}
|
|
689
|
+
get end() {
|
|
690
|
+
return this.start + this.duration;
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// src/video-captions.ts
|
|
695
|
+
var VideoCaptions = class extends Base {
|
|
696
|
+
video;
|
|
697
|
+
languages = [];
|
|
698
|
+
constructor(attr) {
|
|
699
|
+
super(attr.client);
|
|
700
|
+
this.video = attr.video;
|
|
701
|
+
}
|
|
702
|
+
load(data) {
|
|
703
|
+
const captionTracks = data?.captionTracks || [];
|
|
704
|
+
this.languages = captionTracks.map((track) => new CaptionLanguage({
|
|
705
|
+
captions: this,
|
|
706
|
+
name: track.name?.simpleText || track.name?.runs?.[0]?.text || "",
|
|
707
|
+
code: track.languageCode,
|
|
708
|
+
isTranslatable: !!track.isTranslatable,
|
|
709
|
+
url: track.baseUrl
|
|
710
|
+
}));
|
|
711
|
+
return this;
|
|
579
712
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
713
|
+
async get(languageCode, translationLanguageCode) {
|
|
714
|
+
if (!languageCode) {
|
|
715
|
+
languageCode = "en";
|
|
716
|
+
}
|
|
717
|
+
const lang = this.languages.find((l) => l.code.toUpperCase() === languageCode?.toUpperCase());
|
|
718
|
+
const url = lang?.url;
|
|
719
|
+
if (!url) return void 0;
|
|
720
|
+
let fetchUrl = `${url}&fmt=json3`;
|
|
721
|
+
if (translationLanguageCode) {
|
|
722
|
+
fetchUrl += `&tlang=${translationLanguageCode}`;
|
|
723
|
+
}
|
|
724
|
+
const response = await this.client.fetch(fetchUrl);
|
|
725
|
+
if (!response.ok) {
|
|
726
|
+
throw new Error(`Failed to fetch caption: ${response.status}`);
|
|
727
|
+
}
|
|
728
|
+
const json = await response.json();
|
|
729
|
+
const events = json.events || [];
|
|
730
|
+
const captions = [];
|
|
731
|
+
for (const e of events) {
|
|
732
|
+
if (e.segs === void 0) continue;
|
|
733
|
+
captions.push(new Caption({
|
|
734
|
+
duration: e.dDurationMs || 0,
|
|
735
|
+
start: e.tStartMs || 0,
|
|
736
|
+
text: e.segs.map((s) => s.utf8).join(""),
|
|
737
|
+
segments: e.segs
|
|
738
|
+
}));
|
|
739
|
+
}
|
|
740
|
+
return captions;
|
|
592
741
|
}
|
|
593
742
|
};
|
|
594
743
|
|
|
595
|
-
// src/
|
|
596
|
-
var
|
|
744
|
+
// src/base-video.ts
|
|
745
|
+
var BaseVideo = class extends Base {
|
|
597
746
|
id;
|
|
598
747
|
title;
|
|
748
|
+
description;
|
|
599
749
|
thumbnails;
|
|
600
|
-
|
|
750
|
+
viewCount;
|
|
751
|
+
uploadDate;
|
|
601
752
|
channel;
|
|
753
|
+
channels;
|
|
754
|
+
isLiveContent;
|
|
755
|
+
likeCount;
|
|
756
|
+
tags;
|
|
757
|
+
formats;
|
|
758
|
+
adaptiveFormats;
|
|
759
|
+
streamingData;
|
|
760
|
+
related;
|
|
761
|
+
captions;
|
|
602
762
|
constructor(client, data) {
|
|
603
763
|
super(client);
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const byline = data.shortBylineText || data.longBylineText || data.ownerText;
|
|
622
|
-
if (byline && byline.runs && byline.runs[0]) {
|
|
623
|
-
const run = byline.runs[0];
|
|
624
|
-
channelObj = {
|
|
625
|
-
id: run.navigationEndpoint?.browseEndpoint?.browseId || void 0,
|
|
626
|
-
name: run.text
|
|
627
|
-
};
|
|
628
|
-
}
|
|
764
|
+
this.id = "";
|
|
765
|
+
this.title = "";
|
|
766
|
+
this.description = "";
|
|
767
|
+
this.thumbnails = new Thumbnails([]);
|
|
768
|
+
this.viewCount = null;
|
|
769
|
+
this.uploadDate = null;
|
|
770
|
+
this.channel = null;
|
|
771
|
+
this.channels = null;
|
|
772
|
+
this.isLiveContent = false;
|
|
773
|
+
this.likeCount = null;
|
|
774
|
+
this.tags = [];
|
|
775
|
+
this.formats = [];
|
|
776
|
+
this.adaptiveFormats = [];
|
|
777
|
+
this.related = new VideoRelated(client, this);
|
|
778
|
+
this.captions = null;
|
|
779
|
+
if (data) {
|
|
780
|
+
this.parse(data);
|
|
629
781
|
}
|
|
630
|
-
this.id = playlistId;
|
|
631
|
-
this.title = titleText;
|
|
632
|
-
this.thumbnails = new Thumbnails(thumbnails);
|
|
633
|
-
this.videoCount = videoCountNum;
|
|
634
|
-
this.channel = channelObj;
|
|
635
782
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
783
|
+
parse(data) {
|
|
784
|
+
const videoDetails = data.videoDetails || {};
|
|
785
|
+
const playerResponse = data.playerResponse || data;
|
|
786
|
+
const watchResponse = data.response || {};
|
|
787
|
+
this.id = videoDetails.videoId || playerResponse.videoDetails?.videoId || "";
|
|
788
|
+
this.title = videoDetails.title || playerResponse.videoDetails?.title || "";
|
|
789
|
+
this.description = videoDetails.shortDescription || playerResponse.videoDetails?.shortDescription || "";
|
|
790
|
+
const thumbs = videoDetails.thumbnail?.thumbnails || playerResponse.videoDetails?.thumbnail?.thumbnails || [];
|
|
791
|
+
this.thumbnails = new Thumbnails(thumbs);
|
|
792
|
+
this.viewCount = videoDetails.viewCount ? parseInt(videoDetails.viewCount, 10) : playerResponse.videoDetails?.viewCount ? parseInt(playerResponse.videoDetails.viewCount, 10) : null;
|
|
793
|
+
this.isLiveContent = videoDetails.isLiveContent || playerResponse.videoDetails?.isLiveContent || false;
|
|
794
|
+
const authorId = videoDetails.channelId || playerResponse.videoDetails?.channelId || "";
|
|
795
|
+
const authorName = videoDetails.author || playerResponse.videoDetails?.author || "";
|
|
796
|
+
if (authorId || authorName) {
|
|
797
|
+
this.channel = new BaseChannel(this.client, {
|
|
798
|
+
channelId: authorId,
|
|
799
|
+
title: { simpleText: authorName }
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
const watchNextResults = watchResponse.contents?.twoColumnWatchNextResults || data.contents?.twoColumnWatchNextResults;
|
|
803
|
+
const primaryInfo = watchNextResults?.results?.results?.contents?.find((c) => c.videoPrimaryInfoRenderer)?.videoPrimaryInfoRenderer;
|
|
804
|
+
if (primaryInfo) {
|
|
805
|
+
this.uploadDate = primaryInfo.dateText?.simpleText || primaryInfo.dateText?.runs?.[0]?.text || null;
|
|
806
|
+
const topLevelButtons = primaryInfo.videoActions?.menuRenderer?.topLevelButtons || [];
|
|
807
|
+
let likeButtonText = "";
|
|
808
|
+
for (const button of topLevelButtons) {
|
|
809
|
+
const renderer = button.toggleButtonRenderer || button.buttonRenderer;
|
|
810
|
+
if (renderer) {
|
|
811
|
+
const label = renderer.defaultText?.accessibility?.accessibilityData?.accessibilityData || renderer.accessibilityData?.accessibilityData?.label;
|
|
812
|
+
if (label && label.toLowerCase().includes("like")) {
|
|
813
|
+
likeButtonText = label;
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
658
817
|
}
|
|
818
|
+
if (likeButtonText) {
|
|
819
|
+
this.likeCount = parseInt(likeButtonText.replace(/[^0-9]/g, "") || "0", 10) || null;
|
|
820
|
+
}
|
|
821
|
+
const runs = primaryInfo.superTitleLink?.runs || [];
|
|
822
|
+
this.tags = runs.map((r) => r.text.trim()).filter((t) => t.startsWith("#"));
|
|
659
823
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
824
|
+
const streamingData = data.streamingData || playerResponse.streamingData;
|
|
825
|
+
if (streamingData) {
|
|
826
|
+
this.formats = streamingData.formats || [];
|
|
827
|
+
this.adaptiveFormats = streamingData.adaptiveFormats || [];
|
|
828
|
+
const parseFormat = (f) => ({
|
|
829
|
+
itag: f.itag,
|
|
830
|
+
url: f.url,
|
|
831
|
+
mimeType: f.mimeType,
|
|
832
|
+
bitrate: f.bitrate,
|
|
833
|
+
width: f.width,
|
|
834
|
+
height: f.height,
|
|
835
|
+
hasVideo: !!f.width || f.mimeType?.includes("video/"),
|
|
836
|
+
hasAudio: !!f.audioBitrate || !!f.audioChannels || f.mimeType?.includes("audio/") || f.mimeType?.includes("video/") && (f.mimeType?.includes("mp4a") || f.mimeType?.includes("opus") || f.mimeType?.includes("vorbis") || f.mimeType?.includes("ec-3")),
|
|
837
|
+
isLive: this.isLiveContent,
|
|
838
|
+
contentLength: f.contentLength,
|
|
839
|
+
quality: f.quality,
|
|
840
|
+
qualityLabel: f.qualityLabel,
|
|
841
|
+
audioQuality: f.audioQuality,
|
|
842
|
+
approxDurationMs: f.approxDurationMs
|
|
843
|
+
});
|
|
844
|
+
this.streamingData = {
|
|
845
|
+
expiresInSeconds: streamingData.expiresInSeconds || "0",
|
|
846
|
+
formats: this.formats.map(parseFormat),
|
|
847
|
+
adaptiveFormats: this.adaptiveFormats.map(parseFormat)
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
const relatedRes = parseRelatedData(this.client, data);
|
|
851
|
+
this.related.items = relatedRes.items;
|
|
852
|
+
this.related.continuation = relatedRes.continuation;
|
|
853
|
+
const captionTracks = playerResponse.captions?.playerCaptionsTracklistRenderer;
|
|
854
|
+
if (captionTracks) {
|
|
855
|
+
this.captions = new VideoCaptions({ video: this, client: this.client }).load(captionTracks);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
get upNext() {
|
|
859
|
+
return this.related.items[0] || null;
|
|
664
860
|
}
|
|
665
861
|
};
|
|
666
862
|
|
|
667
|
-
// src/
|
|
668
|
-
var
|
|
669
|
-
constructor(client,
|
|
863
|
+
// src/comment.ts
|
|
864
|
+
var CommentReplies = class extends Continuable {
|
|
865
|
+
constructor(client, comment) {
|
|
670
866
|
super(client);
|
|
671
|
-
|
|
672
|
-
this.items = items;
|
|
673
|
-
this.continuation = continuation;
|
|
867
|
+
this.comment = comment;
|
|
674
868
|
}
|
|
869
|
+
comment;
|
|
675
870
|
async fetch() {
|
|
676
871
|
if (!this.continuation) {
|
|
677
872
|
return { items: [], continuation: null };
|
|
678
873
|
}
|
|
679
|
-
const data = await this.client.request("
|
|
874
|
+
const data = await this.client.request("next", {
|
|
680
875
|
continuation: this.continuation
|
|
681
876
|
});
|
|
682
|
-
return _SearchResult.parseData(this.client, data);
|
|
683
|
-
}
|
|
684
|
-
static parseData(client, data) {
|
|
685
877
|
const items = [];
|
|
686
|
-
let
|
|
878
|
+
let nextContinuation = null;
|
|
879
|
+
const mutations = data.frameworkUpdates?.entityBatchUpdate?.mutations || [];
|
|
880
|
+
for (const mutation of mutations) {
|
|
881
|
+
const payload = mutation.payload?.commentEntityPayload;
|
|
882
|
+
if (payload) {
|
|
883
|
+
items.push(new Comment(this.client, { payload, video: this.comment.video }));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
687
886
|
function traverse(obj) {
|
|
688
887
|
if (!obj || typeof obj !== "object") return;
|
|
689
|
-
if (
|
|
690
|
-
|
|
888
|
+
if (obj.continuationCommand && obj.continuationCommand.token) {
|
|
889
|
+
nextContinuation = obj.continuationCommand.token;
|
|
691
890
|
return;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
items.push(new VideoCompact(client, obj.videoRenderer || obj));
|
|
891
|
+
} else if (obj.continuationItemRenderer?.continuationEndpoint) {
|
|
892
|
+
nextContinuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
|
|
695
893
|
return;
|
|
696
894
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
return;
|
|
895
|
+
for (const key of Object.keys(obj)) {
|
|
896
|
+
traverse(obj[key]);
|
|
700
897
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
898
|
+
}
|
|
899
|
+
traverse(data);
|
|
900
|
+
return { items, continuation: nextContinuation };
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
var VideoComments = class extends Continuable {
|
|
904
|
+
constructor(client, video) {
|
|
905
|
+
super(client);
|
|
906
|
+
this.video = video;
|
|
907
|
+
}
|
|
908
|
+
video;
|
|
909
|
+
async fetch() {
|
|
910
|
+
if (!this.continuation) {
|
|
911
|
+
return { items: [], continuation: null };
|
|
912
|
+
}
|
|
913
|
+
const data = await this.client.request("next", {
|
|
914
|
+
continuation: this.continuation
|
|
915
|
+
});
|
|
916
|
+
const items = [];
|
|
917
|
+
let nextContinuation = null;
|
|
918
|
+
const mutations = data.frameworkUpdates?.entityBatchUpdate?.mutations || [];
|
|
919
|
+
for (const mutation of mutations) {
|
|
920
|
+
const payload = mutation.payload?.commentEntityPayload;
|
|
921
|
+
if (payload) {
|
|
922
|
+
items.push(new Comment(this.client, { payload, video: this.video }));
|
|
704
923
|
}
|
|
924
|
+
}
|
|
925
|
+
function traverse(obj) {
|
|
926
|
+
if (!obj || typeof obj !== "object") return;
|
|
705
927
|
if (obj.continuationCommand && obj.continuationCommand.token) {
|
|
706
|
-
|
|
928
|
+
nextContinuation = obj.continuationCommand.token;
|
|
929
|
+
return;
|
|
707
930
|
} else if (obj.continuationItemRenderer?.continuationEndpoint) {
|
|
708
|
-
|
|
931
|
+
nextContinuation = obj.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token || null;
|
|
932
|
+
return;
|
|
709
933
|
}
|
|
710
934
|
for (const key of Object.keys(obj)) {
|
|
711
935
|
traverse(obj[key]);
|
|
712
936
|
}
|
|
713
937
|
}
|
|
714
938
|
traverse(data);
|
|
715
|
-
return { items, continuation };
|
|
939
|
+
return { items, continuation: nextContinuation };
|
|
716
940
|
}
|
|
717
941
|
};
|
|
718
|
-
|
|
719
|
-
// src/base-video.ts
|
|
720
|
-
var BaseVideo = class extends Base {
|
|
942
|
+
var Comment = class extends Base {
|
|
721
943
|
id;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
viewCount;
|
|
944
|
+
video;
|
|
945
|
+
author;
|
|
946
|
+
content;
|
|
726
947
|
publishDate;
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
948
|
+
likeCount;
|
|
949
|
+
isAuthorChannelOwner;
|
|
950
|
+
isPinned;
|
|
951
|
+
replyCount;
|
|
952
|
+
replies;
|
|
953
|
+
constructor(client, options) {
|
|
730
954
|
super(client);
|
|
955
|
+
this.video = options.video;
|
|
731
956
|
this.id = "";
|
|
732
|
-
this.
|
|
733
|
-
this.
|
|
734
|
-
this.
|
|
735
|
-
this.
|
|
736
|
-
this.
|
|
737
|
-
this.
|
|
738
|
-
this.
|
|
957
|
+
this.content = "";
|
|
958
|
+
this.publishDate = "";
|
|
959
|
+
this.likeCount = 0;
|
|
960
|
+
this.isAuthorChannelOwner = false;
|
|
961
|
+
this.isPinned = false;
|
|
962
|
+
this.replyCount = 0;
|
|
963
|
+
this.author = new BaseChannel(client);
|
|
964
|
+
this.replies = new CommentReplies(client, this);
|
|
965
|
+
this.load(options.payload);
|
|
739
966
|
}
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
this.
|
|
746
|
-
this.
|
|
747
|
-
this.
|
|
748
|
-
this.
|
|
749
|
-
this.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
967
|
+
load(payload) {
|
|
968
|
+
const props = payload.properties || {};
|
|
969
|
+
const toolbar = payload.toolbar || {};
|
|
970
|
+
const authorInfo = payload.author || {};
|
|
971
|
+
const avatar = payload.avatar || {};
|
|
972
|
+
this.id = props.commentId || payload.commentId || "";
|
|
973
|
+
this.content = props.content?.content || payload.contentText?.simpleText || payload.contentText?.runs?.map((r) => r.text).join("") || "";
|
|
974
|
+
this.publishDate = props.publishedTime || payload.publishedTimeText?.simpleText || "";
|
|
975
|
+
this.isAuthorChannelOwner = !!authorInfo.isCreator || !!payload.authorIsCreator;
|
|
976
|
+
this.isPinned = !!payload.pinned;
|
|
977
|
+
const likeStr = toolbar.likeCountLiked || toolbar.likeCountNotliked || payload.voteCount?.simpleText || "0";
|
|
978
|
+
this.likeCount = parseInt(String(likeStr).replace(/[^0-9]/g, "") || "0", 10);
|
|
979
|
+
const replyStr = toolbar.replyCount || payload.replyCount || "0";
|
|
980
|
+
this.replyCount = parseInt(String(replyStr).replace(/[^0-9]/g, "") || "0", 10);
|
|
981
|
+
const authorId = authorInfo.id || payload.authorEndpoint?.browseEndpoint?.browseId || "";
|
|
982
|
+
const authorName = authorInfo.displayName || payload.authorText?.simpleText || payload.authorText?.runs?.[0]?.text || "";
|
|
983
|
+
const thumbs = avatar.image?.sources || payload.authorThumbnail?.thumbnails || [];
|
|
984
|
+
this.author = new BaseChannel(this.client, {
|
|
985
|
+
channelId: authorId,
|
|
986
|
+
title: { simpleText: authorName },
|
|
987
|
+
thumbnail: { thumbnails: thumbs }
|
|
988
|
+
});
|
|
989
|
+
return this;
|
|
990
|
+
}
|
|
991
|
+
get url() {
|
|
992
|
+
return `https://www.youtube.com/watch?v=${this.video.id}&lc=${this.id}`;
|
|
753
993
|
}
|
|
754
994
|
};
|
|
755
995
|
|
|
756
996
|
// src/video.ts
|
|
757
997
|
var Video = class extends BaseVideo {
|
|
758
|
-
|
|
998
|
+
duration;
|
|
999
|
+
chapters;
|
|
1000
|
+
comments;
|
|
1001
|
+
music;
|
|
759
1002
|
constructor(client, data) {
|
|
760
|
-
super(client
|
|
1003
|
+
super(client);
|
|
1004
|
+
this.duration = 0;
|
|
1005
|
+
this.chapters = [];
|
|
1006
|
+
this.music = null;
|
|
1007
|
+
this.comments = new VideoComments(client, this);
|
|
1008
|
+
this.load(data);
|
|
1009
|
+
}
|
|
1010
|
+
load(data) {
|
|
1011
|
+
super.parse(data);
|
|
1012
|
+
const videoDetails = data.videoDetails || data.playerResponse?.videoDetails || {};
|
|
1013
|
+
this.duration = parseInt(videoDetails.lengthSeconds || "0", 10);
|
|
1014
|
+
this.chapters = [];
|
|
1015
|
+
const markersMap = data.response?.playerOverlays?.playerOverlayRenderer?.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer?.playerBar?.multiMarkersPlayerBarRenderer?.markersMap;
|
|
1016
|
+
const chaptersList = markersMap?.[0]?.value?.chapters || [];
|
|
1017
|
+
for (const chap of chaptersList) {
|
|
1018
|
+
const renderer = chap.chapterRenderer;
|
|
1019
|
+
if (renderer) {
|
|
1020
|
+
this.chapters.push({
|
|
1021
|
+
title: renderer.title?.simpleText || renderer.title?.runs?.[0]?.text || "",
|
|
1022
|
+
start: renderer.timeRangeStartMillis || 0,
|
|
1023
|
+
thumbnails: new Thumbnails(renderer.thumbnail?.thumbnails || [])
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
this.music = null;
|
|
1028
|
+
const panels = data.response?.engagementPanels || [];
|
|
1029
|
+
const structuredPanel = panels.find((p) => p.engagementPanelSectionListRenderer?.content?.structuredDescriptionContentRenderer);
|
|
1030
|
+
const descriptionItems = structuredPanel?.engagementPanelSectionListRenderer?.content?.structuredDescriptionContentRenderer?.items || [];
|
|
1031
|
+
const musicItem = descriptionItems.find((i) => i.horizontalCardListRenderer?.footerButton?.buttonViewModel?.iconName === "MUSIC" || i.horizontalCardListRenderer?.cards?.some((c) => c.videoAttributeViewModel));
|
|
1032
|
+
if (musicItem) {
|
|
1033
|
+
const card = musicItem.horizontalCardListRenderer?.cards?.find((c) => c.videoAttributeViewModel)?.videoAttributeViewModel;
|
|
1034
|
+
if (card) {
|
|
1035
|
+
this.music = {
|
|
1036
|
+
imageUrl: card.image?.sources?.[0]?.url || "",
|
|
1037
|
+
title: card.title || "",
|
|
1038
|
+
artist: card.subtitle || "",
|
|
1039
|
+
album: card.secondarySubtitle?.content || null
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const watchNextResults = data.response?.contents?.twoColumnWatchNextResults || data.contents?.twoColumnWatchNextResults;
|
|
1044
|
+
const contents = watchNextResults?.results?.results?.contents || [];
|
|
1045
|
+
let commentToken = null;
|
|
1046
|
+
for (const content of contents) {
|
|
1047
|
+
const itemSection = content.itemSectionRenderer;
|
|
1048
|
+
if (itemSection?.contents) {
|
|
1049
|
+
for (const item of itemSection.contents) {
|
|
1050
|
+
if (item.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token) {
|
|
1051
|
+
commentToken = item.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
if (item.commentsEntryPointHeaderRenderer?.content?.commentsEntryPointHeaderRenderer?.headerText?.runs?.[0]?.navigationEndpoint?.continuationCommand?.token) {
|
|
1055
|
+
commentToken = item.commentsEntryPointHeaderRenderer.content.commentsEntryPointHeaderRenderer.headerText.runs[0].navigationEndpoint.continuationCommand.token;
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (commentToken) break;
|
|
1061
|
+
}
|
|
1062
|
+
if (!commentToken) {
|
|
1063
|
+
for (const panel of panels) {
|
|
1064
|
+
const token = panel.engagementPanelSectionListRenderer?.content?.sectionListRenderer?.contents?.[0]?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
|
|
1065
|
+
if (token) {
|
|
1066
|
+
commentToken = token;
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
this.comments.continuation = commentToken;
|
|
1072
|
+
return this;
|
|
761
1073
|
}
|
|
762
1074
|
};
|
|
763
1075
|
|
|
@@ -867,18 +1179,293 @@ var Playlist = class extends Base {
|
|
|
867
1179
|
}
|
|
868
1180
|
};
|
|
869
1181
|
|
|
1182
|
+
// src/channel.ts
|
|
1183
|
+
var Channel = class extends BaseChannel {
|
|
1184
|
+
videoCount;
|
|
1185
|
+
banner;
|
|
1186
|
+
mobileBanner;
|
|
1187
|
+
tvBanner;
|
|
1188
|
+
shelves;
|
|
1189
|
+
constructor(client, data) {
|
|
1190
|
+
super(client, data);
|
|
1191
|
+
this.banner = new Thumbnails([]);
|
|
1192
|
+
this.mobileBanner = new Thumbnails([]);
|
|
1193
|
+
this.tvBanner = new Thumbnails([]);
|
|
1194
|
+
this.shelves = [];
|
|
1195
|
+
if (data) {
|
|
1196
|
+
this.load(data);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
load(data) {
|
|
1200
|
+
super.load(data);
|
|
1201
|
+
const headerObj = data.header?.c4TabbedHeaderRenderer || data.header?.pageHeaderRenderer || data.c4TabbedHeaderRenderer || data;
|
|
1202
|
+
const viewModel = headerObj.content?.pageHeaderViewModel;
|
|
1203
|
+
if (viewModel) {
|
|
1204
|
+
this.banner = new Thumbnails(viewModel.banner?.imageBannerViewModel?.image?.sources || []);
|
|
1205
|
+
this.handle = viewModel.metadata?.contentMetadataViewModel?.metadataRows?.[0]?.metadataParts?.[0]?.text?.content || this.handle;
|
|
1206
|
+
this.description = viewModel.description?.descriptionPreviewViewModel?.description?.content || this.description;
|
|
1207
|
+
const rows = viewModel.metadata?.contentMetadataViewModel?.metadataRows || [];
|
|
1208
|
+
for (const row of rows) {
|
|
1209
|
+
for (const part of row.metadataParts || []) {
|
|
1210
|
+
const txt = part.text?.content || "";
|
|
1211
|
+
if (txt.includes("video")) {
|
|
1212
|
+
this.videoCount = txt;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (headerObj.channelId) {
|
|
1218
|
+
this.videoCount = headerObj.videosCountText?.runs?.[0]?.text || headerObj.videosCountText?.simpleText || this.videoCount;
|
|
1219
|
+
this.banner = new Thumbnails(headerObj.banner?.thumbnails || []);
|
|
1220
|
+
this.tvBanner = new Thumbnails(headerObj.tvBanner?.thumbnails || []);
|
|
1221
|
+
this.mobileBanner = new Thumbnails(headerObj.mobileBanner?.thumbnails || []);
|
|
1222
|
+
}
|
|
1223
|
+
this.shelves = [];
|
|
1224
|
+
const tabs = data.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
|
|
1225
|
+
const homeTab = tabs.find((t) => t.tabRenderer?.selected || t.tabRenderer?.title?.toLowerCase() === "home");
|
|
1226
|
+
const sectionList = homeTab?.tabRenderer?.content?.sectionListRenderer?.contents || [];
|
|
1227
|
+
for (const section of sectionList) {
|
|
1228
|
+
const shelf = section.itemSectionRenderer?.contents?.[0]?.shelfRenderer;
|
|
1229
|
+
if (shelf) {
|
|
1230
|
+
const title = shelf.title?.simpleText || shelf.title?.runs?.[0]?.text || "";
|
|
1231
|
+
const subtitle = shelf.subtitle?.simpleText || shelf.subtitle?.runs?.[0]?.text || void 0;
|
|
1232
|
+
const items = [];
|
|
1233
|
+
const gridItems = shelf.content?.horizontalListRenderer?.items || shelf.content?.gridRenderer?.items || [];
|
|
1234
|
+
for (const item of gridItems) {
|
|
1235
|
+
if (item.gridVideoRenderer) {
|
|
1236
|
+
items.push(item.gridVideoRenderer);
|
|
1237
|
+
} else if (item.gridPlaylistRenderer) {
|
|
1238
|
+
items.push(item.gridPlaylistRenderer);
|
|
1239
|
+
} else if (item.gridChannelRenderer) {
|
|
1240
|
+
items.push(item.gridChannelRenderer);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
this.shelves.push({ title, subtitle, items });
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return this;
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
// src/mix-playlist.ts
|
|
1251
|
+
var MixPlaylist = class extends Base {
|
|
1252
|
+
id;
|
|
1253
|
+
title;
|
|
1254
|
+
videoCount;
|
|
1255
|
+
videos;
|
|
1256
|
+
constructor(client, data) {
|
|
1257
|
+
super(client);
|
|
1258
|
+
this.id = "";
|
|
1259
|
+
this.title = "";
|
|
1260
|
+
this.videoCount = 0;
|
|
1261
|
+
this.videos = [];
|
|
1262
|
+
if (data) {
|
|
1263
|
+
this.load(data);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
load(data) {
|
|
1267
|
+
const playlist = data.contents?.twoColumnWatchNextResults?.playlist?.playlist || {};
|
|
1268
|
+
this.title = playlist.titleText?.simpleText || playlist.title || "";
|
|
1269
|
+
this.id = playlist.playlistId || "";
|
|
1270
|
+
const contents = playlist.contents || [];
|
|
1271
|
+
this.videos = [];
|
|
1272
|
+
for (const item of contents) {
|
|
1273
|
+
const renderer = item.playlistPanelVideoRenderer;
|
|
1274
|
+
if (renderer) {
|
|
1275
|
+
this.videos.push(new VideoCompact(this.client, renderer));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
this.videoCount = this.videos.length;
|
|
1279
|
+
return this;
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
// src/chat.ts
|
|
1284
|
+
var Chat = class extends Base {
|
|
1285
|
+
id;
|
|
1286
|
+
message;
|
|
1287
|
+
author;
|
|
1288
|
+
timestamp;
|
|
1289
|
+
constructor(client, data) {
|
|
1290
|
+
super(client);
|
|
1291
|
+
if (data) {
|
|
1292
|
+
this.load(data);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
load(data) {
|
|
1296
|
+
const { id, message, authorName, authorPhoto, timestampUsec, authorExternalChannelId } = data;
|
|
1297
|
+
this.id = id;
|
|
1298
|
+
this.message = message?.runs?.map((r) => r.text).join("") || "";
|
|
1299
|
+
this.timestamp = parseInt(timestampUsec || "0", 10);
|
|
1300
|
+
this.author = new BaseChannel(this.client, {
|
|
1301
|
+
channelId: authorExternalChannelId,
|
|
1302
|
+
title: { simpleText: authorName },
|
|
1303
|
+
thumbnail: { thumbnails: authorPhoto }
|
|
1304
|
+
});
|
|
1305
|
+
return this;
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
// src/live-video.ts
|
|
1310
|
+
var LiveVideo = class extends BaseVideo {
|
|
1311
|
+
watchingCount = 0;
|
|
1312
|
+
chatContinuation = "";
|
|
1313
|
+
delay = 0;
|
|
1314
|
+
chatRequestPoolingTimeout = null;
|
|
1315
|
+
timeoutMs = 0;
|
|
1316
|
+
isChatPlaying = false;
|
|
1317
|
+
chatQueue = /* @__PURE__ */ new Set();
|
|
1318
|
+
pollSessionId = 0;
|
|
1319
|
+
listeners = {};
|
|
1320
|
+
constructor(client, data) {
|
|
1321
|
+
super(client, data);
|
|
1322
|
+
if (data) {
|
|
1323
|
+
this.load(data);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
on(event, listener) {
|
|
1327
|
+
if (!this.listeners[event]) {
|
|
1328
|
+
this.listeners[event] = [];
|
|
1329
|
+
}
|
|
1330
|
+
this.listeners[event].push(listener);
|
|
1331
|
+
return this;
|
|
1332
|
+
}
|
|
1333
|
+
emit(event, ...args) {
|
|
1334
|
+
const list = this.listeners[event];
|
|
1335
|
+
if (!list || list.length === 0) return false;
|
|
1336
|
+
for (const listener of list) {
|
|
1337
|
+
listener(...args);
|
|
1338
|
+
}
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1341
|
+
load(data) {
|
|
1342
|
+
super.parse(data);
|
|
1343
|
+
const videoDetails = data.videoDetails || {};
|
|
1344
|
+
const runText = data.response?.contents?.twoColumnWatchNextResults?.results?.results?.contents?.[0]?.videoPrimaryInfoRenderer?.viewCount?.videoViewCountRenderer?.viewCount?.runs?.map((r) => r.text).join(" ");
|
|
1345
|
+
const countStr = runText || videoDetails.viewCount || "0";
|
|
1346
|
+
this.watchingCount = parseInt(String(countStr).replace(/[^0-9]/g, "") || "0", 10);
|
|
1347
|
+
this.chatContinuation = data.response?.contents?.twoColumnWatchNextResults?.conversationBar?.liveChatRenderer?.continuations?.[0]?.reloadContinuationData?.continuation || "";
|
|
1348
|
+
return this;
|
|
1349
|
+
}
|
|
1350
|
+
playChat(delay = 0) {
|
|
1351
|
+
if (this.isChatPlaying) return;
|
|
1352
|
+
this.delay = delay;
|
|
1353
|
+
this.isChatPlaying = true;
|
|
1354
|
+
const sessionId = ++this.pollSessionId;
|
|
1355
|
+
this.pollChatContinuation(sessionId);
|
|
1356
|
+
}
|
|
1357
|
+
stopChat() {
|
|
1358
|
+
this.isChatPlaying = false;
|
|
1359
|
+
this.pollSessionId++;
|
|
1360
|
+
if (this.chatRequestPoolingTimeout) {
|
|
1361
|
+
clearTimeout(this.chatRequestPoolingTimeout);
|
|
1362
|
+
this.chatRequestPoolingTimeout = null;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
async pollChatContinuation(sessionId) {
|
|
1366
|
+
if (!this.isChatPlaying || !this.chatContinuation || sessionId !== this.pollSessionId) return;
|
|
1367
|
+
try {
|
|
1368
|
+
const response = await this.client.request("live_chat/get_live_chat", {
|
|
1369
|
+
continuation: this.chatContinuation
|
|
1370
|
+
});
|
|
1371
|
+
if (!response?.continuationContents?.liveChatContinuation || sessionId !== this.pollSessionId) return;
|
|
1372
|
+
const chatCont = response.continuationContents.liveChatContinuation;
|
|
1373
|
+
const actions = chatCont.actions || [];
|
|
1374
|
+
for (const action of actions) {
|
|
1375
|
+
const item = action.addChatItemAction?.item?.liveChatTextMessageRenderer;
|
|
1376
|
+
if (item) {
|
|
1377
|
+
const chat = new Chat(this.client).load(item);
|
|
1378
|
+
if (this.chatQueue.has(chat.id)) continue;
|
|
1379
|
+
this.chatQueue.add(chat.id);
|
|
1380
|
+
if (this.chatQueue.size > 500) {
|
|
1381
|
+
const firstKey = this.chatQueue.keys().next().value;
|
|
1382
|
+
if (firstKey !== void 0) {
|
|
1383
|
+
this.chatQueue.delete(firstKey);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
const timeout = chat.timestamp / 1e3 - (Date.now() - this.delay);
|
|
1387
|
+
setTimeout(() => {
|
|
1388
|
+
if (sessionId === this.pollSessionId) {
|
|
1389
|
+
this.emit("chat", chat);
|
|
1390
|
+
}
|
|
1391
|
+
}, Math.max(0, timeout));
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
const continuation = chatCont.continuations?.[0];
|
|
1395
|
+
const continuationData = continuation?.timedContinuationData || continuation?.invalidationContinuationData;
|
|
1396
|
+
if (continuationData && sessionId === this.pollSessionId) {
|
|
1397
|
+
this.timeoutMs = continuationData.timeoutMs || 5e3;
|
|
1398
|
+
this.chatContinuation = continuationData.continuation;
|
|
1399
|
+
this.chatRequestPoolingTimeout = setTimeout(() => this.pollChatContinuation(sessionId), this.timeoutMs);
|
|
1400
|
+
}
|
|
1401
|
+
} catch (e) {
|
|
1402
|
+
console.error("[LiveVideo] Error polling live chat:", e);
|
|
1403
|
+
if (sessionId === this.pollSessionId) {
|
|
1404
|
+
this.chatRequestPoolingTimeout = setTimeout(() => this.pollChatContinuation(sessionId), 5e3);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
|
|
870
1410
|
// src/client.ts
|
|
871
|
-
|
|
1411
|
+
var CLIENT_PROFILES = [
|
|
1412
|
+
{
|
|
1413
|
+
name: "ios",
|
|
1414
|
+
clientName: "IOS",
|
|
1415
|
+
clientVersion: "20.10.4",
|
|
1416
|
+
clientNameHeader: "5",
|
|
1417
|
+
userAgent: "com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)",
|
|
1418
|
+
context: {
|
|
1419
|
+
deviceMake: "Apple",
|
|
1420
|
+
deviceModel: "iPhone16,2",
|
|
1421
|
+
platform: "MOBILE",
|
|
1422
|
+
osName: "iOS",
|
|
1423
|
+
osVersion: "18.3.2.22D82"
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
{
|
|
1427
|
+
name: "android_vr",
|
|
1428
|
+
clientName: "ANDROID_VR",
|
|
1429
|
+
clientVersion: "1.62.20",
|
|
1430
|
+
clientNameHeader: "28",
|
|
1431
|
+
userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.20 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip",
|
|
1432
|
+
context: {
|
|
1433
|
+
deviceMake: "Oculus",
|
|
1434
|
+
deviceModel: "Quest 3",
|
|
1435
|
+
platform: "MOBILE",
|
|
1436
|
+
osName: "Android",
|
|
1437
|
+
osVersion: "12L",
|
|
1438
|
+
androidSdkVersion: 32
|
|
1439
|
+
}
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
name: "mweb",
|
|
1443
|
+
clientName: "MWEB",
|
|
1444
|
+
clientVersion: "2.20251209.01.00",
|
|
1445
|
+
clientNameHeader: "2",
|
|
1446
|
+
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1",
|
|
1447
|
+
context: {
|
|
1448
|
+
platform: "MOBILE",
|
|
1449
|
+
osName: "iOS",
|
|
1450
|
+
osVersion: "17.5.1"
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
];
|
|
1454
|
+
function getFallbackClientVersion() {
|
|
872
1455
|
const d = /* @__PURE__ */ new Date();
|
|
873
1456
|
d.setDate(d.getDate() - 2);
|
|
874
1457
|
const yyyymmdd = d.toISOString().split("T")[0].replace(/-/g, "");
|
|
875
1458
|
return `2.${yyyymmdd}.00.00`;
|
|
876
1459
|
}
|
|
877
1460
|
function getSapisidFromCookieString(cookieString) {
|
|
1461
|
+
if (!cookieString && typeof document !== "undefined") {
|
|
1462
|
+
cookieString = document.cookie;
|
|
1463
|
+
}
|
|
1464
|
+
if (!cookieString) return null;
|
|
878
1465
|
const match = cookieString.match(/__Secure-3PAPISID=([^;]+)/) || cookieString.match(/__Secure-1PAPISID=([^;]+)/) || cookieString.match(/SAPISID=([^;]+)/);
|
|
879
1466
|
return match ? match[1] : null;
|
|
880
1467
|
}
|
|
881
|
-
async function
|
|
1468
|
+
async function getSApiSidHash(sapisid, origin = "https://www.youtube.com") {
|
|
882
1469
|
if (!sapisid) return null;
|
|
883
1470
|
try {
|
|
884
1471
|
const timestamp = Math.floor(Date.now() / 1e3);
|
|
@@ -893,13 +1480,47 @@ async function getSApiSidHash2(sapisid, origin = "https://www.youtube.com") {
|
|
|
893
1480
|
return null;
|
|
894
1481
|
}
|
|
895
1482
|
}
|
|
1483
|
+
var cachedApiKey = null;
|
|
1484
|
+
var cachedClientVersion = null;
|
|
1485
|
+
var cachedIdToken = null;
|
|
1486
|
+
async function getInnerTubeConfig(injectedConfig, customFetch) {
|
|
1487
|
+
if (injectedConfig && injectedConfig.apiKey) {
|
|
1488
|
+
return {
|
|
1489
|
+
apiKey: injectedConfig.apiKey,
|
|
1490
|
+
clientVersion: injectedConfig.clientVersion || getFallbackClientVersion(),
|
|
1491
|
+
idToken: injectedConfig.idToken ?? null
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
if (cachedApiKey && cachedClientVersion) {
|
|
1495
|
+
return { apiKey: cachedApiKey, clientVersion: cachedClientVersion, idToken: cachedIdToken };
|
|
1496
|
+
}
|
|
1497
|
+
try {
|
|
1498
|
+
const fetchFn = customFetch || (typeof globalThis !== "undefined" ? globalThis.fetch : fetch);
|
|
1499
|
+
const response = await fetchFn("https://www.youtube.com", { credentials: "include" });
|
|
1500
|
+
const html = await response.text();
|
|
1501
|
+
const apiKeyMatch = html.match(/"INNERTUBE_API_KEY":"([^"]+)"/);
|
|
1502
|
+
const clientVersionMatch = html.match(/"INNERTUBE_CLIENT_VERSION":"([^"]+)"/);
|
|
1503
|
+
const idTokenMatch = html.match(/"ID_TOKEN":"([^"]+)"/);
|
|
1504
|
+
if (apiKeyMatch && apiKeyMatch[1]) cachedApiKey = apiKeyMatch[1];
|
|
1505
|
+
if (clientVersionMatch && clientVersionMatch[1]) cachedClientVersion = clientVersionMatch[1];
|
|
1506
|
+
if (idTokenMatch && idTokenMatch[1]) cachedIdToken = idTokenMatch[1];
|
|
1507
|
+
} catch (e) {
|
|
1508
|
+
console.warn("[Client] Failed to extract InnerTube config from HTML", e);
|
|
1509
|
+
}
|
|
1510
|
+
if (!cachedClientVersion) cachedClientVersion = getFallbackClientVersion();
|
|
1511
|
+
return { apiKey: cachedApiKey, clientVersion: cachedClientVersion, idToken: cachedIdToken };
|
|
1512
|
+
}
|
|
896
1513
|
var Client = class {
|
|
897
1514
|
apiKey = null;
|
|
898
1515
|
clientVersion = "";
|
|
899
1516
|
idToken = null;
|
|
900
1517
|
cookie = "";
|
|
1518
|
+
fetch;
|
|
1519
|
+
cache;
|
|
901
1520
|
constructor(options = {}) {
|
|
902
1521
|
this.cookie = options.cookie !== void 0 ? options.cookie : "";
|
|
1522
|
+
this.fetch = options.fetch || (typeof globalThis !== "undefined" ? globalThis.fetch : fetch);
|
|
1523
|
+
this.cache = options.cache;
|
|
903
1524
|
let resolvedApiKey = options.apiKey !== void 0 ? options.apiKey : null;
|
|
904
1525
|
let resolvedClientVersion = options.clientVersion !== void 0 ? options.clientVersion : null;
|
|
905
1526
|
let resolvedIdToken = options.idToken !== void 0 ? options.idToken : null;
|
|
@@ -915,114 +1536,550 @@ var Client = class {
|
|
|
915
1536
|
if (resolvedIdToken === null) resolvedIdToken = ytcfg.ID_TOKEN || null;
|
|
916
1537
|
}
|
|
917
1538
|
}
|
|
918
|
-
this.apiKey = resolvedApiKey;
|
|
919
|
-
this.clientVersion = resolvedClientVersion ||
|
|
920
|
-
this.idToken = resolvedIdToken;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1539
|
+
this.apiKey = resolvedApiKey;
|
|
1540
|
+
this.clientVersion = resolvedClientVersion || getFallbackClientVersion();
|
|
1541
|
+
this.idToken = resolvedIdToken;
|
|
1542
|
+
}
|
|
1543
|
+
async ensureConfig() {
|
|
1544
|
+
if (this.apiKey) return;
|
|
1545
|
+
const config = await getInnerTubeConfig(null, this.fetch);
|
|
1546
|
+
this.apiKey = config.apiKey;
|
|
1547
|
+
if (config.clientVersion) this.clientVersion = config.clientVersion;
|
|
1548
|
+
this.idToken = config.idToken;
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Standard InnerTube request using the WEB client (useful for authenticated endpoints).
|
|
1552
|
+
*/
|
|
1553
|
+
async request(endpoint, payload) {
|
|
1554
|
+
await this.ensureConfig();
|
|
1555
|
+
if (!this.apiKey) {
|
|
1556
|
+
throw new Error("[Client] InnerTube API key is not configured.");
|
|
1557
|
+
}
|
|
1558
|
+
const cleanEndpoint = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
|
|
1559
|
+
const url = `https://www.youtube.com/youtubei/v1/${cleanEndpoint}?key=${this.apiKey}&prettyPrint=false`;
|
|
1560
|
+
const headers = {
|
|
1561
|
+
"Content-Type": "application/json",
|
|
1562
|
+
"X-Youtube-Client-Name": "1",
|
|
1563
|
+
"X-Youtube-Client-Version": this.clientVersion
|
|
1564
|
+
};
|
|
1565
|
+
if (this.idToken) headers["X-Youtube-Identity-Token"] = this.idToken;
|
|
1566
|
+
const activeCookie = this.cookie || (typeof document !== "undefined" ? document.cookie : "");
|
|
1567
|
+
if (activeCookie) {
|
|
1568
|
+
headers["Cookie"] = activeCookie;
|
|
1569
|
+
const sapisid = getSapisidFromCookieString(activeCookie);
|
|
1570
|
+
if (sapisid) {
|
|
1571
|
+
const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
|
|
1572
|
+
if (authHash) headers["Authorization"] = `SAPISIDHASH ${authHash}`;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const bodyPayload = payload && typeof payload === "object" ? payload : {};
|
|
1576
|
+
const requestBody = {
|
|
1577
|
+
...bodyPayload,
|
|
1578
|
+
context: {
|
|
1579
|
+
...bodyPayload.context,
|
|
1580
|
+
client: {
|
|
1581
|
+
clientName: "WEB",
|
|
1582
|
+
clientVersion: this.clientVersion,
|
|
1583
|
+
hl: "en",
|
|
1584
|
+
gl: "US",
|
|
1585
|
+
...bodyPayload.context?.client
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
const authContext = this.idToken ? "_auth" : "_anon";
|
|
1590
|
+
const cacheKey = `yt_request_${cleanEndpoint}_${JSON.stringify(bodyPayload)}${authContext}`;
|
|
1591
|
+
if (this.cache) {
|
|
1592
|
+
const cached = await this.cache.get(cacheKey);
|
|
1593
|
+
if (cached) return cached;
|
|
1594
|
+
}
|
|
1595
|
+
const response = await this.fetch(url, {
|
|
1596
|
+
method: "POST",
|
|
1597
|
+
headers,
|
|
1598
|
+
body: JSON.stringify(requestBody),
|
|
1599
|
+
credentials: "include"
|
|
1600
|
+
});
|
|
1601
|
+
if (!response.ok) {
|
|
1602
|
+
throw new Error(`InnerTube request failed with status: ${response.status}`);
|
|
1603
|
+
}
|
|
1604
|
+
const data = await response.json();
|
|
1605
|
+
if (this.cache) {
|
|
1606
|
+
await this.cache.set(cacheKey, data);
|
|
1607
|
+
}
|
|
1608
|
+
return data;
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Specialized request for the /player endpoint that loops through client profiles
|
|
1612
|
+
* to bypass IP blocks or Captcha walls.
|
|
1613
|
+
*/
|
|
1614
|
+
async requestPlayerWithFallback(videoId) {
|
|
1615
|
+
await this.ensureConfig();
|
|
1616
|
+
const authContext = this.idToken ? "_auth" : "_anon";
|
|
1617
|
+
const cacheKey = `yt_player_fallback_${videoId}${authContext}`;
|
|
1618
|
+
if (this.cache) {
|
|
1619
|
+
const cached = await this.cache.get(cacheKey);
|
|
1620
|
+
if (cached) return cached;
|
|
1621
|
+
}
|
|
1622
|
+
const url = `https://www.youtube.com/youtubei/v1/player?key=${this.apiKey}&prettyPrint=false`;
|
|
1623
|
+
let firstPlayable = null;
|
|
1624
|
+
const failures = [];
|
|
1625
|
+
for (const profile of CLIENT_PROFILES) {
|
|
1626
|
+
try {
|
|
1627
|
+
const body = {
|
|
1628
|
+
videoId,
|
|
1629
|
+
context: {
|
|
1630
|
+
client: {
|
|
1631
|
+
clientName: profile.clientName,
|
|
1632
|
+
clientVersion: profile.clientVersion,
|
|
1633
|
+
hl: "en",
|
|
1634
|
+
gl: "US",
|
|
1635
|
+
...profile.context
|
|
1636
|
+
},
|
|
1637
|
+
user: { lockedSafetyMode: false },
|
|
1638
|
+
request: { useSsl: true }
|
|
1639
|
+
},
|
|
1640
|
+
contentCheckOk: true,
|
|
1641
|
+
racyCheckOk: true
|
|
1642
|
+
};
|
|
1643
|
+
const headers = {
|
|
1644
|
+
"Content-Type": "application/json",
|
|
1645
|
+
"User-Agent": profile.userAgent,
|
|
1646
|
+
"X-YouTube-Client-Name": profile.clientNameHeader,
|
|
1647
|
+
"X-YouTube-Client-Version": profile.clientVersion,
|
|
1648
|
+
"Origin": "https://www.youtube.com"
|
|
1649
|
+
};
|
|
1650
|
+
if (this.idToken) headers["X-Youtube-Identity-Token"] = this.idToken;
|
|
1651
|
+
const activeCookie = this.cookie || (typeof document !== "undefined" ? document.cookie : "");
|
|
1652
|
+
if (activeCookie) {
|
|
1653
|
+
headers["Cookie"] = activeCookie;
|
|
1654
|
+
const sapisid = getSapisidFromCookieString(activeCookie);
|
|
1655
|
+
if (sapisid) {
|
|
1656
|
+
const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
|
|
1657
|
+
if (authHash) headers["Authorization"] = `SAPISIDHASH ${authHash}`;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const response = await this.fetch(url, {
|
|
1661
|
+
method: "POST",
|
|
1662
|
+
headers,
|
|
1663
|
+
body: JSON.stringify(body),
|
|
1664
|
+
credentials: "include"
|
|
1665
|
+
});
|
|
1666
|
+
if (!response.ok) {
|
|
1667
|
+
failures.push(`${profile.name}: ${response.status}`);
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
const data = await response.json();
|
|
1671
|
+
const status = data.playabilityStatus?.status;
|
|
1672
|
+
if (status && status !== "OK") {
|
|
1673
|
+
const reason = data.playabilityStatus?.reason;
|
|
1674
|
+
failures.push(`${profile.name}: ${status}${reason ? ` - ${reason}` : ""}`);
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
if (!firstPlayable) firstPlayable = data;
|
|
1678
|
+
const tracks = data.captions?.playerCaptionsTracklistRenderer?.captionTracks;
|
|
1679
|
+
if (tracks && tracks.length > 0) {
|
|
1680
|
+
if (this.cache) await this.cache.set(cacheKey, data);
|
|
1681
|
+
return data;
|
|
1682
|
+
}
|
|
1683
|
+
failures.push(`${profile.name}: OK but no caption tracks`);
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
failures.push(`${profile.name}: ${err.message}`);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (firstPlayable) {
|
|
1689
|
+
if (this.cache) await this.cache.set(cacheKey, firstPlayable);
|
|
1690
|
+
return firstPlayable;
|
|
1691
|
+
}
|
|
1692
|
+
console.warn(`[Client] All mobile profiles failed, falling back to WEB profile. Attempts:
|
|
1693
|
+
${failures.join("\n")}`);
|
|
1694
|
+
return this.request("player", { videoId, contentCheckOk: true, racyCheckOk: true });
|
|
1695
|
+
}
|
|
1696
|
+
async search(query, options) {
|
|
1697
|
+
const payload = { query };
|
|
1698
|
+
if (options?.type === "video") payload.params = "EgIQAQ%3D%3D";
|
|
1699
|
+
else if (options?.type === "playlist") payload.params = "EgIQAw%3D%3D";
|
|
1700
|
+
else if (options?.type === "channel") payload.params = "EgIQAg%3D%3D";
|
|
1701
|
+
const data = await this.request("search", payload);
|
|
1702
|
+
return new SearchResult(this, data);
|
|
1703
|
+
}
|
|
1704
|
+
async findOne(query, options) {
|
|
1705
|
+
const result = await this.search(query, options);
|
|
1706
|
+
return result.items[0] || void 0;
|
|
1707
|
+
}
|
|
1708
|
+
async getVideo(videoId) {
|
|
1709
|
+
const [playerData, nextData] = await Promise.all([
|
|
1710
|
+
this.requestPlayerWithFallback(videoId),
|
|
1711
|
+
this.request("next", { videoId })
|
|
1712
|
+
]);
|
|
1713
|
+
const merged = { ...playerData, ...nextData };
|
|
1714
|
+
if (playerData.playabilityStatus?.liveStreamability) {
|
|
1715
|
+
return new LiveVideo(this, merged);
|
|
1716
|
+
}
|
|
1717
|
+
return new Video(this, merged);
|
|
1718
|
+
}
|
|
1719
|
+
async getPlaylist(playlistId) {
|
|
1720
|
+
if (playlistId.startsWith("RD")) {
|
|
1721
|
+
const data2 = await this.request("next", { playlistId });
|
|
1722
|
+
return new MixPlaylist(this, data2);
|
|
1723
|
+
}
|
|
1724
|
+
const browseId = playlistId.startsWith("VL") ? playlistId : `VL${playlistId}`;
|
|
1725
|
+
const data = await this.request("browse", { browseId });
|
|
1726
|
+
return new Playlist(this, data);
|
|
1727
|
+
}
|
|
1728
|
+
async getChannel(channelId) {
|
|
1729
|
+
const data = await this.request("browse", { browseId: channelId });
|
|
1730
|
+
if (data.error) return void 0;
|
|
1731
|
+
return new Channel(this, data);
|
|
1732
|
+
}
|
|
1733
|
+
async getVideoTranscript(videoId, languageCode) {
|
|
1734
|
+
const video = await this.getVideo(videoId);
|
|
1735
|
+
return video.captions?.get(languageCode);
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
// src/scraper.ts
|
|
1740
|
+
async function fetchYtInitialData(url) {
|
|
1741
|
+
try {
|
|
1742
|
+
console.log(`[Scraper] Fetching HTML from ${url}`);
|
|
1743
|
+
const response = await fetch(url, { credentials: "include" });
|
|
1744
|
+
const text = await response.text();
|
|
1745
|
+
const patterns = [
|
|
1746
|
+
/var ytInitialData\s*=\s*(\{.*?\});<\/script>/,
|
|
1747
|
+
/window\["ytInitialData"\]\s*=\s*(\{.*?\});/
|
|
1748
|
+
];
|
|
1749
|
+
for (const regex of patterns) {
|
|
1750
|
+
const match = text.match(regex);
|
|
1751
|
+
if (match && match[1]) {
|
|
1752
|
+
return JSON.parse(match[1]);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
console.warn(`[Scraper] Could not find ytInitialData in ${url}`);
|
|
1756
|
+
} catch (e) {
|
|
1757
|
+
console.error(`[Scraper] Failed to fetch data from ${url}`, e);
|
|
1758
|
+
}
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
function getSapisidFromCookie() {
|
|
1762
|
+
return getSapisidFromCookieString("");
|
|
1763
|
+
}
|
|
1764
|
+
function extractVideoEntries(data) {
|
|
1765
|
+
const entries = [];
|
|
1766
|
+
const seenTitles = /* @__PURE__ */ new Set();
|
|
1767
|
+
function recurse(obj) {
|
|
1768
|
+
if (!obj || typeof obj !== "object") return;
|
|
1769
|
+
if (Array.isArray(obj)) {
|
|
1770
|
+
for (let i = 0; i < obj.length; i++) {
|
|
1771
|
+
recurse(obj[i]);
|
|
1772
|
+
}
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (obj.videoId && obj.title) {
|
|
1776
|
+
let title = "";
|
|
1777
|
+
if (typeof obj.title === "string") {
|
|
1778
|
+
title = obj.title;
|
|
1779
|
+
} else if (obj.title.runs && obj.title.runs[0] && obj.title.runs[0].text) {
|
|
1780
|
+
title = obj.title.runs[0].text;
|
|
1781
|
+
} else if (obj.title.simpleText) {
|
|
1782
|
+
title = obj.title.simpleText;
|
|
1783
|
+
}
|
|
1784
|
+
title = title.trim();
|
|
1785
|
+
if (title && title.length > 2 && title !== "Skip navigation") {
|
|
1786
|
+
if (!seenTitles.has(title)) {
|
|
1787
|
+
seenTitles.add(title);
|
|
1788
|
+
let channel = "";
|
|
1789
|
+
const byline = obj.longBylineText || obj.shortBylineText || obj.ownerText;
|
|
1790
|
+
if (byline) {
|
|
1791
|
+
if (typeof byline === "string") {
|
|
1792
|
+
channel = byline;
|
|
1793
|
+
} else if (byline.runs && byline.runs[0] && byline.runs[0].text) {
|
|
1794
|
+
channel = byline.runs[0].text;
|
|
1795
|
+
} else if (byline.simpleText) {
|
|
1796
|
+
channel = byline.simpleText;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
entries.push({
|
|
1800
|
+
title,
|
|
1801
|
+
channel: channel.split("\n")[0].replace(/•/g, "").trim()
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (obj.lockupViewModel && obj.lockupViewModel.contentId && obj.lockupViewModel.contentType === "LOCKUP_CONTENT_TYPE_VIDEO") {
|
|
1808
|
+
const model = obj.lockupViewModel;
|
|
1809
|
+
const meta = model.metadata?.lockupMetadataViewModel;
|
|
1810
|
+
if (meta && meta.title && meta.title.content) {
|
|
1811
|
+
const title = meta.title.content.trim();
|
|
1812
|
+
if (title && !seenTitles.has(title)) {
|
|
1813
|
+
seenTitles.add(title);
|
|
1814
|
+
let channel = "";
|
|
1815
|
+
const rows = meta.metadata?.contentMetadataViewModel?.metadataRows;
|
|
1816
|
+
if (rows && rows[0] && rows[0].metadataParts && rows[0].metadataParts[0] && rows[0].metadataParts[0].text) {
|
|
1817
|
+
channel = rows[0].metadataParts[0].text.content || "";
|
|
1818
|
+
}
|
|
1819
|
+
entries.push({
|
|
1820
|
+
title,
|
|
1821
|
+
channel: channel.split("\n")[0].replace(/•/g, "").trim()
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
for (const key in obj) {
|
|
1828
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
1829
|
+
recurse(obj[key]);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
try {
|
|
1834
|
+
recurse(data);
|
|
1835
|
+
} catch (e) {
|
|
1836
|
+
console.warn("[Scraper] Error extracting video entries recursively", e);
|
|
1837
|
+
}
|
|
1838
|
+
return entries;
|
|
1839
|
+
}
|
|
1840
|
+
function findContinuationToken(obj) {
|
|
1841
|
+
if (!obj || typeof obj !== "object") return null;
|
|
1842
|
+
if (obj.continuationCommand && obj.continuationCommand.token) {
|
|
1843
|
+
return obj.continuationCommand.token;
|
|
1844
|
+
}
|
|
1845
|
+
for (const key in obj) {
|
|
1846
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
1847
|
+
const token = findContinuationToken(obj[key]);
|
|
1848
|
+
if (token) return token;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
async function fetchInnerTubeContinuation(apiKey, clientVersion, idToken, initialToken, limit) {
|
|
1854
|
+
const entries = [];
|
|
1855
|
+
let continuationToken = initialToken;
|
|
1856
|
+
try {
|
|
1857
|
+
while (continuationToken && entries.length < limit) {
|
|
1858
|
+
const headers = {
|
|
1859
|
+
"Content-Type": "application/json",
|
|
1860
|
+
"X-Youtube-Client-Name": "1",
|
|
1861
|
+
"X-Youtube-Client-Version": clientVersion
|
|
1862
|
+
};
|
|
1863
|
+
if (idToken) {
|
|
1864
|
+
headers["X-Youtube-Identity-Token"] = idToken;
|
|
1865
|
+
}
|
|
1866
|
+
const sapisid = getSapisidFromCookie();
|
|
1867
|
+
if (sapisid) {
|
|
1868
|
+
const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
|
|
1869
|
+
if (authHash) {
|
|
1870
|
+
headers["Authorization"] = `SAPISIDHASH ${authHash}`;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
const response = await fetch(`https://www.youtube.com/youtubei/v1/browse?key=${apiKey}&prettyPrint=false`, {
|
|
1874
|
+
method: "POST",
|
|
1875
|
+
headers,
|
|
1876
|
+
credentials: "include",
|
|
1877
|
+
body: JSON.stringify({
|
|
1878
|
+
context: {
|
|
1879
|
+
client: {
|
|
1880
|
+
clientName: "WEB",
|
|
1881
|
+
clientVersion,
|
|
1882
|
+
hl: "en",
|
|
1883
|
+
gl: "US"
|
|
1884
|
+
}
|
|
1885
|
+
},
|
|
1886
|
+
continuation: continuationToken
|
|
1887
|
+
})
|
|
1888
|
+
});
|
|
1889
|
+
if (!response.ok) break;
|
|
1890
|
+
const data = await response.json();
|
|
1891
|
+
const pageEntries = extractVideoEntries(data);
|
|
1892
|
+
entries.push(...pageEntries);
|
|
1893
|
+
continuationToken = findContinuationToken(data);
|
|
1894
|
+
}
|
|
1895
|
+
} catch (e) {
|
|
1896
|
+
console.error("[Scraper] Error in InnerTube pagination", e);
|
|
936
1897
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (!this.apiKey) {
|
|
943
|
-
throw new Error("[Client] InnerTube API key is not configured.");
|
|
944
|
-
}
|
|
945
|
-
const cleanEndpoint = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
|
|
946
|
-
const url = `https://www.youtube.com/youtubei/v1/${cleanEndpoint}?key=${this.apiKey}&prettyPrint=false`;
|
|
1898
|
+
return entries;
|
|
1899
|
+
}
|
|
1900
|
+
async function fetchInnerTubeFeed(apiKey, clientVersion, idToken, browseId, limit = 500) {
|
|
1901
|
+
const entries = [];
|
|
1902
|
+
try {
|
|
947
1903
|
const headers = {
|
|
948
1904
|
"Content-Type": "application/json",
|
|
949
1905
|
"X-Youtube-Client-Name": "1",
|
|
950
|
-
"X-Youtube-Client-Version":
|
|
1906
|
+
"X-Youtube-Client-Version": clientVersion
|
|
951
1907
|
};
|
|
952
|
-
if (
|
|
953
|
-
headers["X-Youtube-Identity-Token"] =
|
|
1908
|
+
if (idToken) {
|
|
1909
|
+
headers["X-Youtube-Identity-Token"] = idToken;
|
|
954
1910
|
}
|
|
955
|
-
const
|
|
956
|
-
if (
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
const authHash = await getSApiSidHash2(sapisid, "https://www.youtube.com");
|
|
961
|
-
if (authHash) {
|
|
962
|
-
headers["Authorization"] = `SAPISIDHASH ${authHash}`;
|
|
963
|
-
}
|
|
1911
|
+
const sapisid = getSapisidFromCookie();
|
|
1912
|
+
if (sapisid) {
|
|
1913
|
+
const authHash = await getSApiSidHash(sapisid, "https://www.youtube.com");
|
|
1914
|
+
if (authHash) {
|
|
1915
|
+
headers["Authorization"] = `SAPISIDHASH ${authHash}`;
|
|
964
1916
|
}
|
|
965
1917
|
}
|
|
966
|
-
const
|
|
967
|
-
const requestBody = {
|
|
968
|
-
...bodyPayload,
|
|
969
|
-
context: {
|
|
970
|
-
...bodyPayload.context,
|
|
971
|
-
client: {
|
|
972
|
-
clientName: "WEB",
|
|
973
|
-
clientVersion: this.clientVersion,
|
|
974
|
-
hl: "en",
|
|
975
|
-
gl: "US",
|
|
976
|
-
...bodyPayload.context?.client
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
};
|
|
980
|
-
const response = await fetch(url, {
|
|
1918
|
+
const response = await fetch(`https://www.youtube.com/youtubei/v1/browse?key=${apiKey}&prettyPrint=false`, {
|
|
981
1919
|
method: "POST",
|
|
982
1920
|
headers,
|
|
983
|
-
|
|
984
|
-
|
|
1921
|
+
credentials: "include",
|
|
1922
|
+
body: JSON.stringify({
|
|
1923
|
+
context: {
|
|
1924
|
+
client: {
|
|
1925
|
+
clientName: "WEB",
|
|
1926
|
+
clientVersion,
|
|
1927
|
+
hl: "en",
|
|
1928
|
+
gl: "US"
|
|
1929
|
+
}
|
|
1930
|
+
},
|
|
1931
|
+
browseId
|
|
1932
|
+
})
|
|
985
1933
|
});
|
|
986
|
-
if (!response.ok)
|
|
987
|
-
|
|
1934
|
+
if (!response.ok) return [];
|
|
1935
|
+
const data = await response.json();
|
|
1936
|
+
const pageEntries = extractVideoEntries(data);
|
|
1937
|
+
entries.push(...pageEntries);
|
|
1938
|
+
if (entries.length < limit) {
|
|
1939
|
+
const continuationToken = findContinuationToken(data);
|
|
1940
|
+
if (continuationToken) {
|
|
1941
|
+
const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, continuationToken, limit - entries.length);
|
|
1942
|
+
entries.push(...more);
|
|
1943
|
+
}
|
|
988
1944
|
}
|
|
989
|
-
|
|
1945
|
+
} catch (e) {
|
|
1946
|
+
console.error(`[Scraper] InnerTube fetch error for ${browseId}`, e);
|
|
990
1947
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1948
|
+
return entries;
|
|
1949
|
+
}
|
|
1950
|
+
async function scrapeTasteData(injectedConfig, customPlaylists = [], limit = 500) {
|
|
1951
|
+
let historyEntries = [];
|
|
1952
|
+
let likesEntries = [];
|
|
1953
|
+
let wlEntries = [];
|
|
1954
|
+
const dislikesEntries = [];
|
|
1955
|
+
const config = await getInnerTubeConfig(injectedConfig);
|
|
1956
|
+
const apiKey = config.apiKey;
|
|
1957
|
+
const clientVersion = config.clientVersion;
|
|
1958
|
+
const idToken = config.idToken;
|
|
1959
|
+
if (apiKey) {
|
|
1960
|
+
historyEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "FEhistory", limit);
|
|
1961
|
+
likesEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "VLLL", limit);
|
|
1962
|
+
wlEntries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, "VLWL", limit);
|
|
1963
|
+
}
|
|
1964
|
+
if (historyEntries.length === 0) {
|
|
1965
|
+
const historyData = await fetchYtInitialData("https://www.youtube.com/feed/history");
|
|
1966
|
+
if (historyData) {
|
|
1967
|
+
historyEntries = extractVideoEntries(historyData);
|
|
1968
|
+
if (historyEntries.length < limit) {
|
|
1969
|
+
const token = findContinuationToken(historyData);
|
|
1970
|
+
if (token && apiKey) {
|
|
1971
|
+
const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - historyEntries.length);
|
|
1972
|
+
historyEntries.push(...more);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1002
1975
|
}
|
|
1003
|
-
const data = await this.request("search", payload);
|
|
1004
|
-
return new SearchResult(this, data);
|
|
1005
1976
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1977
|
+
if (likesEntries.length === 0) {
|
|
1978
|
+
const likesData = await fetchYtInitialData("https://www.youtube.com/playlist?list=LL");
|
|
1979
|
+
if (likesData) {
|
|
1980
|
+
likesEntries = extractVideoEntries(likesData);
|
|
1981
|
+
if (likesEntries.length < limit) {
|
|
1982
|
+
const token = findContinuationToken(likesData);
|
|
1983
|
+
if (token && apiKey) {
|
|
1984
|
+
const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - likesEntries.length);
|
|
1985
|
+
likesEntries.push(...more);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1016
1989
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1990
|
+
if (wlEntries.length === 0) {
|
|
1991
|
+
const wlData = await fetchYtInitialData("https://www.youtube.com/playlist?list=WL");
|
|
1992
|
+
if (wlData) {
|
|
1993
|
+
wlEntries = extractVideoEntries(wlData);
|
|
1994
|
+
if (wlEntries.length < limit) {
|
|
1995
|
+
const token = findContinuationToken(wlData);
|
|
1996
|
+
if (token && apiKey) {
|
|
1997
|
+
const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - wlEntries.length);
|
|
1998
|
+
wlEntries.push(...more);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
1024
2002
|
}
|
|
1025
|
-
|
|
2003
|
+
const customPlaylistsData = [];
|
|
2004
|
+
for (const pl of customPlaylists) {
|
|
2005
|
+
if (!pl.url) continue;
|
|
2006
|
+
const match = pl.url.match(/[&?]list=([a-zA-Z0-9_-]+)/);
|
|
2007
|
+
const playlistId = match ? match[1] : pl.url.trim();
|
|
2008
|
+
if (!playlistId || !/^[a-zA-Z0-9_-]+$/.test(playlistId)) continue;
|
|
2009
|
+
const browseId = playlistId.startsWith("VL") ? playlistId : "VL" + playlistId;
|
|
2010
|
+
let entries = [];
|
|
2011
|
+
if (apiKey) {
|
|
2012
|
+
entries = await fetchInnerTubeFeed(apiKey, clientVersion, idToken, browseId, limit);
|
|
2013
|
+
}
|
|
2014
|
+
if (entries.length === 0) {
|
|
2015
|
+
const data = await fetchYtInitialData(`https://www.youtube.com/playlist?list=${playlistId}`);
|
|
2016
|
+
if (data) {
|
|
2017
|
+
entries = extractVideoEntries(data);
|
|
2018
|
+
if (entries.length < limit) {
|
|
2019
|
+
const token = findContinuationToken(data);
|
|
2020
|
+
if (token && apiKey) {
|
|
2021
|
+
const more = await fetchInnerTubeContinuation(apiKey, clientVersion, idToken, token, limit - entries.length);
|
|
2022
|
+
entries.push(...more);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
customPlaylistsData.push({
|
|
2028
|
+
id: playlistId,
|
|
2029
|
+
entries: entries.slice(0, limit)
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
return {
|
|
2033
|
+
historyEntries: historyEntries.slice(0, limit),
|
|
2034
|
+
likesEntries: likesEntries.slice(0, limit),
|
|
2035
|
+
wlEntries: wlEntries.slice(0, limit),
|
|
2036
|
+
dislikesEntries: dislikesEntries.slice(0, limit),
|
|
2037
|
+
customPlaylistsData
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// src/formatters.ts
|
|
2042
|
+
function formatSrtTimestamp(seconds) {
|
|
2043
|
+
const totalMs = Math.round(seconds * 1e3);
|
|
2044
|
+
const ms = totalMs % 1e3;
|
|
2045
|
+
const totalSeconds = Math.floor(totalMs / 1e3);
|
|
2046
|
+
const s = totalSeconds % 60;
|
|
2047
|
+
const m = Math.floor(totalSeconds / 60) % 60;
|
|
2048
|
+
const h = Math.floor(totalSeconds / 3600);
|
|
2049
|
+
return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0") + "," + String(ms).padStart(3, "0");
|
|
2050
|
+
}
|
|
2051
|
+
function formatVttTimestamp(seconds) {
|
|
2052
|
+
const totalMs = Math.round(seconds * 1e3);
|
|
2053
|
+
const ms = totalMs % 1e3;
|
|
2054
|
+
const totalSeconds = Math.floor(totalMs / 1e3);
|
|
2055
|
+
const s = totalSeconds % 60;
|
|
2056
|
+
const m = Math.floor(totalSeconds / 60) % 60;
|
|
2057
|
+
const h = Math.floor(totalSeconds / 3600);
|
|
2058
|
+
return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0") + "." + String(ms).padStart(3, "0");
|
|
2059
|
+
}
|
|
2060
|
+
function toSRT(segments) {
|
|
2061
|
+
return segments.map((segment, index) => {
|
|
2062
|
+
const start = formatSrtTimestamp(segment.start);
|
|
2063
|
+
const end = formatSrtTimestamp(segment.start + segment.duration);
|
|
2064
|
+
return `${index + 1}
|
|
2065
|
+
${start} --> ${end}
|
|
2066
|
+
${segment.text}`;
|
|
2067
|
+
}).join("\n\n");
|
|
2068
|
+
}
|
|
2069
|
+
function toVTT(segments) {
|
|
2070
|
+
const cues = segments.map((segment) => {
|
|
2071
|
+
const start = formatVttTimestamp(segment.start);
|
|
2072
|
+
const end = formatVttTimestamp(segment.start + segment.duration);
|
|
2073
|
+
return `${start} --> ${end}
|
|
2074
|
+
${segment.text}`;
|
|
2075
|
+
}).join("\n\n");
|
|
2076
|
+
return `WEBVTT
|
|
2077
|
+
|
|
2078
|
+
${cues}`;
|
|
2079
|
+
}
|
|
2080
|
+
function toPlainText(segments, separator = "\n") {
|
|
2081
|
+
return segments.map((segment) => segment.text).join(separator);
|
|
2082
|
+
}
|
|
1026
2083
|
|
|
1027
2084
|
// src/comments.ts
|
|
1028
2085
|
function findValue(obj, path, defaultValue = void 0) {
|
|
@@ -1146,7 +2203,7 @@ async function fetchCommentsFromYouTube(videoId, count = 50, injectedConfig = nu
|
|
|
1146
2203
|
console.log(`[Comments] Fetching comment batch, total comments so far: ${fetchedCount}`);
|
|
1147
2204
|
const options = createCommentsApiRequestOptions(continuationToken, clientVersion);
|
|
1148
2205
|
const headers = { ...options.headers };
|
|
1149
|
-
const sapisid =
|
|
2206
|
+
const sapisid = getSapisidFromCookieString();
|
|
1150
2207
|
if (sapisid) {
|
|
1151
2208
|
const authHash = await getSApiSidHash(sapisid);
|
|
1152
2209
|
if (authHash) {
|
|
@@ -1285,6 +2342,15 @@ function extractPlayerResponse(html) {
|
|
|
1285
2342
|
}
|
|
1286
2343
|
return null;
|
|
1287
2344
|
}
|
|
2345
|
+
function decodeHtmlEntities(text) {
|
|
2346
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
|
|
2347
|
+
const code = parseInt(hex, 16);
|
|
2348
|
+
return code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : "";
|
|
2349
|
+
}).replace(/&#(\d+);/g, (_, dec) => {
|
|
2350
|
+
const code = parseInt(dec, 10);
|
|
2351
|
+
return code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : "";
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
1288
2354
|
function parseXmlTranscriptRegex(xmlText) {
|
|
1289
2355
|
const segments = [];
|
|
1290
2356
|
const regex = /<text start="([^"]+)" dur="([^"]+)"[^>]*>(.*?)<\/text>/g;
|
|
@@ -1292,26 +2358,45 @@ function parseXmlTranscriptRegex(xmlText) {
|
|
|
1292
2358
|
while ((match = regex.exec(xmlText)) !== null) {
|
|
1293
2359
|
const start = parseFloat(match[1]);
|
|
1294
2360
|
const duration = parseFloat(match[2]);
|
|
1295
|
-
|
|
2361
|
+
let text = decodeHtmlEntities(match[3]).replace(/\n/g, " ").trim();
|
|
1296
2362
|
if (text && !isNaN(start)) {
|
|
1297
2363
|
segments.push({ start, duration, text });
|
|
1298
2364
|
}
|
|
1299
2365
|
}
|
|
1300
2366
|
return segments;
|
|
1301
2367
|
}
|
|
1302
|
-
async function fetchSubtitlesFromYouTube(videoId, language = "en") {
|
|
2368
|
+
async function fetchSubtitlesFromYouTube(videoId, language = "en", clientOrOptions) {
|
|
1303
2369
|
try {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
2370
|
+
let client;
|
|
2371
|
+
if (clientOrOptions instanceof Client) {
|
|
2372
|
+
client = clientOrOptions;
|
|
2373
|
+
} else {
|
|
2374
|
+
client = new Client(clientOrOptions);
|
|
2375
|
+
}
|
|
2376
|
+
const cacheKey = `yt_subtitles_${videoId}_${language}`;
|
|
2377
|
+
if (client.cache) {
|
|
2378
|
+
const cached = await client.cache.get(cacheKey);
|
|
2379
|
+
if (cached) return cached;
|
|
2380
|
+
}
|
|
2381
|
+
console.log(`[Subtitles] Fetching player data for video: ${videoId}`);
|
|
2382
|
+
let playerResponse = null;
|
|
2383
|
+
try {
|
|
2384
|
+
playerResponse = await client.requestPlayerWithFallback(videoId);
|
|
2385
|
+
} catch (apiError) {
|
|
2386
|
+
console.warn(`[Subtitles] InnerTube API failed for ${videoId}. Falling back to HTML scraping.`, apiError);
|
|
2387
|
+
}
|
|
2388
|
+
if (!playerResponse || !playerResponse.captions || !playerResponse.captions.playerCaptionsTracklistRenderer) {
|
|
2389
|
+
console.log(`[Subtitles] No captions in API response, trying HTML scraping fallback for ${videoId}`);
|
|
2390
|
+
const videoPageUrl = `https://www.youtube.com/watch?v=${videoId}&bpctr=9999999999`;
|
|
2391
|
+
const response = await client.fetch(videoPageUrl, {
|
|
2392
|
+
headers: {
|
|
2393
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
|
2394
|
+
"Accept-Language": "en-US"
|
|
2395
|
+
}
|
|
2396
|
+
});
|
|
2397
|
+
const pageHtml = await response.text();
|
|
2398
|
+
playerResponse = extractPlayerResponse(pageHtml);
|
|
2399
|
+
}
|
|
1315
2400
|
if (!playerResponse || !playerResponse.captions || !playerResponse.captions.playerCaptionsTracklistRenderer) {
|
|
1316
2401
|
console.warn(`[Subtitles] No captions tracklist found for video: ${videoId}`);
|
|
1317
2402
|
return [];
|
|
@@ -1323,6 +2408,7 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
|
|
|
1323
2408
|
}
|
|
1324
2409
|
const tracks = captionTracks.map((t) => ({
|
|
1325
2410
|
baseUrl: t.baseUrl,
|
|
2411
|
+
vssId: t.vssId,
|
|
1326
2412
|
languageCode: t.languageCode,
|
|
1327
2413
|
name: t.name?.simpleText || t.name,
|
|
1328
2414
|
isTranslatable: t.kind === "asr"
|
|
@@ -1330,9 +2416,7 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
|
|
|
1330
2416
|
let selectedTrack;
|
|
1331
2417
|
if (language && language !== "auto") {
|
|
1332
2418
|
const lowerLang = language.toLowerCase();
|
|
1333
|
-
selectedTrack = tracks.find((t) => t.languageCode.toLowerCase() === lowerLang && !t.isTranslatable);
|
|
1334
|
-
if (!selectedTrack) selectedTrack = tracks.find((t) => t.languageCode.toLowerCase() === lowerLang);
|
|
1335
|
-
if (!selectedTrack) selectedTrack = tracks.find((t) => t.languageCode.toLowerCase().includes(lowerLang));
|
|
2419
|
+
selectedTrack = tracks.find((t) => t.vssId === `.${lowerLang}`) || tracks.find((t) => t.vssId === `a.${lowerLang}`) || tracks.find((t) => t.languageCode.toLowerCase() === lowerLang && !t.isTranslatable) || tracks.find((t) => t.languageCode.toLowerCase() === lowerLang) || tracks.find((t) => t.languageCode.toLowerCase().includes(lowerLang));
|
|
1336
2420
|
}
|
|
1337
2421
|
if (!selectedTrack) {
|
|
1338
2422
|
selectedTrack = tracks.find((t) => t.languageCode.toLowerCase().startsWith("en") && !t.isTranslatable);
|
|
@@ -1346,9 +2430,10 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
|
|
|
1346
2430
|
return [];
|
|
1347
2431
|
}
|
|
1348
2432
|
console.log(`[Subtitles] Selected track: ${selectedTrack.languageCode} (${selectedTrack.isTranslatable ? "ASR/Auto" : "Manual"})`);
|
|
1349
|
-
|
|
2433
|
+
let urlWithFormat = selectedTrack.baseUrl.replace(/&fmt=[^&]+/, "");
|
|
2434
|
+
urlWithFormat += "&fmt=json3";
|
|
1350
2435
|
console.log(`[Subtitles] Fetching transcript from: ${urlWithFormat.substring(0, 100)}...`);
|
|
1351
|
-
const transcriptResponse = await fetch(urlWithFormat, {
|
|
2436
|
+
const transcriptResponse = await client.fetch(urlWithFormat, {
|
|
1352
2437
|
headers: {
|
|
1353
2438
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
|
1354
2439
|
"Referer": "https://www.youtube.com/",
|
|
@@ -1357,21 +2442,31 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
|
|
|
1357
2442
|
});
|
|
1358
2443
|
const transcriptData = await transcriptResponse.text();
|
|
1359
2444
|
let transcripts = [];
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
console.warn("[Subtitles] Failed to parse transcript string as JSON/XML.");
|
|
2445
|
+
try {
|
|
2446
|
+
const json = JSON.parse(transcriptData);
|
|
2447
|
+
if (json.events) {
|
|
2448
|
+
transcripts = json.events.map((e) => {
|
|
2449
|
+
if (!e) return null;
|
|
2450
|
+
const rawText = e.segs ? e.segs.map((s) => s?.utf8 || "").join("") : "";
|
|
2451
|
+
const cleanText = decodeHtmlEntities(rawText.replace(/<[^>]+>/g, "")).replace(/\n/g, " ").trim();
|
|
2452
|
+
return {
|
|
2453
|
+
start: (e.tStartMs || 0) / 1e3,
|
|
2454
|
+
duration: (e.dDurationMs || 0) / 1e3,
|
|
2455
|
+
text: cleanText
|
|
2456
|
+
};
|
|
2457
|
+
}).filter((s) => s && s.text);
|
|
1374
2458
|
}
|
|
2459
|
+
} catch (jsonError) {
|
|
2460
|
+
console.warn("[Subtitles] Failed to parse transcript string as JSON3. Falling back to XML regex.", jsonError);
|
|
2461
|
+
const xmlUrl = selectedTrack.baseUrl.replace(/&fmt=[^&]+/, "") + "&fmt=srv3";
|
|
2462
|
+
const xmlResponse = await client.fetch(xmlUrl, {
|
|
2463
|
+
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" }
|
|
2464
|
+
});
|
|
2465
|
+
const xmlData = await xmlResponse.text();
|
|
2466
|
+
transcripts = parseXmlTranscriptRegex(xmlData);
|
|
2467
|
+
}
|
|
2468
|
+
if (client.cache && transcripts.length > 0) {
|
|
2469
|
+
await client.cache.set(cacheKey, transcripts);
|
|
1375
2470
|
}
|
|
1376
2471
|
console.log(`[Subtitles] Retrieved ${transcripts.length} segments.`);
|
|
1377
2472
|
return transcripts;
|
|
@@ -1380,20 +2475,47 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
|
|
|
1380
2475
|
return [];
|
|
1381
2476
|
}
|
|
1382
2477
|
}
|
|
2478
|
+
|
|
2479
|
+
// src/index.ts
|
|
2480
|
+
async function searchYouTube(query, options) {
|
|
2481
|
+
const client = new Client(options);
|
|
2482
|
+
return client.search(query, options);
|
|
2483
|
+
}
|
|
2484
|
+
async function getVideoPlayback(videoId, options) {
|
|
2485
|
+
const client = new Client(options);
|
|
2486
|
+
return client.getVideo(videoId);
|
|
2487
|
+
}
|
|
1383
2488
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1384
2489
|
0 && (module.exports = {
|
|
1385
2490
|
Base,
|
|
2491
|
+
BaseChannel,
|
|
1386
2492
|
BaseVideo,
|
|
2493
|
+
Caption,
|
|
2494
|
+
CaptionLanguage,
|
|
2495
|
+
Channel,
|
|
1387
2496
|
ChannelCompact,
|
|
2497
|
+
ChannelLive,
|
|
2498
|
+
ChannelPlaylists,
|
|
2499
|
+
ChannelPosts,
|
|
2500
|
+
ChannelShorts,
|
|
2501
|
+
ChannelVideos,
|
|
2502
|
+
Chat,
|
|
1388
2503
|
Client,
|
|
2504
|
+
Comment,
|
|
2505
|
+
CommentReplies,
|
|
1389
2506
|
Continuable,
|
|
2507
|
+
LiveVideo,
|
|
2508
|
+
MixPlaylist,
|
|
1390
2509
|
Playlist,
|
|
1391
2510
|
PlaylistCompact,
|
|
1392
2511
|
PlaylistVideos,
|
|
1393
2512
|
SearchResult,
|
|
1394
2513
|
Thumbnails,
|
|
1395
2514
|
Video,
|
|
2515
|
+
VideoCaptions,
|
|
2516
|
+
VideoComments,
|
|
1396
2517
|
VideoCompact,
|
|
2518
|
+
VideoRelated,
|
|
1397
2519
|
createCommentsApiRequestOptions,
|
|
1398
2520
|
extractPlayerResponse,
|
|
1399
2521
|
extractVideoEntries,
|
|
@@ -1404,7 +2526,12 @@ async function fetchSubtitlesFromYouTube(videoId, language = "en") {
|
|
|
1404
2526
|
findContinuationToken,
|
|
1405
2527
|
getInnerTubeConfig,
|
|
1406
2528
|
getSApiSidHash,
|
|
1407
|
-
|
|
2529
|
+
getSapisidFromCookieString,
|
|
2530
|
+
getVideoPlayback,
|
|
1408
2531
|
parseXmlTranscriptRegex,
|
|
1409
|
-
scrapeTasteData
|
|
2532
|
+
scrapeTasteData,
|
|
2533
|
+
searchYouTube,
|
|
2534
|
+
toPlainText,
|
|
2535
|
+
toSRT,
|
|
2536
|
+
toVTT
|
|
1410
2537
|
});
|