getraw 0.2.0 → 0.2.2

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.
@@ -41,3 +41,4 @@ jobs:
41
41
  --generate-notes
42
42
  env:
43
43
  GH_TOKEN: ${{ github.token }}
44
+
package/bun.lock CHANGED
@@ -7,6 +7,7 @@
7
7
  "dependencies": {
8
8
  "hls-parser": "^0.13.6",
9
9
  "mpd-parser": "^1.3.0",
10
+ "youtubei.js": "^17.0.1",
10
11
  },
11
12
  "devDependencies": {
12
13
  "@types/bun": "latest",
@@ -19,6 +20,8 @@
19
20
  "packages": {
20
21
  "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
21
22
 
23
+ "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.0", "", {}, "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA=="],
24
+
22
25
  "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
23
26
 
24
27
  "@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
@@ -35,6 +38,8 @@
35
38
 
36
39
  "hls-parser": ["hls-parser@0.13.6", "", {}, "sha512-I40sl22E2muqeSTpG8kMN2dAegAhubkXPXtnsUXFwdKwZK47d1Q+XwuX32VMZ++AZU5oeQIZqAnGNHxSG1sWaw=="],
37
40
 
41
+ "meriyah": ["meriyah@6.1.4", "", {}, "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ=="],
42
+
38
43
  "min-document": ["min-document@2.19.2", "", { "dependencies": { "dom-walk": "^0.1.0" } }, "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A=="],
39
44
 
40
45
  "mpd-parser": ["mpd-parser@1.3.1", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "^4.0.0", "@xmldom/xmldom": "^0.8.3", "global": "^4.4.0" }, "bin": { "mpd-to-m3u8-json": "bin/parse.js" } }, "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw=="],
@@ -46,5 +51,7 @@
46
51
  "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
47
52
 
48
53
  "url-toolkit": ["url-toolkit@2.2.5", "", {}, "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg=="],
54
+
55
+ "youtubei.js": ["youtubei.js@17.0.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.0.0", "meriyah": "^6.1.4" } }, "sha512-1lO4b8UqMDzE0oh2qEGzbBOd4UYRdxn/4PdpRM7BGTHxM6ddsEsKZTu90jp8V9FHVgC2h1UirQyqoqLiKwl+Zg=="],
49
56
  }
50
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getraw",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Fast media downloader CLI built natively in Bun/TypeScript",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,11 +10,13 @@
10
10
  "dev": "bun run src/cli/index.ts",
11
11
  "test": "bun test",
12
12
  "build": "bun build src/cli/index.ts --compile --outfile=getraw",
13
- "dashboard": "bun run tools/dashboard.ts"
13
+ "dashboard": "bun run tools/dashboard.ts",
14
+ "postinstall": "bun run scripts/patch-youtubei.js"
14
15
  },
15
16
  "dependencies": {
16
17
  "hls-parser": "^0.13.6",
17
- "mpd-parser": "^1.3.0"
18
+ "mpd-parser": "^1.3.0",
19
+ "youtubei.js": "^17.0.1"
18
20
  },
19
21
  "devDependencies": {
20
22
  "@types/bun": "latest"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ // Patches youtubei.js to use Bun-native JS evaluation instead of the default stub
3
+ import { writeFileSync } from "fs";
4
+ import { resolve } from "path";
5
+
6
+ const evalPath = resolve("node_modules/youtubei.js/dist/src/platform/jsruntime/default.js");
7
+ const evalCode = `export default async function evaluate(data) {
8
+ const fn = new Function(data.output);
9
+ return fn();
10
+ }
11
+ `;
12
+
13
+ writeFileSync(evalPath, evalCode);
14
+ console.log("Patched youtubei.js jsruntime for Bun-native evaluation");
package/src/cli/index.ts CHANGED
@@ -3,7 +3,8 @@ import { parseArgs, printHelp } from "./options";
3
3
  import { Orchestrator } from "../core/orchestrator";
4
4
  import { logger } from "../core/logger";
5
5
 
6
- const VERSION = "0.0.0";
6
+ import pkg from "../../package.json";
7
+ const VERSION = pkg.version;
7
8
 
8
9
  async function main(): Promise<void> {
9
10
  const args = process.argv.slice(2);
@@ -20,7 +20,7 @@ export const FLAG_DEFS: FlagDef[] = [
20
20
  { long: "--list-formats", short: "-F", description: "List available formats", type: "boolean", key: "listFormats" },
21
21
  { long: "--dump-json", short: "-j", description: "Dump info JSON to stdout", type: "boolean", key: "dumpJson" },
22
22
  { long: "--quiet", short: "-q", description: "Suppress output", type: "boolean", key: "quiet" },
23
- { long: "--verbose", short: "-v", description: "Verbose output", type: "boolean", key: "verbose" },
23
+ { long: "--verbose", description: "Verbose output", type: "boolean", key: "verbose" },
24
24
  { long: "--no-progress", description: "Disable progress bar", type: "boolean", key: "noProgress" },
25
25
  { long: "--retries", short: "-R", description: "Number of retries", type: "number", key: "retries" },
26
26
  { long: "--rate-limit", short: "-r", description: "Rate limit in bytes/sec", type: "number", key: "rateLimit" },
@@ -32,7 +32,7 @@ export const FLAG_DEFS: FlagDef[] = [
32
32
  { long: "--embed-subs", description: "Embed subtitles in output", type: "boolean", key: "embedSubs" },
33
33
  { long: "--merge-output-format", description: "Output container for merging", type: "string", key: "mergeOutputFormat" },
34
34
  { long: "--ffmpeg-location", description: "Path to ffmpeg binary", type: "string", key: "ffmpegLocation" },
35
- { long: "--version", short: "-V", description: "Print version", type: "boolean", key: "version" },
35
+ { long: "--version", short: "-v", description: "Print version", type: "boolean", key: "version" },
36
36
  { long: "--help", short: "-h", description: "Show help", type: "boolean", key: "help" },
37
37
  ];
38
38
 
@@ -102,7 +102,7 @@ export class Orchestrator {
102
102
  ? `${filepath}.f${format.format_id}.${format.ext}`
103
103
  : filepath;
104
104
 
105
- await downloader.download(targetPath, format.url, {
105
+ await downloader.download(format.url, targetPath, {
106
106
  headers: { ...info.http_headers, ...format.http_headers },
107
107
  rateLimit: options.rateLimit,
108
108
  retries: options.retries,
@@ -1,9 +1,78 @@
1
1
  import { BaseExtractor } from "../core/types";
2
2
  import { GenericExtractor } from "./generic";
3
+ import { YouTubeExtractor } from "./youtube/index";
4
+ import { TwitterExtractor } from "./twitter/index";
5
+ import { TwitterSpacesExtractor } from "./twitter/spaces";
6
+ import { TikTokExtractor } from "./tiktok/index";
7
+ import { TikTokUserExtractor } from "./tiktok/user";
8
+ import { InstagramExtractor } from "./instagram/index";
9
+ import { InstagramReelsExtractor } from "./instagram/reels";
10
+ import { RedditExtractor } from "./reddit/index";
11
+ import { RedditGalleryExtractor } from "./reddit/gallery";
12
+ import { TwitchVODExtractor } from "./twitch/index";
13
+ import { TwitchClipExtractor } from "./twitch/clips";
14
+ import { TwitchLiveExtractor } from "./twitch/live";
15
+ import { VimeoExtractor } from "./vimeo/index";
16
+ import { SoundCloudExtractor } from "./soundcloud/index";
17
+ import { SoundCloudPlaylistExtractor } from "./soundcloud/playlist";
18
+ import { BilibiliExtractor } from "./bilibili/index";
19
+ import { BilibiliBangumiExtractor } from "./bilibili/bangumi";
20
+ import { KickExtractor } from "./kick/index";
21
+ import { KickClipsExtractor } from "./kick/clips";
22
+ import { KickLiveExtractor } from "./kick/live";
23
+ import { NiconicoExtractor } from "./niconico/index";
24
+ import { DailymotionExtractor } from "./dailymotion";
25
+ import { RumbleExtractor } from "./rumble";
26
+ import { BandcampExtractor } from "./bandcamp";
27
+ import { SpotifyExtractor } from "./spotify";
28
+ import { PeerTubeExtractor } from "./peertube";
29
+ import { OdyseeExtractor } from "./odysee";
30
+ import { StreamableExtractor } from "./streamable";
31
+ import { ImgurExtractor } from "./imgur";
32
+ import { CoubExtractor } from "./coub";
33
+ import { TEDExtractor } from "./ted";
34
+ import { ArchiveOrgExtractor } from "./archive-org";
35
+ import { DropboxExtractor } from "./dropbox";
36
+ import { GoogleDriveExtractor } from "./google-drive";
3
37
 
4
38
  export { BaseExtractor };
5
39
 
6
- const extractors: BaseExtractor[] = [];
40
+ const extractors: BaseExtractor[] = [
41
+ new YouTubeExtractor(),
42
+ new TwitterExtractor(),
43
+ new TwitterSpacesExtractor(),
44
+ new TikTokExtractor(),
45
+ new TikTokUserExtractor(),
46
+ new InstagramExtractor(),
47
+ new InstagramReelsExtractor(),
48
+ new RedditExtractor(),
49
+ new RedditGalleryExtractor(),
50
+ new TwitchVODExtractor(),
51
+ new TwitchClipExtractor(),
52
+ new TwitchLiveExtractor(),
53
+ new VimeoExtractor(),
54
+ new SoundCloudExtractor(),
55
+ new SoundCloudPlaylistExtractor(),
56
+ new BilibiliExtractor(),
57
+ new BilibiliBangumiExtractor(),
58
+ new KickExtractor(),
59
+ new KickClipsExtractor(),
60
+ new KickLiveExtractor(),
61
+ new NiconicoExtractor(),
62
+ new DailymotionExtractor(),
63
+ new RumbleExtractor(),
64
+ new BandcampExtractor(),
65
+ new SpotifyExtractor(),
66
+ new PeerTubeExtractor(),
67
+ new OdyseeExtractor(),
68
+ new StreamableExtractor(),
69
+ new ImgurExtractor(),
70
+ new CoubExtractor(),
71
+ new TEDExtractor(),
72
+ new ArchiveOrgExtractor(),
73
+ new DropboxExtractor(),
74
+ new GoogleDriveExtractor(),
75
+ ];
7
76
  const genericExtractor = new GenericExtractor();
8
77
 
9
78
  export function registerExtractor(extractor: BaseExtractor): void {
@@ -1,17 +1,29 @@
1
1
  import { BaseExtractor, ExtractorError } from "../../core/types";
2
2
  import type { InfoDict, Format, Thumbnail } from "../../core/types";
3
- import { InnerTubeClient } from "./innertube";
4
- import type { PlayerResponse, VideoDetails, StreamingData } from "./innertube";
5
- import { fetchPlayerJs, decipherSignatureUrl, clearCache as clearSigCache } from "./signature";
6
- import { transformNsig, clearNsigCache } from "./nsig";
7
3
  import { parseCaptionTracks } from "./captions";
8
- import { PlaylistExtractor } from "./playlist";
9
4
 
10
5
  const VALID_URL = /^https?:\/\/(?:(?:www|m|music)\.)?(?:youtube\.com\/(?:watch\?.*v=|shorts\/|live\/|embed\/|v\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
11
6
  const PLAYLIST_URL = /^https?:\/\/(?:(?:www|m|music)\.)?youtube\.com\/playlist\?.*list=([a-zA-Z0-9_-]+)/;
12
7
  const CHANNEL_URL = /^https?:\/\/(?:(?:www|m|music)\.)?youtube\.com\/(?:channel\/|@)([a-zA-Z0-9_-]+)/;
13
8
 
14
- const PLAYER_URL_RE = /"jsUrl"\s*:\s*"(\/s\/player\/[^"]+\/base\.js)"/;
9
+ function generateCpn(): string {
10
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
11
+ return Array.from({ length: 16 }, () => chars[Math.floor(Math.random() * 64)]).join("");
12
+ }
13
+
14
+ let _innertube: Awaited<ReturnType<typeof createInnertube>> | null = null;
15
+
16
+ async function createInnertube() {
17
+ const { Innertube } = await import("youtubei.js");
18
+ return Innertube.create({ generate_session_locally: true });
19
+ }
20
+
21
+ async function getInnertube() {
22
+ if (!_innertube) {
23
+ _innertube = await createInnertube();
24
+ }
25
+ return _innertube;
26
+ }
15
27
 
16
28
  export class YouTubeExtractor extends BaseExtractor {
17
29
  readonly _VALID_URL = new RegExp(
@@ -19,234 +31,131 @@ export class YouTubeExtractor extends BaseExtractor {
19
31
  );
20
32
  readonly _NAME = "youtube";
21
33
 
22
- private playlistExtractor = new PlaylistExtractor();
23
-
24
34
  protected async _real_extract(url: string): Promise<InfoDict> {
25
- const playlistMatch = url.match(PLAYLIST_URL);
26
- if (playlistMatch) {
27
- return this.playlistExtractor.extractPlaylist(playlistMatch[1]);
28
- }
29
-
30
- const channelMatch = url.match(CHANNEL_URL);
31
- if (channelMatch && !url.match(VALID_URL)) {
32
- return this.playlistExtractor.extractChannelVideos(channelMatch[1]);
33
- }
34
-
35
35
  const videoMatch = url.match(VALID_URL);
36
36
  if (!videoMatch) {
37
- throw new ExtractorError(`Could not extract video ID from URL: ${url}`);
37
+ throw new ExtractorError(`Unsupported YouTube URL: ${url}`);
38
38
  }
39
39
 
40
40
  return this.extractVideo(videoMatch[1]);
41
41
  }
42
42
 
43
43
  private async extractVideo(videoId: string): Promise<InfoDict> {
44
- const webClient = InnerTubeClient.withClient("WEB");
45
- let playerResponse = await webClient.getPlayerResponse(videoId);
44
+ const yt = await getInnertube();
45
+ const info = await yt.getInfo(videoId);
46
46
 
47
- const status = playerResponse.playabilityStatus?.status;
48
- if (status === "LOGIN_REQUIRED" || status === "CONTENT_CHECK_REQUIRED") {
49
- playerResponse = await this.tryAgeGateBypass(videoId, playerResponse);
47
+ if (!info.basic_info.title) {
48
+ throw new ExtractorError("Could not extract video info");
50
49
  }
51
50
 
52
- if (playerResponse.playabilityStatus?.status === "ERROR") {
53
- throw new ExtractorError(
54
- playerResponse.playabilityStatus.reason ?? "Video unavailable"
55
- );
56
- }
51
+ const formats = await this.extractFormats(info, yt);
57
52
 
58
- const videoDetails = playerResponse.videoDetails;
59
- if (!videoDetails) {
60
- throw new ExtractorError("No video details in player response");
61
- }
53
+ const thumbnails: Thumbnail[] = (info.basic_info.thumbnail ?? []).map((t: { url: string; width: number; height: number }) => ({
54
+ url: t.url,
55
+ width: t.width,
56
+ height: t.height,
57
+ }));
62
58
 
63
- let formats = await this.extractFormats(playerResponse, webClient, videoId);
59
+ const result: InfoDict = {
60
+ id: videoId,
61
+ title: info.basic_info.title,
62
+ formats,
63
+ thumbnails,
64
+ description: info.basic_info.short_description,
65
+ channel: info.basic_info.author,
66
+ channel_id: info.basic_info.channel_id,
67
+ duration: info.basic_info.duration,
68
+ view_count: info.basic_info.view_count,
69
+ webpage_url: `https://www.youtube.com/watch?v=${videoId}`,
70
+ live_status: info.basic_info.is_live ? "is_live" : "not_live",
71
+ };
64
72
 
65
- if (formats.length === 0) {
66
- const androidClient = InnerTubeClient.withClient("ANDROID");
67
- const androidResponse = await androidClient.getPlayerResponse(videoId);
68
- if (androidResponse.streamingData) {
69
- formats = androidClient.parseFormats(androidResponse.streamingData);
73
+ // Extract captions from page response
74
+ const pageResponse = await this.fetchPagePlayerResponse(videoId);
75
+ if (pageResponse) {
76
+ const captionTracks = pageResponse.captions?.playerCaptionsTracklistRenderer?.captionTracks;
77
+ if (captionTracks?.length) {
78
+ const { subtitles, automatic_captions } = parseCaptionTracks(captionTracks);
79
+ result.subtitles = subtitles;
80
+ result.automatic_captions = automatic_captions;
70
81
  }
71
82
  }
72
83
 
73
- const info = this.buildInfoDict(videoId, videoDetails, playerResponse, formats);
74
- return info;
75
- }
76
-
77
- private async tryAgeGateBypass(
78
- videoId: string,
79
- originalResponse: PlayerResponse,
80
- ): Promise<PlayerResponse> {
81
- const tvClient = InnerTubeClient.withClient("TVHTML5_EMBED");
82
- const embedUrl = `https://www.youtube.com/embed/${videoId}`;
83
- const tvResponse = await tvClient.getPlayerResponse(videoId, embedUrl);
84
-
85
- if (tvResponse.playabilityStatus?.status === "OK" && tvResponse.streamingData) {
86
- return {
87
- ...tvResponse,
88
- videoDetails: originalResponse.videoDetails ?? tvResponse.videoDetails,
89
- captions: originalResponse.captions ?? tvResponse.captions,
90
- microformat: originalResponse.microformat ?? tvResponse.microformat,
91
- };
92
- }
93
-
94
- return originalResponse;
84
+ return result;
95
85
  }
96
86
 
97
- private async extractFormats(
98
- playerResponse: PlayerResponse,
99
- client: InnerTubeClient,
100
- videoId: string,
101
- ): Promise<Format[]> {
102
- const streamingData = playerResponse.streamingData;
103
- if (!streamingData) return [];
104
-
105
- let formats = client.parseFormats(streamingData);
106
-
107
- const needsDecipher = this.formatsNeedDecipher(streamingData);
108
- if (needsDecipher) {
109
- formats = await this.decipherFormats(formats, streamingData, videoId);
110
- }
111
-
112
- return formats;
113
- }
87
+ private async extractFormats(info: { streaming_data?: { formats?: unknown[]; adaptive_formats?: unknown[] }; chooseFormat: (opts: { type: string; quality: string }) => unknown }, yt: { session: { player: unknown } }): Promise<Format[]> {
88
+ const formats: Format[] = [];
89
+ const player = yt.session.player;
90
+ const cpn = generateCpn();
114
91
 
115
- private formatsNeedDecipher(streamingData: StreamingData): boolean {
116
92
  const allFormats = [
117
- ...(streamingData.formats ?? []),
118
- ...(streamingData.adaptiveFormats ?? []),
93
+ ...(info.streaming_data?.formats ?? []),
94
+ ...(info.streaming_data?.adaptive_formats ?? []),
119
95
  ];
120
- return allFormats.some((f) => f.signatureCipher && !f.url);
121
- }
122
-
123
- private async decipherFormats(
124
- formats: Format[],
125
- streamingData: StreamingData,
126
- videoId: string,
127
- ): Promise<Format[]> {
128
- const playerJsUrl = await this.getPlayerJsUrl(videoId);
129
- if (!playerJsUrl) return formats;
130
-
131
- const playerJs = await fetchPlayerJs(playerJsUrl);
132
-
133
- const allRaw = [
134
- ...(streamingData.formats ?? []),
135
- ...(streamingData.adaptiveFormats ?? []),
136
- ];
137
-
138
- for (let i = 0; i < formats.length; i++) {
139
- const raw = allRaw[i];
140
- if (!raw) continue;
141
96
 
142
- if (raw.signatureCipher && !raw.url) {
143
- try {
144
- formats[i].url = decipherSignatureUrl(raw.signatureCipher, playerJs);
145
- } catch {
146
- continue;
97
+ for (const raw of allFormats) {
98
+ const f = raw as Record<string, unknown>;
99
+ try {
100
+ let url: string | undefined;
101
+
102
+ if (typeof (f as { decipher?: unknown }).decipher === "function") {
103
+ const deciphered = await (f as { decipher: (p: unknown) => Promise<unknown> }).decipher(player);
104
+ if (typeof deciphered === "string") {
105
+ const parsed = new URL(deciphered);
106
+ parsed.searchParams.set("cpn", cpn);
107
+ url = parsed.toString();
108
+ }
147
109
  }
148
- }
149
110
 
150
- if (formats[i].url) {
151
- try {
152
- formats[i].url = transformNsig(formats[i].url, playerJs);
153
- } catch {
154
- continue;
155
- }
111
+ if (!url) continue;
112
+
113
+ const mime = String(f.mime_type ?? "");
114
+ const mimeMatch = mime.match(/^(video|audio)\/(\w+);\s*codecs="([^"]+)"/);
115
+ const ext = mimeMatch?.[2] ?? "mp4";
116
+ const codecs = mimeMatch?.[3] ?? "";
117
+ const isVideo = mime.startsWith("video");
118
+ const isAudio = mime.startsWith("audio");
119
+
120
+ formats.push({
121
+ format_id: String(f.itag ?? ""),
122
+ url,
123
+ ext,
124
+ vcodec: isVideo ? codecs.split(",")[0]?.trim() : "none",
125
+ acodec: isAudio ? codecs : (isVideo && codecs.includes(",") ? codecs.split(",")[1]?.trim() : undefined),
126
+ width: (f.width as number) ?? undefined,
127
+ height: (f.height as number) ?? undefined,
128
+ fps: (f.fps as number) ?? undefined,
129
+ tbr: f.bitrate ? Math.round((f.bitrate as number) / 1000) : undefined,
130
+ filesize: f.content_length ? parseInt(String(f.content_length), 10) : undefined,
131
+ format_note: String(f.quality_label ?? f.quality ?? ""),
132
+ audio_channels: (f.audio_channels as number) ?? undefined,
133
+ http_headers: {
134
+ "Origin": "https://www.youtube.com",
135
+ "Referer": "https://www.youtube.com/",
136
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
137
+ },
138
+ });
139
+ } catch {
140
+ continue;
156
141
  }
157
142
  }
158
143
 
159
144
  return formats;
160
145
  }
161
146
 
162
- private async getPlayerJsUrl(videoId: string): Promise<string | null> {
163
- const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;
147
+ private async fetchPagePlayerResponse(videoId: string): Promise<Record<string, unknown> | null> {
164
148
  try {
165
- const response = await fetch(watchUrl, {
149
+ const resp = await fetch(`https://www.youtube.com/watch?v=${videoId}`, {
166
150
  headers: {
167
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
151
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
168
152
  },
169
153
  });
170
- const html = await response.text();
171
- const match = html.match(PLAYER_URL_RE);
172
- return match ? `https://www.youtube.com${match[1]}` : null;
154
+ const html = await resp.text();
155
+ const match = html.match(/var\s+ytInitialPlayerResponse\s*=\s*(\{.+?\});/s);
156
+ return match ? JSON.parse(match[1]) : null;
173
157
  } catch {
174
158
  return null;
175
159
  }
176
160
  }
177
-
178
- private buildInfoDict(
179
- videoId: string,
180
- details: VideoDetails,
181
- response: PlayerResponse,
182
- formats: Format[],
183
- ): InfoDict {
184
- const microformat = response.microformat?.playerMicroformatRenderer;
185
-
186
- const thumbnails: Thumbnail[] = (details.thumbnail?.thumbnails ?? []).map((t) => ({
187
- url: t.url,
188
- width: t.width,
189
- height: t.height,
190
- }));
191
-
192
- const liveStatus = this.getLiveStatus(details, response);
193
-
194
- const info: InfoDict = {
195
- id: videoId,
196
- title: details.title,
197
- formats,
198
- thumbnails,
199
- description: details.shortDescription ?? microformat?.description?.simpleText,
200
- channel: details.author,
201
- channel_id: details.channelId,
202
- channel_url: `https://www.youtube.com/channel/${details.channelId}`,
203
- uploader: details.author,
204
- uploader_id: details.channelId,
205
- uploader_url: microformat?.ownerProfileUrl,
206
- duration: parseInt(details.lengthSeconds, 10) || undefined,
207
- view_count: parseInt(details.viewCount, 10) || undefined,
208
- upload_date: microformat?.uploadDate?.replace(/-/g, ""),
209
- live_status: liveStatus,
210
- webpage_url: `https://www.youtube.com/watch?v=${videoId}`,
211
- age_limit: 0,
212
- categories: microformat?.category ? [microformat.category] : undefined,
213
- };
214
-
215
- if (microformat?.liveBroadcastDetails?.startTimestamp) {
216
- info.release_timestamp = Math.floor(
217
- new Date(microformat.liveBroadcastDetails.startTimestamp).getTime() / 1000
218
- );
219
- }
220
-
221
- const captionTracks = response.captions?.playerCaptionsTracklistRenderer?.captionTracks;
222
- if (captionTracks?.length) {
223
- const { subtitles, automatic_captions } = parseCaptionTracks(captionTracks);
224
- info.subtitles = subtitles;
225
- info.automatic_captions = automatic_captions;
226
- }
227
-
228
- return info;
229
- }
230
-
231
- private getLiveStatus(
232
- details: VideoDetails,
233
- response: PlayerResponse,
234
- ): InfoDict["live_status"] {
235
- if (details.isLive) return "is_live";
236
- if (details.isUpcoming) return "is_upcoming";
237
- if (details.isLiveContent) return "was_live";
238
- if (response.playabilityStatus?.liveStreamability) return "is_live";
239
- return "not_live";
240
- }
241
-
242
- static clearCaches(): void {
243
- clearSigCache();
244
- clearNsigCache();
245
- }
246
161
  }
247
-
248
- export { InnerTubeClient } from "./innertube";
249
- export { PlaylistExtractor } from "./playlist";
250
- export { parseCaptionTracks, convertToSrt, convertToVtt } from "./captions";
251
- export { decipherSignatureUrl, fetchPlayerJs } from "./signature";
252
- export { transformNsig } from "./nsig";
@@ -104,14 +104,14 @@ export interface BrowseResponse {
104
104
  const CLIENTS: Record<string, ClientContext> = {
105
105
  WEB: {
106
106
  clientName: "WEB",
107
- clientVersion: "2.20240530.02.00",
108
- userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
107
+ clientVersion: "2.20250615.01.00",
108
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
109
109
  apiKey: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
110
110
  },
111
111
  ANDROID: {
112
112
  clientName: "ANDROID",
113
- clientVersion: "19.29.37",
114
- userAgent: "com.google.android.youtube/19.29.37 (Linux; U; Android 14) gzip",
113
+ clientVersion: "19.44.38",
114
+ userAgent: "com.google.android.youtube/19.44.38 (Linux; U; Android 14) gzip",
115
115
  apiKey: "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w",
116
116
  clientId: 3,
117
117
  },