hlsdownloader 5.0.1 โ 6.0.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/README.md +147 -67
- package/dist/index.d.ts +35 -15
- package/dist/index.js +1 -1
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -39,15 +39,16 @@ Downloads HLS Playlist file and TS chunks. You can use it for content pre-fetchi
|
|
|
39
39
|
|
|
40
40
|
## Features
|
|
41
41
|
|
|
42
|
+
- **Event Based:** Event based API
|
|
42
43
|
- **Modern ESM**: Optimized for Node.js 20+ environments using native modules.
|
|
43
|
-
- **
|
|
44
|
+
- **TypeScript Native:** Built with strong typing for mission-critical applications.
|
|
45
|
+
- **Resilient Networking and Retryable**: Built-in resilience that automatically retries with exponential backoff of failed segment requests to ensure download completion.
|
|
44
46
|
- **Promise Based**: Fully asynchronous API designed for seamless integration with `async/await` and modern control flows.
|
|
45
47
|
- **Support for HTTP/2**: Leverages multiplexing to download multiple segments over a single connection for reduced overhead.
|
|
46
48
|
- **Overwrite Protection**: Safeguards your local data by preventing accidental overwriting of existing files unless explicitly enabled.
|
|
47
49
|
- **Support for Custom HTTP Headers**: Allows injection of custom headers for handling authentication, user-agents, or session tokens.
|
|
48
|
-
- **Support for Custom HTTP Client**: Modular architecture that lets you swap the default engine for any custom client implementation.
|
|
49
|
-
- **Bring Your Own Progress Bar**: Exposed event hooks and lifecycle data allow you to hook in any CLI or GUI progress visualization.
|
|
50
50
|
- **Concurrent Downloads**: Maximizes bandwidth by fetching multiple HLS segments simultaneously through parallel HTTP connections.
|
|
51
|
+
- **Proxy and NoProxy Support:** Proxy support and No Proxy support (undici integration).
|
|
51
52
|
- **Professional Docs**: Integrated JSDoc-to-HTML pipeline using TypeDoc and the Fresh theme.
|
|
52
53
|
|
|
53
54
|
---
|
|
@@ -60,57 +61,109 @@ npm install hlsdownloader
|
|
|
60
61
|
|
|
61
62
|
## Examples
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
#### Example 1: Basic Download with Event Monitoring
|
|
65
|
+
|
|
66
|
+
This is the standard implementation for saving a remote HLS stream to a local directory.
|
|
64
67
|
|
|
65
68
|
```ts
|
|
66
|
-
import
|
|
69
|
+
import { HLSDownloader } from 'hlsdownloader';
|
|
70
|
+
|
|
71
|
+
async function downloadStream() {
|
|
72
|
+
const downloader = new HLSDownloader({
|
|
73
|
+
playlistURL: 'https://example.com/video/master.m3u8',
|
|
74
|
+
destination: './downloads/my-video',
|
|
75
|
+
overwrite: true,
|
|
76
|
+
concurrency: 5, // Process 5 segments simultaneously
|
|
77
|
+
retry: { limit: 3, delay: 1000 },
|
|
78
|
+
timeout: 15000,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
downloader.on('start', ({ total }) => console.log(`start downloading assets...`));
|
|
82
|
+
|
|
83
|
+
// Listen to progress updates
|
|
84
|
+
downloader.on('progress', data => {
|
|
85
|
+
const percent = ((data.total / data.total) * 100).toFixed(2); // Simple logic for example
|
|
86
|
+
console.log(`[Progress] Downloaded: ${data.url}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Handle errors for specific segments
|
|
90
|
+
downloader.on('error', err => {
|
|
91
|
+
console.error(`[Segment Error] Failed to fetch ${err.url}: ${err.message}`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Final summary
|
|
95
|
+
const summary = await downloader.startDownload();
|
|
96
|
+
console.log(`Finished! Total items: ${summary.total}. Errors: ${summary.errors.length}`);
|
|
97
|
+
}
|
|
67
98
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
destination: './downloads/my-video',
|
|
71
|
-
});
|
|
99
|
+
downloadStream();
|
|
100
|
+
```
|
|
72
101
|
|
|
73
|
-
|
|
74
|
-
|
|
102
|
+
#### Example 2: "Dry-Run" / CDN Priming (No File Writing)
|
|
103
|
+
|
|
104
|
+
If the destination is omitted, the library fetches streams but doesn't write to disk. This is excellent for `CDN Priming` or `validating manifest health`.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { HLSDownloader } from 'hlsdownloader';
|
|
108
|
+
|
|
109
|
+
async function primeCDN() {
|
|
110
|
+
const downloader = new HLSDownloader({
|
|
111
|
+
playlistURL: 'https://cdn.provider.com/live/stream.m3u8',
|
|
112
|
+
// destination is omitted -> results in memory-only stream fetch
|
|
113
|
+
concurrency: 10,
|
|
114
|
+
headers: {
|
|
115
|
+
Authorization: 'Bearer internal-token-123',
|
|
116
|
+
'X-Custom-Source': 'Prewarm-Service',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
downloader.on('start', ({ total }) => console.log(`Priming ${total} assets...`));
|
|
121
|
+
|
|
122
|
+
const result = await downloader.startDownload();
|
|
123
|
+
|
|
124
|
+
if (result.errors.length === 0) {
|
|
125
|
+
console.log('CDN Cache successfully warmed.');
|
|
126
|
+
} else {
|
|
127
|
+
console.error('Priming failed for some chunks', result.errors);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
primeCDN();
|
|
75
132
|
```
|
|
76
133
|
|
|
77
|
-
|
|
134
|
+
#### Example 3: Corporate Proxy & Advanced Networking
|
|
135
|
+
|
|
136
|
+
If you are using behind corporate proxy, pass the proxy and no proxy configuration as follows
|
|
78
137
|
|
|
79
138
|
```ts
|
|
80
|
-
import { HLSDownloader } from 'hlsdownloader
|
|
139
|
+
import { HLSDownloader } from 'hlsdownloader';
|
|
81
140
|
|
|
82
141
|
const downloader = new HLSDownloader({
|
|
83
|
-
playlistURL: 'https://
|
|
84
|
-
destination: '
|
|
85
|
-
|
|
86
|
-
|
|
142
|
+
playlistURL: 'https://secure-stream.corp.internal/index.m3u8',
|
|
143
|
+
destination: '/mnt/storage/archive',
|
|
144
|
+
proxy: 'http://proxy.corporate.net:8080',
|
|
145
|
+
noProxy: '.internal.com,localhost', // Bypass proxy for internal domains
|
|
146
|
+
headers: {
|
|
147
|
+
Cookie: 'session_id=abc123',
|
|
148
|
+
'User-Agent': 'MediaArchiver/1.0',
|
|
149
|
+
},
|
|
87
150
|
});
|
|
88
151
|
|
|
89
|
-
|
|
152
|
+
downloader.on('start', ({ total }) => console.log(`Priming ${total} assets...`));
|
|
90
153
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
154
|
+
// Listen to progress updates
|
|
155
|
+
downloader.on('progress', data => {
|
|
156
|
+
const percent = ((data.total / data.total) * 100).toFixed(2); // Simple logic for example
|
|
157
|
+
console.log(`[Progress] Downloaded: ${data.url}`);
|
|
158
|
+
});
|
|
95
159
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
- method
|
|
104
|
-
- setHost
|
|
105
|
-
- isStream
|
|
106
|
-
- parseJson
|
|
107
|
-
- prefixUrl
|
|
108
|
-
- cookieJar
|
|
109
|
-
- playlistURL
|
|
110
|
-
- concurrency
|
|
111
|
-
- allowGetBody
|
|
112
|
-
- stringifyJson
|
|
113
|
-
- methodRewriting
|
|
160
|
+
// Handle errors for specific segments
|
|
161
|
+
downloader.on('error', err => {
|
|
162
|
+
console.error(`[Segment Error] Failed to fetch ${err.url}: ${err.message}`);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const summary = await downloader.startDownload();
|
|
166
|
+
```
|
|
114
167
|
|
|
115
168
|
## API Documentation
|
|
116
169
|
|
|
@@ -120,28 +173,58 @@ The library is organized under the `HLSDownloader` module. For full interactive
|
|
|
120
173
|
|
|
121
174
|
The main service orchestrator for fetching HLS content.
|
|
122
175
|
|
|
123
|
-
| Method
|
|
124
|
-
|
|
|
125
|
-
|
|
|
176
|
+
| Method | Returns | Description |
|
|
177
|
+
| --------------- | -------------------------- | ------------------------------------- |
|
|
178
|
+
| startDownload() | `Promise<DownloadSummary>` | Begins parsing and fetching segments. |
|
|
126
179
|
|
|
127
180
|
### DownloaderOptions (Interface)
|
|
128
181
|
|
|
129
|
-
|
|
|
130
|
-
| ----------- |
|
|
131
|
-
| playlistURL | `string`
|
|
132
|
-
| destination | `string`
|
|
133
|
-
| concurrency | `number`
|
|
134
|
-
| overwrite | `boolean`
|
|
135
|
-
|
|
|
136
|
-
|
|
|
182
|
+
| Option | Type | Default | Description |
|
|
183
|
+
| ----------- | --------- | ------------------------ | ------------------------------------------------------------------------------------------ |
|
|
184
|
+
| playlistURL | `string` | Required | The source .m3u8 URL. |
|
|
185
|
+
| destination | `string` | undefined | Local path to save files. Omit for "dry-run" mode. |
|
|
186
|
+
| concurrency | `number` | os.cpus().length | Simultaneous segment downloads. |
|
|
187
|
+
| overwrite | `boolean` | false | Overwrite existing files in the destination. |
|
|
188
|
+
| headers | `object` | {} | Custom headers to pass |
|
|
189
|
+
| timeout | `number` | 10000 | Network request timeout in ms. |
|
|
190
|
+
| retry | `object` | { limit: 1, delay: 500 } | Exponential backoff settings. |
|
|
191
|
+
| proxy | `string` | undefined | Corporate proxy URL. Also reads URLs for `HTTP_PROXY`, `HTTPS_PROXY` environment variables |
|
|
192
|
+
| noProxy | `string` | undefined | Corporate No Proxy Urls. Also reads urls from `NO_PROXY` environment vriable |
|
|
193
|
+
|
|
194
|
+
### DownloaderEvents (Interface)
|
|
195
|
+
|
|
196
|
+
| Event Name | Description |
|
|
197
|
+
| ---------- | -------------------------------------------- |
|
|
198
|
+
| start | emits when download started |
|
|
199
|
+
| progress | emits for each segement downloded |
|
|
200
|
+
| error | emits when a segement downlod error occurred |
|
|
201
|
+
| end | emits when download ended |
|
|
137
202
|
|
|
138
203
|
### DownloadSummary (Interface)
|
|
139
204
|
|
|
140
|
-
| Property | Type | Description |
|
|
141
|
-
| -------- | ----------------- | ------------------------------------- |
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
|
|
|
205
|
+
| Property | Type | Description |
|
|
206
|
+
| -------- | ----------------- | ------------------------------------- |
|
|
207
|
+
| total | `number` | Count of successfully saved segments. |
|
|
208
|
+
| message | `string` | User friendly message. |
|
|
209
|
+
| errors | `DownloadError[]` | Array of detailed failure objects. |
|
|
210
|
+
|
|
211
|
+
### SegmentDownloadedData (Interface) - emits on `progress` events
|
|
212
|
+
|
|
213
|
+
| Property | Type | Description |
|
|
214
|
+
| -------- | -------- | -------------------------------------------------------------------------- |
|
|
215
|
+
| url | `string` | Original segment URL as referenced in the HLS playlist (`.m3u8`). |
|
|
216
|
+
| path | `string` | Local file system path where the segment was saved. Empty if not provided. |
|
|
217
|
+
| total | `number` | Total number of segments downloaded so far, including this one. |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### SegmentDownloadErrorData (Interface) - emits on `error` events
|
|
222
|
+
|
|
223
|
+
| Property | Type | Description |
|
|
224
|
+
| -------- | -------- | ---------------------------------------------------------- |
|
|
225
|
+
| url | `string` | Original segment URL that failed to download. |
|
|
226
|
+
| name | `string` | Error name or type (e.g., `NetworkError`, `TimeoutError`). |
|
|
227
|
+
| message | `string` | Human-readable error description. |
|
|
145
228
|
|
|
146
229
|
## Development & Contributing
|
|
147
230
|
|
|
@@ -149,18 +232,19 @@ Contributions are welcome! This project enforces strict quality standards to mai
|
|
|
149
232
|
|
|
150
233
|
### Prerequisites
|
|
151
234
|
|
|
152
|
-
- Node.js >= 20.0.0
|
|
153
|
-
- npm >= 10.0.0
|
|
235
|
+
- [Node.js](https://nodejs.org/) >= 20.0.0
|
|
236
|
+
- [npm](https://github.com/npm/cli) >= 10.0.0
|
|
154
237
|
|
|
155
238
|
### Workflow
|
|
156
239
|
|
|
157
240
|
Fork & Clone: Get the repo locally.
|
|
158
241
|
|
|
159
|
-
- Install
|
|
160
|
-
-
|
|
161
|
-
-
|
|
162
|
-
- Build
|
|
163
|
-
- Docs
|
|
242
|
+
- `Install`: `npm install`
|
|
243
|
+
- `Test`: `npm run test` (Must pass without warnings)
|
|
244
|
+
- `Lint`: `npm run lint` (Must pass without warnings)
|
|
245
|
+
- `Build`: `npm run build` (Generates `./dist` and bundled types)
|
|
246
|
+
- `Docs`: `npm run docs` (Generates TypeDoc HTML)
|
|
247
|
+
- `Test with Coverage Report`: `npm run test:coverage` (Must maintain 100% coverage)
|
|
164
248
|
|
|
165
249
|
### Guidelines
|
|
166
250
|
|
|
@@ -174,10 +258,6 @@ Contributions, issues and feature requests are welcome!<br />Feel free to check
|
|
|
174
258
|
|
|
175
259
|
Give a โญ๏ธ if this project helped you!. I will be grateful if you all help me to improve this package by giving your suggestions, feature request and pull requests. I am all ears!!
|
|
176
260
|
|
|
177
|
-
## Special Thanks to
|
|
178
|
-
|
|
179
|
-
- [Ky Team](https://www.npmjs.com/package/ky)
|
|
180
|
-
|
|
181
261
|
## License
|
|
182
262
|
|
|
183
263
|
Copyright ยฉ 2026 [Nur Rony](https://github.com/nurrony).<br />
|
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,46 @@
|
|
|
1
1
|
// Generated by dts-bundle-generator v9.5.1
|
|
2
2
|
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
|
|
5
|
+
export interface HttpClientOptions {
|
|
6
|
+
timeout?: number;
|
|
7
|
+
retry?: {
|
|
8
|
+
limit: number;
|
|
9
|
+
delay: number;
|
|
10
|
+
};
|
|
11
|
+
proxy?: string;
|
|
12
|
+
noProxy?: string[];
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export interface DownloaderEvents {
|
|
16
|
+
start: (data: {
|
|
17
|
+
total: number;
|
|
18
|
+
destination: string;
|
|
19
|
+
}) => void;
|
|
20
|
+
progress: (data: SegmentDownloadedData) => void;
|
|
21
|
+
error: (error: DownloadError) => void;
|
|
22
|
+
end: (summary: DownloadSummary) => void;
|
|
23
|
+
}
|
|
3
24
|
export interface DownloadError {
|
|
4
25
|
url: string;
|
|
5
26
|
name: string;
|
|
6
27
|
message: string;
|
|
7
28
|
}
|
|
8
|
-
export interface
|
|
29
|
+
export interface SegmentDownloadedData {
|
|
30
|
+
url: string;
|
|
31
|
+
path?: string;
|
|
32
|
+
total: number;
|
|
33
|
+
}
|
|
34
|
+
export interface SegmentDownloadErrorData {
|
|
35
|
+
url: string;
|
|
36
|
+
name: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
export interface DownloaderOptions extends HttpClientOptions {
|
|
9
40
|
playlistURL: string;
|
|
10
41
|
destination?: string;
|
|
11
42
|
overwrite?: boolean;
|
|
12
43
|
concurrency?: number;
|
|
13
|
-
onData?: (data: {
|
|
14
|
-
url: string;
|
|
15
|
-
path?: string;
|
|
16
|
-
total: number;
|
|
17
|
-
}) => void;
|
|
18
|
-
onError?: (error: {
|
|
19
|
-
url: string;
|
|
20
|
-
name: string;
|
|
21
|
-
message: string;
|
|
22
|
-
}) => void;
|
|
23
44
|
[key: string]: any;
|
|
24
45
|
}
|
|
25
46
|
export interface DownloadSummary {
|
|
@@ -27,19 +48,18 @@ export interface DownloadSummary {
|
|
|
27
48
|
errors: DownloadError[];
|
|
28
49
|
message: string;
|
|
29
50
|
}
|
|
30
|
-
declare class Downloader {
|
|
51
|
+
declare class Downloader extends EventEmitter {
|
|
31
52
|
private options;
|
|
53
|
+
private items;
|
|
32
54
|
private playlistURL;
|
|
33
|
-
private onData;
|
|
34
|
-
private onError;
|
|
35
55
|
private pool;
|
|
36
56
|
private http;
|
|
37
57
|
private fileService;
|
|
38
|
-
private items;
|
|
39
58
|
private errors;
|
|
40
59
|
private concurrency;
|
|
41
60
|
constructor(options: DownloaderOptions);
|
|
42
61
|
startDownload(): Promise<DownloadSummary>;
|
|
62
|
+
emit(event: string | symbol, ...args: any[]): boolean;
|
|
43
63
|
private processQueue;
|
|
44
64
|
private downloadFile;
|
|
45
65
|
private handleError;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{EventEmitter as k}from"node:events";import{cpus as A}from"node:os";import C from"p-limit";import{URL as v}from"node:url";var h=class l extends Error{constructor(r){super(r),this.name=this.constructor.name,Object.setPrototypeOf(this,l.prototype),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}},f=h;var n=class{static isValidUrl(r,t=["http:","https:","ftp:","sftp:"]){let{protocol:e}=new v(r);if(e&&!t.includes(e))throw new f(`${e} is not supported. Supported protocols are ${t.join(", ")}`);return!0}static stripFirstSlash(r){return r.startsWith("/")?r.substring(1):r}static isValidPlaylist(r){return/^#EXTM3U/im.test(r)}static parseUrl(r){return new v(r)}static omit(r,...t){let e=new Set(t.flat());return Object.fromEntries(Object.entries(r).filter(([s])=>!e.has(s)))}static isNotFunction(r){return typeof r!="function"}static async sleep(r){return new Promise(t=>setTimeout(t,r))}};import{constants as x,createWriteStream as O}from"node:fs";import*as m from"node:fs/promises";import S from"node:path";import{Readable as L}from"node:stream";import{pipeline as T}from"node:stream/promises";var d=class{destination;overwrite;constructor(r,t=!1){this.destination=r,this.overwrite=t}async getTargetPath(r){let{pathname:t}=n.parseUrl(r);return S.join(this.destination,n.stripFirstSlash(t))}async prepareDirectory(r){let t=await this.getTargetPath(r),e=S.dirname(t);return await m.mkdir(e,{recursive:!0}),t}async canWrite(r){try{let t=await this.getTargetPath(r);return await m.access(t,x.F_OK),this.overwrite}catch(t){if(t.code==="ENOENT")return!0;throw t}}async saveStream(r,t){let e=L.fromWeb(r),s=O(t);try{await T(e,s)}catch(i){throw await m.unlink(t).catch(()=>{}),i}}},D=d;import{ProxyAgent as U}from"undici";var u=class l extends Error{constructor(r){super(r),this.name=this.constructor.name,Object.setPrototypeOf(this,l.prototype),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}},P=u;var y=class{options;primaryOrigin=null;sensitiveHeaders=["authorization","cookie","x-auth-token"];timeout;retryOptions;dispatcher;noProxy=[];constructor(r={}){if(this.options={method:"GET",headers:{"User-Agent":"HLSDownloader"}},r.headers){let s={};for(let[i,o]of Object.entries(r.headers))s[i.toLowerCase()]=o;this.options.headers={"User-Agent":"HLSDownloader",...this.options.headers,...s}}this.timeout=r.timeout??5e3,this.retryOptions=r.retry??{limit:1,delay:500};let t=process.env.NO_PROXY||process.env.no_proxy||"";this.noProxy=r.noProxy??t.split(",").map(s=>s.trim()).filter(Boolean);let e=r.proxy||process.env.HTTPS_PROXY||process.env.HTTP_PROXY;e&&(this.dispatcher=new U({uri:e}))}setPrimaryOrigin(r){try{this.primaryOrigin=new URL(r).origin}catch{this.primaryOrigin=null}}getRequestHeaders(r){let t={...this.options.headers};try{let e=new URL(r).origin;this.primaryOrigin&&e!==this.primaryOrigin&&this.sensitiveHeaders.forEach(s=>{delete t[s]})}catch{}return t}shouldBypassProxy(r){if(!this.dispatcher)return!0;let t=new URL(r).hostname;return this.noProxy.some(e=>t===e||e.startsWith(".")&&t.endsWith(e))}async requestWithRetry(r,t=0){let e=new AbortController,s=setTimeout(()=>e.abort(),this.timeout);try{let i=await fetch(r,{...this.options,headers:this.getRequestHeaders(r),signal:e.signal,dispatcher:this.shouldBypassProxy(r)?void 0:this.dispatcher});return clearTimeout(s),!i.ok&&(i.status>=500||i.status===429)&&t<this.retryOptions.limit?this.handleRetry(r,t):i}catch(i){if(clearTimeout(s),(i.name==="AbortError"||i.name==="TypeError"||i.status&&i.status>=500)&&t<this.retryOptions.limit)return this.handleRetry(r,t);throw i}}async handleRetry(r,t){let e=this.retryOptions.delay*Math.pow(2,t);return await n.sleep(e),this.requestWithRetry(r,t+1)}async fetchText(r){let e=await(await this.requestWithRetry(r)).text();if(!n.isValidPlaylist(e))throw new P("Invalid playlist");return e}async getStream(r){let t=await this.requestWithRetry(r);if(!t.body)throw new Error("Response body is null");return t.body}},b=y;import{URL as H}from"node:url";var g=class l{static HLS_PLAYLIST_EXT=".m3u8";parse(r,t){return t.replace(/^#[\s\S].*/gim,"").split(/\r?\n/).filter(e=>e.trim()!=="").map(e=>new H(e,r).href)}isPlaylist(r){return r.toLowerCase().endsWith(l.HLS_PLAYLIST_EXT)}},p=new g;var w=class extends k{constructor(t){super();this.options=t;let{destination:e="",playlistURL:s="",overwrite:i=!1,concurrency:o=Math.max(1,A().length-1),headers:a={},...c}=t||{};this.playlistURL=s,this.pool=C(o),this.concurrency=o,this.http=new b({headers:a,...c}),this.http.setPrimaryOrigin(this.playlistURL),this.fileService=new D(e,i),this.items.add(this.playlistURL)}items=new Set;playlistURL;pool;http;fileService;errors=[];concurrency=1;async startDownload(){try{n.isValidUrl(this.playlistURL);let t=await this.http.fetchText(this.playlistURL),e=p.parse(this.playlistURL,t);e.forEach(a=>this.items.add(a));let s=e.filter(a=>p.isPlaylist(a));(await Promise.allSettled(s.map(a=>this.http.fetchText(a)))).forEach((a,c)=>{a.status==="fulfilled"&&p.parse(s[c],a.value).forEach(E=>this.items.add(E))}),this.emit("start",{total:this.items.size,destination:this.options.destination}),await this.processQueue();let o=this.generateSummary();return this.emit("end",o),o}catch(t){return this.handleError(this.playlistURL,t),this.generateSummary()}}emit(t,...e){return super.emit(t,...e)}async processQueue(){let t=this.items.size,e=Array.from(this.items);if(this.fileService.destination){if(!await this.fileService.canWrite(this.playlistURL))throw new Error("Directory already exists and overwrite is disabled");let i=e.map(o=>this.pool(()=>this.downloadFile(o)));return Promise.allSettled(i)}let s=e.map(i=>this.pool(async()=>{try{let o=await this.http.getStream(i);return this.emit("progress",{url:i,total:t}),o}catch(o){this.handleError(i,o)}}));return Promise.allSettled(s)}async downloadFile(t){try{let e=await this.http.getStream(t),s=await this.fileService.prepareDirectory(t);await this.fileService.saveStream(e,s),this.emit("progress",{url:t,path:s,total:this.items.size})}catch(e){this.handleError(t,e)}}handleError(t,e){let s={url:t,name:e.name,message:e.message};this.errors.push(s),this.emit("error",s)}generateSummary(){return{errors:this.errors,total:this.items.size,message:this.errors.length>0?"Download ended with errors":"Downloaded successfully"}}},R=w;var gt=R;export{R as HLSDownloader,gt as default};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hlsdownloader",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Downloads HLS Playlist file and TS chunks",
|
|
5
5
|
"snyk": true,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,36 +25,36 @@
|
|
|
25
25
|
"prepublishOnly": "echo '๐ Release: Final automated validation gate before NPM publishing.' && npm run build",
|
|
26
26
|
"build:types": "echo '๐ท๏ธ Generating Types' && dts-bundle-generator -o dist/index.d.ts src/index.ts --no-check --silent",
|
|
27
27
|
"docs": "rimraf -fr ./docs && echo '๐งน All documentation has been cleaned.' && echo '๐ Generating documentation...' && jsdoc -c jsdoc.json",
|
|
28
|
-
"build:bundle": "esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --target=node20 --minify --tree-shaking=true --outfile=dist/index.js",
|
|
29
|
-
"build": "echo '
|
|
28
|
+
"build:bundle": "esbuild src/index.ts --bundle --packages=external --external:undici --platform=node --format=esm --target=node20 --minify --tree-shaking=true --outfile=dist/index.js",
|
|
29
|
+
"build": "echo '๐ฆ Bundles ESM source and generates minified distribution files.' && npm run typecheck && rimraf -fr ./dist && npm run build:types && npm run build:bundle",
|
|
30
30
|
"test:coverage": "rimraf -fr ./coverage && echo '๐งน All test coverage reports has been cleaned.' && c8 npm test && echo '๐ Coverage: Validates 100% code coverage across all source files successfully.'"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"ky": "^1.14.3",
|
|
34
33
|
"p-limit": "^7.3.0"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {
|
|
37
36
|
"@babel/core": "^7.29.0",
|
|
38
37
|
"@babel/preset-typescript": "^7.28.5",
|
|
39
|
-
"@commitlint/cli": "^20.4.
|
|
40
|
-
"@commitlint/config-conventional": "^20.4.
|
|
41
|
-
"@eslint/js": "^
|
|
42
|
-
"@types/node": "^25.2
|
|
43
|
-
"@types/web": "^0.0.
|
|
44
|
-
"c8": "^
|
|
38
|
+
"@commitlint/cli": "^20.4.2",
|
|
39
|
+
"@commitlint/config-conventional": "^20.4.2",
|
|
40
|
+
"@eslint/js": "^10.0.1",
|
|
41
|
+
"@types/node": "^25.3.2",
|
|
42
|
+
"@types/web": "^0.0.338",
|
|
43
|
+
"c8": "^11.0.0",
|
|
45
44
|
"clean-jsdoc-theme": "^4.3.0",
|
|
46
45
|
"cz-conventional-changelog": "^3.3.0",
|
|
47
46
|
"dts-bundle-generator": "^9.5.1",
|
|
48
|
-
"eslint": "^
|
|
49
|
-
"eslint-plugin-jsdoc": "^62.
|
|
47
|
+
"eslint": "^10.0.2",
|
|
48
|
+
"eslint-plugin-jsdoc": "^62.7.1",
|
|
50
49
|
"jsdoc": "^4.0.5",
|
|
51
50
|
"jsdoc-babel": "^0.5.0",
|
|
52
|
-
"lefthook": "^2.1.
|
|
53
|
-
"rimraf": "^6.1.
|
|
51
|
+
"lefthook": "^2.1.1",
|
|
52
|
+
"rimraf": "^6.1.3",
|
|
54
53
|
"semantic-release": "^25.0.3",
|
|
55
54
|
"tsx": "^4.21.0",
|
|
56
55
|
"typescript": "^5.9.3",
|
|
57
|
-
"typescript-eslint": "^8.
|
|
56
|
+
"typescript-eslint": "^8.56.1",
|
|
57
|
+
"undici": "^7.22.0"
|
|
58
58
|
},
|
|
59
59
|
"engines": {
|
|
60
60
|
"node": ">=20.0.0",
|