@aitofy/youtube 0.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 +28 -0
- package/README.md +278 -0
- package/dist/chunk-A6HJFYPT.mjs +847 -0
- package/dist/index.d.mts +241 -0
- package/dist/index.d.ts +241 -0
- package/dist/index.js +884 -0
- package/dist/index.mjs +28 -0
- package/dist/mcp.d.mts +1 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +1008 -0
- package/dist/mcp.mjs +251 -0
- package/package.json +75 -0
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
// src/channel/get-videos.ts
|
|
2
|
+
import { XMLParser } from "fast-xml-parser";
|
|
3
|
+
|
|
4
|
+
// src/types.ts
|
|
5
|
+
var YouTubeToolsError = class extends Error {
|
|
6
|
+
constructor(message, code, statusCode) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.name = "YouTubeToolsError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var ErrorCodes = {
|
|
14
|
+
CHANNEL_NOT_FOUND: "CHANNEL_NOT_FOUND",
|
|
15
|
+
VIDEO_NOT_FOUND: "VIDEO_NOT_FOUND",
|
|
16
|
+
TRANSCRIPT_NOT_AVAILABLE: "TRANSCRIPT_NOT_AVAILABLE",
|
|
17
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
18
|
+
PARSING_ERROR: "PARSING_ERROR",
|
|
19
|
+
NETWORK_ERROR: "NETWORK_ERROR"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/utils/fetcher.ts
|
|
23
|
+
var CACHE_TTL = 5 * 60 * 1e3;
|
|
24
|
+
function parseChannelId(input) {
|
|
25
|
+
if (/^UC[a-zA-Z0-9_-]{22}$/.test(input)) {
|
|
26
|
+
return { type: "id", value: input };
|
|
27
|
+
}
|
|
28
|
+
if (input.startsWith("@")) {
|
|
29
|
+
return { type: "handle", value: input };
|
|
30
|
+
}
|
|
31
|
+
const patterns = [
|
|
32
|
+
{ regex: /youtube\.com\/channel\/(UC[a-zA-Z0-9_-]{22})/, type: "id" },
|
|
33
|
+
{ regex: /youtube\.com\/@([a-zA-Z0-9_-]+)/, type: "handle" },
|
|
34
|
+
{ regex: /youtube\.com\/c\/([a-zA-Z0-9_-]+)/, type: "custom" },
|
|
35
|
+
{ regex: /youtube\.com\/user\/([a-zA-Z0-9_-]+)/, type: "custom" }
|
|
36
|
+
];
|
|
37
|
+
for (const { regex, type } of patterns) {
|
|
38
|
+
const match = input.match(regex);
|
|
39
|
+
if (match) {
|
|
40
|
+
return { type, value: type === "handle" ? `@${match[1]}` : match[1] };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { type: "custom", value: input };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/channel/get-videos.ts
|
|
47
|
+
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
48
|
+
async function getChannelVideos(options) {
|
|
49
|
+
const opts = typeof options === "string" ? { channel: options } : options;
|
|
50
|
+
const { channel, limit = 15, sortBy = "newest", contentType = "videos" } = opts;
|
|
51
|
+
const channelInfo = parseChannelId(channel);
|
|
52
|
+
if (limit <= 15 && sortBy === "newest" && contentType === "videos") {
|
|
53
|
+
try {
|
|
54
|
+
const channelId = await resolveChannelId(channelInfo);
|
|
55
|
+
return getVideosFromRSS(channelId, limit);
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return getVideosFromScraping(channelInfo, limit, sortBy, contentType);
|
|
60
|
+
}
|
|
61
|
+
async function getVideosFromRSS(channelId, limit) {
|
|
62
|
+
const url = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
|
|
63
|
+
const response = await fetch(url, {
|
|
64
|
+
headers: { "User-Agent": USER_AGENT }
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new YouTubeToolsError(
|
|
68
|
+
`Failed to fetch RSS: ${response.status}`,
|
|
69
|
+
ErrorCodes.NETWORK_ERROR,
|
|
70
|
+
response.status
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const xml = await response.text();
|
|
74
|
+
const parser = new XMLParser({
|
|
75
|
+
ignoreAttributes: false,
|
|
76
|
+
attributeNamePrefix: "@_"
|
|
77
|
+
});
|
|
78
|
+
const data = parser.parse(xml);
|
|
79
|
+
const entries = data?.feed?.entry || [];
|
|
80
|
+
const videos = [];
|
|
81
|
+
for (const entry of entries.slice(0, limit)) {
|
|
82
|
+
videos.push({
|
|
83
|
+
videoId: entry["yt:videoId"],
|
|
84
|
+
title: entry.title,
|
|
85
|
+
description: entry["media:group"]?.["media:description"] || "",
|
|
86
|
+
publishedAt: entry.published,
|
|
87
|
+
url: entry.link?.["@_href"] || `https://youtube.com/watch?v=${entry["yt:videoId"]}`,
|
|
88
|
+
thumbnails: {
|
|
89
|
+
default: entry["media:group"]?.["media:thumbnail"]?.["@_url"],
|
|
90
|
+
medium: entry["media:group"]?.["media:thumbnail"]?.["@_url"]?.replace(
|
|
91
|
+
"default",
|
|
92
|
+
"mqdefault"
|
|
93
|
+
),
|
|
94
|
+
high: entry["media:group"]?.["media:thumbnail"]?.["@_url"]?.replace(
|
|
95
|
+
"default",
|
|
96
|
+
"hqdefault"
|
|
97
|
+
)
|
|
98
|
+
},
|
|
99
|
+
channelId: entry["yt:channelId"],
|
|
100
|
+
channelTitle: entry.author?.name
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return videos;
|
|
104
|
+
}
|
|
105
|
+
async function getVideosFromScraping(channelInfo, limit, sortBy, contentType) {
|
|
106
|
+
let baseUrl;
|
|
107
|
+
if (channelInfo.type === "id") {
|
|
108
|
+
baseUrl = `https://www.youtube.com/channel/${channelInfo.value}`;
|
|
109
|
+
} else if (channelInfo.type === "handle") {
|
|
110
|
+
baseUrl = `https://www.youtube.com/${channelInfo.value}`;
|
|
111
|
+
} else {
|
|
112
|
+
baseUrl = `https://www.youtube.com/c/${channelInfo.value}`;
|
|
113
|
+
}
|
|
114
|
+
const contentPath = contentType === "shorts" ? "/shorts" : contentType === "streams" ? "/streams" : "/videos";
|
|
115
|
+
const url = baseUrl + contentPath;
|
|
116
|
+
const sortParam = sortBy === "popular" ? "?sort=p" : sortBy === "oldest" ? "?sort=da" : "";
|
|
117
|
+
const fullUrl = url + sortParam;
|
|
118
|
+
const response = await fetch(fullUrl, {
|
|
119
|
+
headers: {
|
|
120
|
+
"User-Agent": USER_AGENT,
|
|
121
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new YouTubeToolsError(
|
|
126
|
+
`Failed to fetch channel page: ${response.status}`,
|
|
127
|
+
ErrorCodes.NETWORK_ERROR,
|
|
128
|
+
response.status
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const html = await response.text();
|
|
132
|
+
return extractVideosFromHTML(html, limit);
|
|
133
|
+
}
|
|
134
|
+
function extractVideosFromHTML(html, limit) {
|
|
135
|
+
const dataMatch = html.match(/var ytInitialData = (.+?);<\/script>/s);
|
|
136
|
+
if (!dataMatch) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const data = JSON.parse(dataMatch[1]);
|
|
141
|
+
const videos = [];
|
|
142
|
+
const tabs = data?.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
|
|
143
|
+
for (const tab of tabs) {
|
|
144
|
+
const tabContent = tab?.tabRenderer?.content;
|
|
145
|
+
if (!tabContent) continue;
|
|
146
|
+
const items = tabContent?.richGridRenderer?.contents || tabContent?.sectionListRenderer?.contents?.[0]?.itemSectionRenderer?.contents || [];
|
|
147
|
+
for (const item of items) {
|
|
148
|
+
if (videos.length >= limit) break;
|
|
149
|
+
const videoRenderer = item?.richItemRenderer?.content?.videoRenderer || item?.gridVideoRenderer || item?.videoRenderer;
|
|
150
|
+
if (videoRenderer?.videoId) {
|
|
151
|
+
videos.push(parseVideoRenderer(videoRenderer));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (videos.length > 0) break;
|
|
155
|
+
}
|
|
156
|
+
return videos;
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function parseVideoRenderer(renderer) {
|
|
162
|
+
const videoId = renderer.videoId;
|
|
163
|
+
return {
|
|
164
|
+
videoId,
|
|
165
|
+
title: renderer.title?.runs?.[0]?.text || renderer.title?.simpleText || "",
|
|
166
|
+
description: renderer.descriptionSnippet?.runs?.map((r) => r.text).join("") || "",
|
|
167
|
+
publishedAt: renderer.publishedTimeText?.simpleText || "",
|
|
168
|
+
duration: renderer.lengthText?.simpleText || "",
|
|
169
|
+
durationSeconds: parseDuration(renderer.lengthText?.simpleText),
|
|
170
|
+
viewCount: parseViewCount(renderer.viewCountText?.simpleText),
|
|
171
|
+
url: `https://youtube.com/watch?v=${videoId}`,
|
|
172
|
+
thumbnails: {
|
|
173
|
+
default: `https://i.ytimg.com/vi/${videoId}/default.jpg`,
|
|
174
|
+
medium: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`,
|
|
175
|
+
high: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
|
|
176
|
+
maxres: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function parseDuration(duration) {
|
|
181
|
+
if (!duration) return void 0;
|
|
182
|
+
const parts = duration.split(":").map(Number);
|
|
183
|
+
if (parts.length === 3) {
|
|
184
|
+
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
185
|
+
} else if (parts.length === 2) {
|
|
186
|
+
return parts[0] * 60 + parts[1];
|
|
187
|
+
}
|
|
188
|
+
return parts[0];
|
|
189
|
+
}
|
|
190
|
+
function parseViewCount(viewText) {
|
|
191
|
+
if (!viewText) return void 0;
|
|
192
|
+
const match = viewText.match(/[\d,]+/);
|
|
193
|
+
if (match) {
|
|
194
|
+
return parseInt(match[0].replace(/,/g, ""), 10);
|
|
195
|
+
}
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
async function resolveChannelId(channelInfo) {
|
|
199
|
+
if (channelInfo.type === "id") {
|
|
200
|
+
return channelInfo.value;
|
|
201
|
+
}
|
|
202
|
+
let url;
|
|
203
|
+
if (channelInfo.type === "handle") {
|
|
204
|
+
url = `https://www.youtube.com/${channelInfo.value}`;
|
|
205
|
+
} else {
|
|
206
|
+
url = `https://www.youtube.com/c/${channelInfo.value}`;
|
|
207
|
+
}
|
|
208
|
+
const response = await fetch(url, {
|
|
209
|
+
headers: { "User-Agent": USER_AGENT }
|
|
210
|
+
});
|
|
211
|
+
const html = await response.text();
|
|
212
|
+
const match = html.match(/"channelId":"(UC[a-zA-Z0-9_-]{22})"/);
|
|
213
|
+
if (match) {
|
|
214
|
+
return match[1];
|
|
215
|
+
}
|
|
216
|
+
throw new YouTubeToolsError(
|
|
217
|
+
`Could not resolve channel ID for: ${channelInfo.value}`,
|
|
218
|
+
ErrorCodes.CHANNEL_NOT_FOUND
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/channel/get-info.ts
|
|
223
|
+
var USER_AGENT2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
224
|
+
async function getChannelInfo(channel) {
|
|
225
|
+
const channelInfo = parseChannelId(channel);
|
|
226
|
+
let url;
|
|
227
|
+
if (channelInfo.type === "id") {
|
|
228
|
+
url = `https://www.youtube.com/channel/${channelInfo.value}`;
|
|
229
|
+
} else if (channelInfo.type === "handle") {
|
|
230
|
+
url = `https://www.youtube.com/${channelInfo.value}`;
|
|
231
|
+
} else {
|
|
232
|
+
url = `https://www.youtube.com/c/${channelInfo.value}`;
|
|
233
|
+
}
|
|
234
|
+
const response = await fetch(url, {
|
|
235
|
+
headers: {
|
|
236
|
+
"User-Agent": USER_AGENT2,
|
|
237
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
throw new YouTubeToolsError(
|
|
242
|
+
`Channel not found: ${channel}`,
|
|
243
|
+
ErrorCodes.CHANNEL_NOT_FOUND,
|
|
244
|
+
response.status
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const html = await response.text();
|
|
248
|
+
return extractChannelInfo(html);
|
|
249
|
+
}
|
|
250
|
+
function extractChannelInfo(html) {
|
|
251
|
+
const dataMatch = html.match(/var ytInitialData = (.+?);<\/script>/s);
|
|
252
|
+
if (!dataMatch) {
|
|
253
|
+
throw new YouTubeToolsError(
|
|
254
|
+
"Could not parse channel data",
|
|
255
|
+
ErrorCodes.PARSING_ERROR
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const data = JSON.parse(dataMatch[1]);
|
|
260
|
+
const metadata = data?.metadata?.channelMetadataRenderer || {};
|
|
261
|
+
const header = data?.header?.c4TabbedHeaderRenderer || data?.header?.pageHeaderRenderer?.pageHeaderViewModel || {};
|
|
262
|
+
const channelIdMatch = html.match(/"channelId":"(UC[a-zA-Z0-9_-]{22})"/);
|
|
263
|
+
const channelId = channelIdMatch?.[1] || metadata.externalId || "";
|
|
264
|
+
let subscriberCount;
|
|
265
|
+
const subText = header?.subscriberCountText?.simpleText || header?.metadata?.contentMetadataViewModel?.metadataRows?.[1]?.metadataParts?.[0]?.text?.content;
|
|
266
|
+
if (subText) {
|
|
267
|
+
subscriberCount = parseCount(subText);
|
|
268
|
+
}
|
|
269
|
+
let videoCount;
|
|
270
|
+
const videoText = header?.videosCountText?.runs?.[0]?.text;
|
|
271
|
+
if (videoText) {
|
|
272
|
+
videoCount = parseInt(videoText.replace(/,/g, ""), 10);
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
channelId,
|
|
276
|
+
title: metadata.title || header?.title || "",
|
|
277
|
+
description: metadata.description || "",
|
|
278
|
+
customUrl: metadata.vanityChannelUrl?.split("/").pop() || "",
|
|
279
|
+
subscriberCount,
|
|
280
|
+
videoCount,
|
|
281
|
+
url: `https://youtube.com/channel/${channelId}`,
|
|
282
|
+
thumbnails: {
|
|
283
|
+
default: metadata.avatar?.thumbnails?.[0]?.url,
|
|
284
|
+
high: metadata.avatar?.thumbnails?.slice(-1)[0]?.url
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
} catch (error) {
|
|
288
|
+
throw new YouTubeToolsError(
|
|
289
|
+
"Failed to parse channel info",
|
|
290
|
+
ErrorCodes.PARSING_ERROR
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function parseCount(text) {
|
|
295
|
+
const match = text.match(/([\d.]+)\s*([KMB])?/i);
|
|
296
|
+
if (!match) return 0;
|
|
297
|
+
let num = parseFloat(match[1]);
|
|
298
|
+
const suffix = match[2]?.toUpperCase();
|
|
299
|
+
if (suffix === "K") num *= 1e3;
|
|
300
|
+
else if (suffix === "M") num *= 1e6;
|
|
301
|
+
else if (suffix === "B") num *= 1e9;
|
|
302
|
+
return Math.round(num);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/video/get-transcript.ts
|
|
306
|
+
var USER_AGENT3 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
307
|
+
var WATCH_URL = "https://www.youtube.com/watch?v=";
|
|
308
|
+
var INNERTUBE_API_URL = "https://www.youtube.com/youtubei/v1/player?key=";
|
|
309
|
+
var INNERTUBE_CONTEXT = {
|
|
310
|
+
client: {
|
|
311
|
+
hl: "en",
|
|
312
|
+
gl: "US",
|
|
313
|
+
clientName: "WEB",
|
|
314
|
+
clientVersion: "2.20240101.00.00"
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
async function listTranscripts(videoId) {
|
|
318
|
+
const captionsData = await fetchCaptionsData(videoId);
|
|
319
|
+
return captionsData.tracks;
|
|
320
|
+
}
|
|
321
|
+
async function getTranscript(videoId, options = {}) {
|
|
322
|
+
const { languages = ["en"], preferGenerated = false } = options;
|
|
323
|
+
const captionsData = await fetchCaptionsData(videoId);
|
|
324
|
+
const tracks = captionsData.tracks;
|
|
325
|
+
if (tracks.length === 0) {
|
|
326
|
+
throw new YouTubeToolsError(
|
|
327
|
+
`No transcripts available for video: ${videoId}`,
|
|
328
|
+
ErrorCodes.TRANSCRIPT_NOT_AVAILABLE
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const track = findBestTrack(tracks, languages, preferGenerated);
|
|
332
|
+
if (!track) {
|
|
333
|
+
throw new YouTubeToolsError(
|
|
334
|
+
`No transcript found for languages: ${languages.join(", ")}`,
|
|
335
|
+
ErrorCodes.TRANSCRIPT_NOT_AVAILABLE
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const url = track.baseUrl.replace("&fmt=srv3", "");
|
|
339
|
+
return fetchTranscriptXML(url);
|
|
340
|
+
}
|
|
341
|
+
async function getTranscriptText(videoId, options = {}) {
|
|
342
|
+
const segments = await getTranscript(videoId, options);
|
|
343
|
+
return segments.map((s) => s.text).join("\n");
|
|
344
|
+
}
|
|
345
|
+
async function getTranscriptSRT(videoId, options = {}) {
|
|
346
|
+
const segments = await getTranscript(videoId, options);
|
|
347
|
+
return formatAsSRT(segments);
|
|
348
|
+
}
|
|
349
|
+
async function getTranscriptVTT(videoId, options = {}) {
|
|
350
|
+
const segments = await getTranscript(videoId, options);
|
|
351
|
+
return formatAsVTT(segments);
|
|
352
|
+
}
|
|
353
|
+
async function fetchCaptionsData(videoId) {
|
|
354
|
+
const html = await fetchVideoHtml(videoId);
|
|
355
|
+
const apiKey = extractInnertubeApiKey(html);
|
|
356
|
+
const data = await fetchInnertubeData(videoId, apiKey);
|
|
357
|
+
return extractCaptionsData(data, videoId);
|
|
358
|
+
}
|
|
359
|
+
async function fetchVideoHtml(videoId) {
|
|
360
|
+
const url = WATCH_URL + videoId;
|
|
361
|
+
const response = await fetch(url, {
|
|
362
|
+
headers: {
|
|
363
|
+
"User-Agent": USER_AGENT3,
|
|
364
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
365
|
+
Accept: "text/html,application/xhtml+xml"
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
if (!response.ok) {
|
|
369
|
+
throw new YouTubeToolsError(
|
|
370
|
+
`Failed to fetch video page: ${response.status}`,
|
|
371
|
+
ErrorCodes.NETWORK_ERROR,
|
|
372
|
+
response.status
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const html = await response.text();
|
|
376
|
+
return decodeHtmlEntities(html);
|
|
377
|
+
}
|
|
378
|
+
function extractInnertubeApiKey(html) {
|
|
379
|
+
const match = html.match(/"INNERTUBE_API_KEY":\s*"([a-zA-Z0-9_-]+)"/);
|
|
380
|
+
if (match && match[1]) {
|
|
381
|
+
return match[1];
|
|
382
|
+
}
|
|
383
|
+
if (html.includes('class="g-recaptcha"')) {
|
|
384
|
+
throw new YouTubeToolsError(
|
|
385
|
+
"IP blocked by YouTube (reCAPTCHA required)",
|
|
386
|
+
ErrorCodes.RATE_LIMITED
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
throw new YouTubeToolsError(
|
|
390
|
+
"Could not extract Innertube API key",
|
|
391
|
+
ErrorCodes.PARSING_ERROR
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
async function fetchInnertubeData(videoId, apiKey) {
|
|
395
|
+
const url = INNERTUBE_API_URL + apiKey;
|
|
396
|
+
const response = await fetch(url, {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers: {
|
|
399
|
+
"User-Agent": USER_AGENT3,
|
|
400
|
+
"Content-Type": "application/json"
|
|
401
|
+
},
|
|
402
|
+
body: JSON.stringify({
|
|
403
|
+
context: INNERTUBE_CONTEXT,
|
|
404
|
+
videoId
|
|
405
|
+
})
|
|
406
|
+
});
|
|
407
|
+
if (!response.ok) {
|
|
408
|
+
throw new YouTubeToolsError(
|
|
409
|
+
`Innertube API failed: ${response.status}`,
|
|
410
|
+
ErrorCodes.NETWORK_ERROR,
|
|
411
|
+
response.status
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return response.json();
|
|
415
|
+
}
|
|
416
|
+
function extractCaptionsData(data, videoId) {
|
|
417
|
+
const playabilityStatus = data?.playabilityStatus?.status;
|
|
418
|
+
if (playabilityStatus && playabilityStatus !== "OK") {
|
|
419
|
+
const reason = data?.playabilityStatus?.reason || "Video unavailable";
|
|
420
|
+
throw new YouTubeToolsError(
|
|
421
|
+
`Video not playable: ${reason}`,
|
|
422
|
+
ErrorCodes.VIDEO_NOT_FOUND
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
const captionsData = data?.captions?.playerCaptionsTracklistRenderer;
|
|
426
|
+
if (!captionsData || !captionsData.captionTracks) {
|
|
427
|
+
throw new YouTubeToolsError(
|
|
428
|
+
`No transcripts available for video: ${videoId}`,
|
|
429
|
+
ErrorCodes.TRANSCRIPT_NOT_AVAILABLE
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
const tracks = captionsData.captionTracks.map((track) => ({
|
|
433
|
+
languageCode: track.languageCode,
|
|
434
|
+
language: track.name?.runs?.[0]?.text || track.name?.simpleText || track.languageCode,
|
|
435
|
+
baseUrl: track.baseUrl,
|
|
436
|
+
isGenerated: track.kind === "asr",
|
|
437
|
+
isTranslatable: track.isTranslatable || false
|
|
438
|
+
}));
|
|
439
|
+
const translationLanguages = (captionsData.translationLanguages || []).map(
|
|
440
|
+
(lang) => lang.languageCode
|
|
441
|
+
);
|
|
442
|
+
return { tracks, translationLanguages };
|
|
443
|
+
}
|
|
444
|
+
async function fetchTranscriptXML(url) {
|
|
445
|
+
const response = await fetch(url, {
|
|
446
|
+
headers: {
|
|
447
|
+
"User-Agent": USER_AGENT3,
|
|
448
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
if (!response.ok) {
|
|
452
|
+
throw new YouTubeToolsError(
|
|
453
|
+
`Failed to fetch transcript: ${response.status}`,
|
|
454
|
+
ErrorCodes.NETWORK_ERROR,
|
|
455
|
+
response.status
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
const xml = await response.text();
|
|
459
|
+
return parseTranscriptXML(xml);
|
|
460
|
+
}
|
|
461
|
+
function parseTranscriptXML(xml) {
|
|
462
|
+
const segments = [];
|
|
463
|
+
const regex = /<text\s+start="([^"]+)"\s+dur="([^"]*)"[^>]*>([^<]*)<\/text>/g;
|
|
464
|
+
let match;
|
|
465
|
+
while ((match = regex.exec(xml)) !== null) {
|
|
466
|
+
const text = decodeHtmlEntities(match[3]).trim();
|
|
467
|
+
if (text) {
|
|
468
|
+
segments.push({
|
|
469
|
+
start: parseFloat(match[1]),
|
|
470
|
+
duration: parseFloat(match[2] || "0"),
|
|
471
|
+
text
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (segments.length === 0) {
|
|
476
|
+
const altRegex = /<text\s+start="([^"]+)"[^>]*>([^<]*)<\/text>/g;
|
|
477
|
+
while ((match = altRegex.exec(xml)) !== null) {
|
|
478
|
+
const text = decodeHtmlEntities(match[2]).trim();
|
|
479
|
+
if (text) {
|
|
480
|
+
segments.push({
|
|
481
|
+
start: parseFloat(match[1]),
|
|
482
|
+
duration: 0,
|
|
483
|
+
text
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return segments;
|
|
489
|
+
}
|
|
490
|
+
function findBestTrack(tracks, languages, preferGenerated) {
|
|
491
|
+
const sortedTracks = [...tracks].sort((a, b) => {
|
|
492
|
+
if (a.isGenerated !== b.isGenerated) {
|
|
493
|
+
if (preferGenerated) {
|
|
494
|
+
return a.isGenerated ? -1 : 1;
|
|
495
|
+
}
|
|
496
|
+
return a.isGenerated ? 1 : -1;
|
|
497
|
+
}
|
|
498
|
+
return 0;
|
|
499
|
+
});
|
|
500
|
+
for (const lang of languages) {
|
|
501
|
+
const track = sortedTracks.find(
|
|
502
|
+
(t) => t.languageCode === lang || t.languageCode.startsWith(lang + "-")
|
|
503
|
+
);
|
|
504
|
+
if (track) {
|
|
505
|
+
return track;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return sortedTracks[0] || null;
|
|
509
|
+
}
|
|
510
|
+
function decodeHtmlEntities(text) {
|
|
511
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(///g, "/").replace(/\n/g, " ").replace(/\\n/g, " ");
|
|
512
|
+
}
|
|
513
|
+
function formatTime(seconds) {
|
|
514
|
+
const hours = Math.floor(seconds / 3600);
|
|
515
|
+
const mins = Math.floor(seconds % 3600 / 60);
|
|
516
|
+
const secs = Math.floor(seconds % 60);
|
|
517
|
+
const ms = Math.floor(seconds % 1 * 1e3);
|
|
518
|
+
return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")},${ms.toString().padStart(3, "0")}`;
|
|
519
|
+
}
|
|
520
|
+
function formatTimeVTT(seconds) {
|
|
521
|
+
const hours = Math.floor(seconds / 3600);
|
|
522
|
+
const mins = Math.floor(seconds % 3600 / 60);
|
|
523
|
+
const secs = Math.floor(seconds % 60);
|
|
524
|
+
const ms = Math.floor(seconds % 1 * 1e3);
|
|
525
|
+
return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}.${ms.toString().padStart(3, "0")}`;
|
|
526
|
+
}
|
|
527
|
+
function formatAsSRT(segments) {
|
|
528
|
+
return segments.map((seg, i) => {
|
|
529
|
+
const start = formatTime(seg.start);
|
|
530
|
+
const end = formatTime(seg.start + seg.duration);
|
|
531
|
+
return `${i + 1}
|
|
532
|
+
${start} --> ${end}
|
|
533
|
+
${seg.text}
|
|
534
|
+
`;
|
|
535
|
+
}).join("\n");
|
|
536
|
+
}
|
|
537
|
+
function formatAsVTT(segments) {
|
|
538
|
+
const lines = ["WEBVTT\n"];
|
|
539
|
+
for (const seg of segments) {
|
|
540
|
+
const start = formatTimeVTT(seg.start);
|
|
541
|
+
const end = formatTimeVTT(seg.start + seg.duration);
|
|
542
|
+
lines.push(`${start} --> ${end}
|
|
543
|
+
${seg.text}
|
|
544
|
+
`);
|
|
545
|
+
}
|
|
546
|
+
return lines.join("\n");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/video/get-info.ts
|
|
550
|
+
var USER_AGENT4 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
551
|
+
async function getVideoInfo(videoIdOrUrl) {
|
|
552
|
+
const videoId = parseVideoId(videoIdOrUrl);
|
|
553
|
+
const html = await fetchVideoPage(videoId);
|
|
554
|
+
return extractVideoInfo(html, videoId);
|
|
555
|
+
}
|
|
556
|
+
async function getBasicVideoInfo(videoIdOrUrl) {
|
|
557
|
+
const videoId = parseVideoId(videoIdOrUrl);
|
|
558
|
+
const url = `https://www.youtube.com/oembed?url=https://youtube.com/watch?v=${videoId}&format=json`;
|
|
559
|
+
const response = await fetch(url);
|
|
560
|
+
if (!response.ok) {
|
|
561
|
+
throw new YouTubeToolsError(
|
|
562
|
+
`Video not found: ${videoId}`,
|
|
563
|
+
ErrorCodes.VIDEO_NOT_FOUND,
|
|
564
|
+
response.status
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
const data = await response.json();
|
|
568
|
+
return {
|
|
569
|
+
videoId,
|
|
570
|
+
title: data.title,
|
|
571
|
+
url: `https://youtube.com/watch?v=${videoId}`,
|
|
572
|
+
channelTitle: data.author_name,
|
|
573
|
+
thumbnails: {
|
|
574
|
+
default: data.thumbnail_url,
|
|
575
|
+
high: data.thumbnail_url.replace("hqdefault", "maxresdefault")
|
|
576
|
+
},
|
|
577
|
+
publishedAt: ""
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function parseVideoId(input) {
|
|
581
|
+
if (/^[a-zA-Z0-9_-]{11}$/.test(input)) {
|
|
582
|
+
return input;
|
|
583
|
+
}
|
|
584
|
+
const patterns = [
|
|
585
|
+
/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
|
|
586
|
+
/youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,
|
|
587
|
+
/youtube\.com\/v\/([a-zA-Z0-9_-]{11})/,
|
|
588
|
+
/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/
|
|
589
|
+
];
|
|
590
|
+
for (const pattern of patterns) {
|
|
591
|
+
const match = input.match(pattern);
|
|
592
|
+
if (match) {
|
|
593
|
+
return match[1];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
throw new YouTubeToolsError(
|
|
597
|
+
`Invalid video ID or URL: ${input}`,
|
|
598
|
+
ErrorCodes.VIDEO_NOT_FOUND
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
async function fetchVideoPage(videoId) {
|
|
602
|
+
const url = `https://www.youtube.com/watch?v=${videoId}`;
|
|
603
|
+
const response = await fetch(url, {
|
|
604
|
+
headers: {
|
|
605
|
+
"User-Agent": USER_AGENT4,
|
|
606
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
607
|
+
Cookie: "CONSENT=YES+"
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
if (!response.ok) {
|
|
611
|
+
throw new YouTubeToolsError(
|
|
612
|
+
`Failed to fetch video page: ${response.status}`,
|
|
613
|
+
ErrorCodes.NETWORK_ERROR,
|
|
614
|
+
response.status
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
return response.text();
|
|
618
|
+
}
|
|
619
|
+
function extractVideoInfo(html, videoId) {
|
|
620
|
+
const playerMatch = html.match(
|
|
621
|
+
/ytInitialPlayerResponse\s*=\s*({.+?});(?:var|<\/script>)/s
|
|
622
|
+
);
|
|
623
|
+
const dataMatch = html.match(/var ytInitialData = (.+?);<\/script>/s);
|
|
624
|
+
let playerData = {};
|
|
625
|
+
let pageData = {};
|
|
626
|
+
try {
|
|
627
|
+
if (playerMatch) {
|
|
628
|
+
playerData = JSON.parse(playerMatch[1]);
|
|
629
|
+
}
|
|
630
|
+
if (dataMatch) {
|
|
631
|
+
pageData = JSON.parse(dataMatch[1]);
|
|
632
|
+
}
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
const videoDetails = playerData?.videoDetails || {};
|
|
636
|
+
const microformat = playerData?.microformat?.playerMicroformatRenderer || {};
|
|
637
|
+
return {
|
|
638
|
+
videoId,
|
|
639
|
+
title: videoDetails.title || "",
|
|
640
|
+
description: videoDetails.shortDescription || "",
|
|
641
|
+
publishedAt: microformat.publishDate || "",
|
|
642
|
+
duration: formatDuration(parseInt(videoDetails.lengthSeconds || "0", 10)),
|
|
643
|
+
durationSeconds: parseInt(videoDetails.lengthSeconds || "0", 10),
|
|
644
|
+
viewCount: parseInt(videoDetails.viewCount || "0", 10),
|
|
645
|
+
likeCount: extractLikeCount(pageData),
|
|
646
|
+
commentCount: extractCommentCount(pageData),
|
|
647
|
+
url: `https://youtube.com/watch?v=${videoId}`,
|
|
648
|
+
thumbnails: {
|
|
649
|
+
default: `https://i.ytimg.com/vi/${videoId}/default.jpg`,
|
|
650
|
+
medium: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`,
|
|
651
|
+
high: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
|
|
652
|
+
maxres: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`
|
|
653
|
+
},
|
|
654
|
+
channelId: videoDetails.channelId,
|
|
655
|
+
channelTitle: videoDetails.author,
|
|
656
|
+
category: microformat.category,
|
|
657
|
+
tags: videoDetails.keywords,
|
|
658
|
+
keywords: videoDetails.keywords,
|
|
659
|
+
chapters: extractChapters(pageData),
|
|
660
|
+
isLive: videoDetails.isLiveContent,
|
|
661
|
+
isUpcoming: videoDetails.isUpcoming
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function formatDuration(seconds) {
|
|
665
|
+
const hours = Math.floor(seconds / 3600);
|
|
666
|
+
const mins = Math.floor(seconds % 3600 / 60);
|
|
667
|
+
const secs = seconds % 60;
|
|
668
|
+
if (hours > 0) {
|
|
669
|
+
return `${hours}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
670
|
+
}
|
|
671
|
+
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
672
|
+
}
|
|
673
|
+
function extractLikeCount(pageData) {
|
|
674
|
+
try {
|
|
675
|
+
const contents = pageData?.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
|
|
676
|
+
for (const content of contents) {
|
|
677
|
+
const buttons = content?.videoPrimaryInfoRenderer?.videoActions?.menuRenderer?.topLevelButtons || [];
|
|
678
|
+
for (const button of buttons) {
|
|
679
|
+
const likeButton = button?.segmentedLikeDislikeButtonViewModel?.likeButtonViewModel;
|
|
680
|
+
if (likeButton?.likeButtonViewModel?.toggleButtonViewModel) {
|
|
681
|
+
const countText = likeButton.likeButtonViewModel.toggleButtonViewModel.toggleButtonViewModel?.defaultButtonViewModel?.buttonViewModel?.title;
|
|
682
|
+
if (countText) {
|
|
683
|
+
return parseCount2(countText);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
return void 0;
|
|
691
|
+
}
|
|
692
|
+
function extractCommentCount(pageData) {
|
|
693
|
+
try {
|
|
694
|
+
const contents = pageData?.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
|
|
695
|
+
for (const content of contents) {
|
|
696
|
+
const header = content?.itemSectionRenderer?.contents?.[0]?.commentsEntryPointHeaderRenderer;
|
|
697
|
+
if (header?.commentCount?.simpleText) {
|
|
698
|
+
return parseCount2(header.commentCount.simpleText);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
return void 0;
|
|
704
|
+
}
|
|
705
|
+
function extractChapters(pageData) {
|
|
706
|
+
try {
|
|
707
|
+
const panels = pageData?.playerOverlays?.playerOverlayRenderer?.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer?.playerBar?.multiMarkersPlayerBarRenderer?.markersMap || [];
|
|
708
|
+
for (const panel of panels) {
|
|
709
|
+
if (panel?.key === "AUTO_CHAPTERS" || panel?.key === "DESCRIPTION_CHAPTERS") {
|
|
710
|
+
const markers = panel?.value?.chapters || [];
|
|
711
|
+
return markers.map((m) => ({
|
|
712
|
+
title: m.chapterRenderer?.title?.simpleText || "",
|
|
713
|
+
startTime: m.chapterRenderer?.timeRangeStartMillis / 1e3 || 0
|
|
714
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
719
|
+
return [];
|
|
720
|
+
}
|
|
721
|
+
function parseCount2(text) {
|
|
722
|
+
const match = text.match(/([\d.]+)\s*([KMB])?/i);
|
|
723
|
+
if (!match) return 0;
|
|
724
|
+
let num = parseFloat(match[1]);
|
|
725
|
+
const suffix = match[2]?.toUpperCase();
|
|
726
|
+
if (suffix === "K") num *= 1e3;
|
|
727
|
+
else if (suffix === "M") num *= 1e6;
|
|
728
|
+
else if (suffix === "B") num *= 1e9;
|
|
729
|
+
return Math.round(num);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/video/search.ts
|
|
733
|
+
var USER_AGENT5 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
734
|
+
async function searchVideos(queryOrOptions) {
|
|
735
|
+
const options = typeof queryOrOptions === "string" ? { query: queryOrOptions } : queryOrOptions;
|
|
736
|
+
const { query, limit = 20, sortBy = "relevance" } = options;
|
|
737
|
+
const searchUrl = buildSearchUrl(query, options);
|
|
738
|
+
const response = await fetch(searchUrl, {
|
|
739
|
+
headers: {
|
|
740
|
+
"User-Agent": USER_AGENT5,
|
|
741
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
if (!response.ok) {
|
|
745
|
+
throw new YouTubeToolsError(
|
|
746
|
+
`Search failed: ${response.status}`,
|
|
747
|
+
ErrorCodes.NETWORK_ERROR,
|
|
748
|
+
response.status
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const html = await response.text();
|
|
752
|
+
return extractSearchResults(html, limit);
|
|
753
|
+
}
|
|
754
|
+
function buildSearchUrl(query, options) {
|
|
755
|
+
const params = new URLSearchParams({
|
|
756
|
+
search_query: query
|
|
757
|
+
});
|
|
758
|
+
if (options.sortBy === "date") {
|
|
759
|
+
params.set("sp", "CAI%3D");
|
|
760
|
+
} else if (options.sortBy === "viewCount") {
|
|
761
|
+
params.set("sp", "CAM%3D");
|
|
762
|
+
} else if (options.sortBy === "rating") {
|
|
763
|
+
params.set("sp", "CAE%3D");
|
|
764
|
+
}
|
|
765
|
+
return `https://www.youtube.com/results?${params.toString()}`;
|
|
766
|
+
}
|
|
767
|
+
function extractSearchResults(html, limit) {
|
|
768
|
+
const dataMatch = html.match(/var ytInitialData = (.+?);<\/script>/s);
|
|
769
|
+
if (!dataMatch) {
|
|
770
|
+
return [];
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
const data = JSON.parse(dataMatch[1]);
|
|
774
|
+
const videos = [];
|
|
775
|
+
const contents = data?.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
|
|
776
|
+
for (const section of contents) {
|
|
777
|
+
const items = section?.itemSectionRenderer?.contents || [];
|
|
778
|
+
for (const item of items) {
|
|
779
|
+
if (videos.length >= limit) break;
|
|
780
|
+
const videoRenderer = item?.videoRenderer;
|
|
781
|
+
if (videoRenderer?.videoId) {
|
|
782
|
+
videos.push(parseVideoRenderer2(videoRenderer));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return videos;
|
|
787
|
+
} catch {
|
|
788
|
+
return [];
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function parseVideoRenderer2(renderer) {
|
|
792
|
+
const videoId = renderer.videoId;
|
|
793
|
+
return {
|
|
794
|
+
videoId,
|
|
795
|
+
title: renderer.title?.runs?.[0]?.text || "",
|
|
796
|
+
description: renderer.detailedMetadataSnippets?.[0]?.snippetText?.runs?.map((r) => r.text).join("") || "",
|
|
797
|
+
publishedAt: renderer.publishedTimeText?.simpleText || "",
|
|
798
|
+
duration: renderer.lengthText?.simpleText || "",
|
|
799
|
+
durationSeconds: parseDuration2(renderer.lengthText?.simpleText),
|
|
800
|
+
viewCount: parseViewCount2(renderer.viewCountText?.simpleText),
|
|
801
|
+
url: `https://youtube.com/watch?v=${videoId}`,
|
|
802
|
+
thumbnails: {
|
|
803
|
+
default: `https://i.ytimg.com/vi/${videoId}/default.jpg`,
|
|
804
|
+
medium: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`,
|
|
805
|
+
high: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
|
|
806
|
+
maxres: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`
|
|
807
|
+
},
|
|
808
|
+
channelId: renderer.ownerText?.runs?.[0]?.navigationEndpoint?.browseEndpoint?.browseId,
|
|
809
|
+
channelTitle: renderer.ownerText?.runs?.[0]?.text
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function parseDuration2(duration) {
|
|
813
|
+
if (!duration) return void 0;
|
|
814
|
+
const parts = duration.split(":").map(Number);
|
|
815
|
+
if (parts.length === 3) {
|
|
816
|
+
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
817
|
+
} else if (parts.length === 2) {
|
|
818
|
+
return parts[0] * 60 + parts[1];
|
|
819
|
+
}
|
|
820
|
+
return parts[0];
|
|
821
|
+
}
|
|
822
|
+
function parseViewCount2(viewText) {
|
|
823
|
+
if (!viewText) return void 0;
|
|
824
|
+
const match = viewText.match(/([\d,.]+)\s*([KMB])?/i);
|
|
825
|
+
if (!match) return void 0;
|
|
826
|
+
let num = parseFloat(match[1].replace(/,/g, ""));
|
|
827
|
+
const suffix = match[2]?.toUpperCase();
|
|
828
|
+
if (suffix === "K") num *= 1e3;
|
|
829
|
+
else if (suffix === "M") num *= 1e6;
|
|
830
|
+
else if (suffix === "B") num *= 1e9;
|
|
831
|
+
return Math.round(num);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export {
|
|
835
|
+
YouTubeToolsError,
|
|
836
|
+
ErrorCodes,
|
|
837
|
+
getChannelVideos,
|
|
838
|
+
getChannelInfo,
|
|
839
|
+
listTranscripts,
|
|
840
|
+
getTranscript,
|
|
841
|
+
getTranscriptText,
|
|
842
|
+
getTranscriptSRT,
|
|
843
|
+
getTranscriptVTT,
|
|
844
|
+
getVideoInfo,
|
|
845
|
+
getBasicVideoInfo,
|
|
846
|
+
searchVideos
|
|
847
|
+
};
|