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