ytgrab 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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/bin/ytgrab.js +194 -0
  4. package/dist/downloader/common.d.ts +22 -0
  5. package/dist/downloader/common.d.ts.map +1 -0
  6. package/dist/downloader/common.js +125 -0
  7. package/dist/downloader/common.js.map +1 -0
  8. package/dist/downloader/hls.d.ts +11 -0
  9. package/dist/downloader/hls.d.ts.map +1 -0
  10. package/dist/downloader/hls.js +134 -0
  11. package/dist/downloader/hls.js.map +1 -0
  12. package/dist/downloader/http.d.ts +10 -0
  13. package/dist/downloader/http.d.ts.map +1 -0
  14. package/dist/downloader/http.js +132 -0
  15. package/dist/downloader/http.js.map +1 -0
  16. package/dist/downloader/index.d.ts +10 -0
  17. package/dist/downloader/index.d.ts.map +1 -0
  18. package/dist/downloader/index.js +24 -0
  19. package/dist/downloader/index.js.map +1 -0
  20. package/dist/extractor/common.d.ts +48 -0
  21. package/dist/extractor/common.d.ts.map +1 -0
  22. package/dist/extractor/common.js +324 -0
  23. package/dist/extractor/common.js.map +1 -0
  24. package/dist/extractor/nsig.d.ts +17 -0
  25. package/dist/extractor/nsig.d.ts.map +1 -0
  26. package/dist/extractor/nsig.js +200 -0
  27. package/dist/extractor/nsig.js.map +1 -0
  28. package/dist/extractor/youtube.d.ts +51 -0
  29. package/dist/extractor/youtube.d.ts.map +1 -0
  30. package/dist/extractor/youtube.js +1113 -0
  31. package/dist/extractor/youtube.js.map +1 -0
  32. package/dist/index.d.ts +36 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +72 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/networking/index.d.ts +33 -0
  37. package/dist/networking/index.d.ts.map +1 -0
  38. package/dist/networking/index.js +171 -0
  39. package/dist/networking/index.js.map +1 -0
  40. package/dist/postprocessor/common.d.ts +21 -0
  41. package/dist/postprocessor/common.d.ts.map +1 -0
  42. package/dist/postprocessor/common.js +42 -0
  43. package/dist/postprocessor/common.js.map +1 -0
  44. package/dist/postprocessor/ffmpeg.d.ts +44 -0
  45. package/dist/postprocessor/ffmpeg.d.ts.map +1 -0
  46. package/dist/postprocessor/ffmpeg.js +286 -0
  47. package/dist/postprocessor/ffmpeg.js.map +1 -0
  48. package/dist/types.d.ts +157 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +3 -0
  51. package/dist/types.js.map +1 -0
  52. package/dist/utils/index.d.ts +57 -0
  53. package/dist/utils/index.d.ts.map +1 -0
  54. package/dist/utils/index.js +403 -0
  55. package/dist/utils/index.js.map +1 -0
  56. package/dist/utils/traversal.d.ts +22 -0
  57. package/dist/utils/traversal.d.ts.map +1 -0
  58. package/dist/utils/traversal.js +112 -0
  59. package/dist/utils/traversal.js.map +1 -0
  60. package/dist/ytgrab.d.ts +48 -0
  61. package/dist/ytgrab.d.ts.map +1 -0
  62. package/dist/ytgrab.js +450 -0
  63. package/dist/ytgrab.js.map +1 -0
  64. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harry Wang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # ytgrab
2
+
3
+ A Node.js YouTube video downloader ported from [yt-dlp](https://github.com/yt-dlp/yt-dlp).
4
+
5
+ ## Features
6
+
7
+ - Download YouTube videos in available formats
8
+ - Download auto-generated and manual subtitles/captions
9
+ - Extract video metadata (title, description, thumbnails, chapters, etc.)
10
+ - N-parameter challenge solver (uses yt-dlp's EJS solver scripts)
11
+ - InnerTube API integration (android_vr, web_safari clients)
12
+ - Audio extraction with FFmpeg
13
+ - HLS/M3U8 stream downloading
14
+ - Playlist and search support
15
+ - CLI and programmatic API
16
+
17
+ ## Requirements
18
+
19
+ - Node.js >= 18
20
+ - [yt-dlp](https://github.com/yt-dlp/yt-dlp) installed (for the EJS challenge solver scripts)
21
+ - [FFmpeg](https://ffmpeg.org/) (optional, for audio extraction and format conversion)
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install
27
+ npm run build
28
+ ```
29
+
30
+ ## CLI Usage
31
+
32
+ ```bash
33
+ # Download a video
34
+ node bin/ytgrab.js "https://www.youtube.com/watch?v=VIDEO_ID"
35
+
36
+ # Download with subtitles
37
+ node bin/ytgrab.js --write-auto-subs --sub-langs en "https://www.youtube.com/watch?v=VIDEO_ID"
38
+
39
+ # List available formats
40
+ node bin/ytgrab.js -F "https://www.youtube.com/watch?v=VIDEO_ID"
41
+
42
+ # Download to a specific directory
43
+ node bin/ytgrab.js -P /path/to/output "https://www.youtube.com/watch?v=VIDEO_ID"
44
+
45
+ # Extract audio as MP3
46
+ node bin/ytgrab.js -x --audio-format mp3 "https://www.youtube.com/watch?v=VIDEO_ID"
47
+
48
+ # Print video info as JSON
49
+ node bin/ytgrab.js -j "https://www.youtube.com/watch?v=VIDEO_ID"
50
+
51
+ # Download with custom format
52
+ node bin/ytgrab.js -f 720p "https://www.youtube.com/watch?v=VIDEO_ID"
53
+
54
+ # Write metadata files
55
+ node bin/ytgrab.js --write-info-json --write-thumbnail --write-description "URL"
56
+ ```
57
+
58
+ Run `node bin/ytgrab.js -h` for all options.
59
+
60
+ ## Programmatic API
61
+
62
+ ```typescript
63
+ import { YtGrab } from 'ytgrab';
64
+
65
+ // Get video info without downloading
66
+ const yt = new YtGrab();
67
+ const info = await yt.getInfo('https://www.youtube.com/watch?v=VIDEO_ID');
68
+ console.log(info.title);
69
+ console.log(info.formats);
70
+ console.log(info.automatic_captions);
71
+
72
+ // Download a video
73
+ await yt.download('https://www.youtube.com/watch?v=VIDEO_ID');
74
+
75
+ // Download with options
76
+ const ytWithOpts = new YtGrab({
77
+ format: 'best',
78
+ output: '%(title)s.%(ext)s',
79
+ writeSubtitles: true,
80
+ subtitleLanguages: ['en'],
81
+ paths: { home: './downloads' },
82
+ progressHooks: [(progress) => {
83
+ console.log(`${progress.status}: ${progress.downloaded_bytes} bytes`);
84
+ }],
85
+ });
86
+ await ytWithOpts.download('https://www.youtube.com/watch?v=VIDEO_ID');
87
+
88
+ // Extract audio
89
+ const ytAudio = new YtGrab({
90
+ extractAudio: true,
91
+ audioFormat: 'mp3',
92
+ });
93
+ await ytAudio.download('https://www.youtube.com/watch?v=VIDEO_ID');
94
+
95
+ // List formats
96
+ const formats = await yt.listFormats('https://www.youtube.com/watch?v=VIDEO_ID');
97
+
98
+ // List subtitles
99
+ const subs = await yt.listSubtitles('https://www.youtube.com/watch?v=VIDEO_ID');
100
+ ```
101
+
102
+ ## Project Structure
103
+
104
+ ```
105
+ ytgrab/
106
+ ├── bin/ytgrab.js # CLI entry point
107
+ ├── src/
108
+ │ ├── index.ts # Public API exports
109
+ │ ├── types.ts # TypeScript interfaces
110
+ │ ├── ytgrab.ts # Main orchestrator (YtGrab class)
111
+ │ ├── utils/
112
+ │ │ ├── index.ts # Utility functions
113
+ │ │ └── traversal.ts # traverse_obj / tryGet
114
+ │ ├── networking/
115
+ │ │ └── index.ts # HTTP client
116
+ │ ├── extractor/
117
+ │ │ ├── common.ts # Base InfoExtractor
118
+ │ │ ├── youtube.ts # YouTube extractor
119
+ │ │ └── nsig.ts # N-parameter challenge solver
120
+ │ ├── downloader/
121
+ │ │ ├── common.ts # Base downloader
122
+ │ │ ├── http.ts # HTTP downloader
123
+ │ │ ├── hls.ts # HLS downloader
124
+ │ │ └── index.ts # Downloader registry
125
+ │ └── postprocessor/
126
+ │ ├── common.ts # Base post-processor
127
+ │ └── ffmpeg.ts # FFmpeg post-processors
128
+ └── dist/ # Compiled output
129
+ ```
130
+
131
+ ## How It Works
132
+
133
+ 1. **Webpage fetch** — Downloads the YouTube watch page to extract the initial player response and player JS URL
134
+ 2. **InnerTube API** — Calls YouTube's internal API with `android_vr` and `web_safari` clients to get video formats and captions
135
+ 3. **N-parameter solving** — Uses yt-dlp's EJS challenge solver (meriyah + astring) to transform the `n` throttle parameter in format URLs
136
+ 4. **Format selection** — Picks the best available format based on user preferences
137
+ 5. **Download** — Downloads the video via HTTP with resume support, or HLS fragment downloading
138
+ 6. **Post-processing** — Optional FFmpeg operations (audio extraction, format conversion, subtitle embedding)
139
+
140
+ ## License
141
+
142
+ MIT
package/bin/ytgrab.js ADDED
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ytgrab CLI - YouTube video downloader
5
+ * Usage: ytgrab [OPTIONS] URL [URL...]
6
+ */
7
+
8
+ import { parseArgs } from 'node:util';
9
+ import { YtGrab } from '../dist/index.js';
10
+
11
+ const { values: opts, positionals: urls } = parseArgs({
12
+ allowPositionals: true,
13
+ options: {
14
+ // Output
15
+ output: { type: 'string', short: 'o', default: '%(title)s [%(id)s].%(ext)s' },
16
+ paths: { type: 'string', short: 'P' },
17
+
18
+ // Format selection
19
+ format: { type: 'string', short: 'f', default: 'best' },
20
+ 'merge-output-format': { type: 'string' },
21
+
22
+ // Info
23
+ 'list-formats': { type: 'boolean', short: 'F', default: false },
24
+ 'list-subs': { type: 'boolean', default: false },
25
+ 'print-json': { type: 'boolean', short: 'j', default: false },
26
+ simulate: { type: 'boolean', short: 's', default: false },
27
+ 'skip-download': { type: 'boolean', default: false },
28
+
29
+ // Subtitles
30
+ 'write-subs': { type: 'boolean', default: false },
31
+ 'write-auto-subs': { type: 'boolean', default: false },
32
+ 'sub-langs': { type: 'string', default: 'en' },
33
+ 'sub-format': { type: 'string', default: 'srt' },
34
+
35
+ // Thumbnails & metadata
36
+ 'write-thumbnail': { type: 'boolean', default: false },
37
+ 'write-info-json': { type: 'boolean', default: false },
38
+ 'write-description': { type: 'boolean', default: false },
39
+
40
+ // Audio
41
+ 'extract-audio': { type: 'boolean', short: 'x', default: false },
42
+ 'audio-format': { type: 'string', default: 'mp3' },
43
+ 'audio-quality': { type: 'string', default: '5' },
44
+
45
+ // Embedding
46
+ 'embed-subs': { type: 'boolean', default: false },
47
+ 'embed-thumbnail': { type: 'boolean', default: false },
48
+ 'embed-metadata': { type: 'boolean', default: false },
49
+
50
+ // Network
51
+ proxy: { type: 'string' },
52
+ retries: { type: 'string', short: 'R', default: '10' },
53
+
54
+ // FFmpeg
55
+ 'ffmpeg-location': { type: 'string' },
56
+
57
+ // Verbosity
58
+ quiet: { type: 'boolean', short: 'q', default: false },
59
+ verbose: { type: 'boolean', short: 'v', default: false },
60
+ 'no-progress': { type: 'boolean', default: false },
61
+
62
+ // Help
63
+ help: { type: 'boolean', short: 'h', default: false },
64
+ version: { type: 'boolean', short: 'V', default: false },
65
+ },
66
+ });
67
+
68
+ if (opts.help) {
69
+ console.log(`
70
+ ytgrab - YouTube video downloader for Node.js (ported from yt-dlp)
71
+
72
+ Usage: ytgrab [OPTIONS] URL [URL...]
73
+
74
+ Options:
75
+ -o, --output TEMPLATE Output filename template (default: %(title)s [%(id)s].%(ext)s)
76
+ -P, --paths PATH Output directory
77
+ -f, --format FORMAT Video format (default: best)
78
+ Examples: best, worst, bestaudio, 720p, 1080p, 137
79
+ -F, --list-formats List available formats and exit
80
+ --list-subs List available subtitles and exit
81
+ -j, --print-json Print info JSON and exit
82
+ -s, --simulate Do not download, just print info
83
+
84
+ --write-subs Download subtitles
85
+ --write-auto-subs Download auto-generated subtitles
86
+ --sub-langs LANGS Subtitle languages (comma-separated, default: en)
87
+ --sub-format FMT Subtitle format (default: srt)
88
+
89
+ --write-thumbnail Download thumbnail
90
+ --write-info-json Write video info to .info.json
91
+ --write-description Write description to .description
92
+
93
+ -x, --extract-audio Extract audio (requires FFmpeg)
94
+ --audio-format FMT Audio format: mp3, aac, opus, flac, wav (default: mp3)
95
+ --audio-quality Q Audio quality: 0 (best) to 9 (worst) (default: 5)
96
+
97
+ --embed-subs Embed subtitles (requires FFmpeg)
98
+ --embed-thumbnail Embed thumbnail (requires FFmpeg)
99
+ --embed-metadata Embed metadata (requires FFmpeg)
100
+ --merge-output-format FMT Merge format: mp4, mkv, webm
101
+
102
+ --proxy URL Use proxy
103
+ -R, --retries N Number of retries (default: 10)
104
+ --ffmpeg-location PATH FFmpeg binary path
105
+
106
+ -q, --quiet Suppress output
107
+ -v, --verbose Verbose output
108
+ --no-progress Hide progress bar
109
+
110
+ -h, --help Show this help
111
+ -V, --version Show version
112
+ `);
113
+ process.exit(0);
114
+ }
115
+
116
+ if (opts.version) {
117
+ console.log('ytgrab 0.1.0');
118
+ process.exit(0);
119
+ }
120
+
121
+ if (urls.length === 0) {
122
+ console.error('Error: No URL provided. Use -h for help.');
123
+ process.exit(1);
124
+ }
125
+
126
+ const ytgrab = new YtGrab({
127
+ format: opts.format,
128
+ output: opts.output,
129
+ paths: opts.paths ? { home: opts.paths } : undefined,
130
+ quiet: opts.quiet,
131
+ verbose: opts.verbose,
132
+ noProgress: opts['no-progress'],
133
+ simulate: opts.simulate || opts['print-json'],
134
+ skipDownload: opts['skip-download'],
135
+ listFormats: opts['list-formats'],
136
+ listSubtitles: opts['list-subs'],
137
+ writeSubtitles: opts['write-subs'],
138
+ writeAutoSubtitles: opts['write-auto-subs'],
139
+ subtitleLanguages: opts['sub-langs']?.split(','),
140
+ subtitleFormat: opts['sub-format'],
141
+ writeThumbnail: opts['write-thumbnail'],
142
+ writeInfoJson: opts['write-info-json'],
143
+ writeDescription: opts['write-description'],
144
+ extractAudio: opts['extract-audio'],
145
+ audioFormat: opts['audio-format'],
146
+ audioQuality: opts['audio-quality'],
147
+ embedSubtitles: opts['embed-subs'],
148
+ embedThumbnail: opts['embed-thumbnail'],
149
+ embedMetadata: opts['embed-metadata'],
150
+ mergeOutputFormat: opts['merge-output-format'],
151
+ proxy: opts.proxy,
152
+ retries: parseInt(opts.retries || '10'),
153
+ ffmpegLocation: opts['ffmpeg-location'],
154
+ progressHooks: opts.quiet ? [] : [
155
+ (progress) => {
156
+ if (opts['no-progress']) return;
157
+ if (progress.status === 'downloading') {
158
+ const pct = progress.total_bytes
159
+ ? ((progress.downloaded_bytes || 0) / progress.total_bytes * 100).toFixed(1)
160
+ : '?';
161
+ const speed = progress.speed
162
+ ? `${(progress.speed / 1024 / 1024).toFixed(2)} MiB/s`
163
+ : '? MiB/s';
164
+ const eta = progress.eta ? `ETA ${progress.eta}s` : '';
165
+ const frag = progress.fragment_count
166
+ ? `frag ${progress.fragment_index}/${progress.fragment_count}`
167
+ : '';
168
+ process.stdout.write(
169
+ `\r[download] ${pct}% ${speed} ${eta} ${frag}`.padEnd(80)
170
+ );
171
+ } else if (progress.status === 'finished') {
172
+ process.stdout.write('\n');
173
+ }
174
+ },
175
+ ],
176
+ });
177
+
178
+ async function main() {
179
+ try {
180
+ if (opts['print-json']) {
181
+ for (const url of urls) {
182
+ const info = await ytgrab.getInfo(url);
183
+ console.log(JSON.stringify(info, null, 2));
184
+ }
185
+ } else {
186
+ await ytgrab.download(urls);
187
+ }
188
+ } catch (err) {
189
+ console.error(`ERROR: ${(err).message}`);
190
+ process.exit(1);
191
+ }
192
+ }
193
+
194
+ main();
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Base FileDownloader - ported from yt_dlp/downloader/common.py
3
+ */
4
+ import type { InfoDict, DownloadProgress, ProgressHook } from '../types.js';
5
+ export declare abstract class FileDownloader {
6
+ protected _ydl: any;
7
+ protected _progressHooks: ProgressHook[];
8
+ constructor(ydl: any);
9
+ abstract realDownload(filename: string, infoDict: InfoDict): Promise<boolean>;
10
+ addProgressHook(hook: ProgressHook): void;
11
+ protected _hookProgress(progress: DownloadProgress): void;
12
+ protected tempName(filename: string): string;
13
+ protected undoTempName(filename: string): string;
14
+ protected reportDestination(filename: string): void;
15
+ protected reportProgress(downloaded: number, total: number | null, startTime: number): void;
16
+ protected reportFinished(filename: string, filesize: number): void;
17
+ protected calcSpeed(start: number, now: number, bytes: number): number | null;
18
+ protected calcEta(start: number, now: number, total: number, current: number): number | null;
19
+ protected ensureDir(filepath: string): void;
20
+ protected _log(msg: string): void;
21
+ }
22
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/downloader/common.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG5E,8BAAsB,cAAc;IAClC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC;IACpB,SAAS,CAAC,cAAc,EAAE,YAAY,EAAE,CAAM;gBAElC,GAAG,EAAE,GAAG;IAOpB,QAAQ,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAE7E,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAIzC,SAAS,CAAC,aAAa,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IAMzD,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAI5C,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAIhD,SAAS,CAAC,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAInD,SAAS,CAAC,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAe3F,SAAS,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAUlE,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAM7E,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAO5F,SAAS,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAO3C,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAQlC"}
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * Base FileDownloader - ported from yt_dlp/downloader/common.py
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.FileDownloader = void 0;
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ const index_js_1 = require("../utils/index.js");
43
+ class FileDownloader {
44
+ _ydl;
45
+ _progressHooks = [];
46
+ constructor(ydl) {
47
+ this._ydl = ydl;
48
+ if (ydl?.params?.progressHooks) {
49
+ this._progressHooks.push(...ydl.params.progressHooks);
50
+ }
51
+ }
52
+ addProgressHook(hook) {
53
+ this._progressHooks.push(hook);
54
+ }
55
+ _hookProgress(progress) {
56
+ for (const hook of this._progressHooks) {
57
+ try {
58
+ hook(progress);
59
+ }
60
+ catch { /* ignore hook errors */ }
61
+ }
62
+ }
63
+ tempName(filename) {
64
+ return `${filename}.part`;
65
+ }
66
+ undoTempName(filename) {
67
+ return filename.replace(/\.part$/, '');
68
+ }
69
+ reportDestination(filename) {
70
+ this._log(`Destination: ${filename}`);
71
+ }
72
+ reportProgress(downloaded, total, startTime) {
73
+ const elapsed = (Date.now() - startTime) / 1000;
74
+ const speed = elapsed > 0 ? downloaded / elapsed : 0;
75
+ const eta = total && speed > 0 ? Math.round((total - downloaded) / speed) : undefined;
76
+ this._hookProgress({
77
+ status: 'downloading',
78
+ downloaded_bytes: downloaded,
79
+ total_bytes: total ?? undefined,
80
+ elapsed,
81
+ speed,
82
+ eta,
83
+ });
84
+ }
85
+ reportFinished(filename, filesize) {
86
+ this._log(`Download completed: ${filename} (${(0, index_js_1.formatBytes)(filesize)})`);
87
+ this._hookProgress({
88
+ status: 'finished',
89
+ filename,
90
+ downloaded_bytes: filesize,
91
+ total_bytes: filesize,
92
+ });
93
+ }
94
+ calcSpeed(start, now, bytes) {
95
+ const elapsed = (now - start) / 1000;
96
+ if (elapsed <= 0)
97
+ return null;
98
+ return bytes / elapsed;
99
+ }
100
+ calcEta(start, now, total, current) {
101
+ const elapsed = (now - start) / 1000;
102
+ if (elapsed <= 0 || current <= 0)
103
+ return null;
104
+ const rate = current / elapsed;
105
+ return Math.round((total - current) / rate);
106
+ }
107
+ ensureDir(filepath) {
108
+ const dir = path.dirname(filepath);
109
+ if (!fs.existsSync(dir)) {
110
+ fs.mkdirSync(dir, { recursive: true });
111
+ }
112
+ }
113
+ _log(msg) {
114
+ if (this._ydl?.params?.quiet)
115
+ return;
116
+ if (this._ydl) {
117
+ this._ydl.toScreen(`[download] ${msg}`);
118
+ }
119
+ else {
120
+ console.log(`[download] ${msg}`);
121
+ }
122
+ }
123
+ }
124
+ exports.FileDownloader = FileDownloader;
125
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/downloader/common.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * HLS (M3U8) Downloader - ported from yt_dlp/downloader/hls.py
3
+ * Downloads HTTP Live Streaming content by fetching fragments.
4
+ */
5
+ import { FileDownloader } from './common.js';
6
+ import type { InfoDict } from '../types.js';
7
+ export declare class HlsFD extends FileDownloader {
8
+ realDownload(filename: string, infoDict: InfoDict): Promise<boolean>;
9
+ private _parseMediaPlaylist;
10
+ }
11
+ //# sourceMappingURL=hls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hls.d.ts","sourceRoot":"","sources":["../../src/downloader/hls.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,qBAAa,KAAM,SAAQ,cAAc;IACjC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IA0E1E,OAAO,CAAC,mBAAmB;CAoB5B"}
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /**
3
+ * HLS (M3U8) Downloader - ported from yt_dlp/downloader/hls.py
4
+ * Downloads HTTP Live Streaming content by fetching fragments.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.HlsFD = void 0;
41
+ const fs = __importStar(require("node:fs"));
42
+ const common_js_1 = require("./common.js");
43
+ const index_js_1 = require("../networking/index.js");
44
+ class HlsFD extends common_js_1.FileDownloader {
45
+ async realDownload(filename, infoDict) {
46
+ const manifestUrl = (infoDict.url || infoDict.manifest_url);
47
+ if (!manifestUrl)
48
+ throw new Error('No HLS manifest URL');
49
+ this.ensureDir(filename);
50
+ this.reportDestination(filename);
51
+ // Download manifest
52
+ this._log('Downloading HLS manifest');
53
+ const manifestResp = await (0, index_js_1.makeRequest)(manifestUrl, {
54
+ headers: infoDict.http_headers ?? {},
55
+ });
56
+ const manifest = manifestResp.text();
57
+ // Parse fragments
58
+ const fragments = this._parseMediaPlaylist(manifest, manifestUrl);
59
+ if (fragments.length === 0) {
60
+ throw new Error('No fragments found in HLS manifest');
61
+ }
62
+ this._log(`Downloading ${fragments.length} fragments`);
63
+ const tmpFilename = this.tempName(filename);
64
+ const stream = fs.createWriteStream(tmpFilename);
65
+ const startTime = Date.now();
66
+ let downloadedBytes = 0;
67
+ for (let i = 0; i < fragments.length; i++) {
68
+ const frag = fragments[i];
69
+ try {
70
+ const resp = await (0, index_js_1.makeRequest)(frag.url, {
71
+ headers: infoDict.http_headers ?? {},
72
+ timeout: 60000,
73
+ });
74
+ stream.write(resp.body);
75
+ downloadedBytes += resp.body.length;
76
+ this._hookProgress({
77
+ status: 'downloading',
78
+ downloaded_bytes: downloadedBytes,
79
+ fragment_index: i + 1,
80
+ fragment_count: fragments.length,
81
+ elapsed: (Date.now() - startTime) / 1000,
82
+ speed: this.calcSpeed(startTime, Date.now(), downloadedBytes) ?? undefined,
83
+ });
84
+ }
85
+ catch (err) {
86
+ this._log(`Fragment ${i + 1}/${fragments.length} failed: ${err.message}`);
87
+ // Continue with next fragment
88
+ }
89
+ }
90
+ await new Promise((resolve, reject) => {
91
+ stream.end(() => {
92
+ try {
93
+ if (fs.existsSync(filename))
94
+ fs.unlinkSync(filename);
95
+ fs.renameSync(tmpFilename, filename);
96
+ resolve();
97
+ }
98
+ catch {
99
+ try {
100
+ fs.copyFileSync(tmpFilename, filename);
101
+ fs.unlinkSync(tmpFilename);
102
+ resolve();
103
+ }
104
+ catch (err) {
105
+ reject(err);
106
+ }
107
+ }
108
+ });
109
+ });
110
+ this.reportFinished(filename, downloadedBytes);
111
+ return true;
112
+ }
113
+ _parseMediaPlaylist(manifest, baseUrl) {
114
+ const fragments = [];
115
+ const lines = manifest.split('\n');
116
+ let duration = 0;
117
+ for (let i = 0; i < lines.length; i++) {
118
+ const line = lines[i].trim();
119
+ if (line.startsWith('#EXTINF:')) {
120
+ const match = line.match(/#EXTINF:([\d.]+)/);
121
+ if (match)
122
+ duration = parseFloat(match[1]);
123
+ }
124
+ else if (line && !line.startsWith('#')) {
125
+ const fragUrl = line.startsWith('http') ? line : new URL(line, baseUrl).toString();
126
+ fragments.push({ url: fragUrl, duration });
127
+ duration = 0;
128
+ }
129
+ }
130
+ return fragments;
131
+ }
132
+ }
133
+ exports.HlsFD = HlsFD;
134
+ //# sourceMappingURL=hls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hls.js","sourceRoot":"","sources":["../../src/downloader/hls.ts"],"names":[],"mappings":"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"}