hlsdownloader 5.0.1 โ†’ 5.0.3

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 CHANGED
@@ -74,6 +74,19 @@ const summary = await downloader.startDownload();
74
74
  console.log(`Downloaded ${summary.total} segments to ${summary.path}`);
75
75
  ```
76
76
 
77
+ ### Using as CDN Primer
78
+
79
+ ```ts
80
+ import HLSDowloader from 'hlsdownloader';
81
+
82
+ const downloader = new HLSDownloader({
83
+ playlistURL: '[https://example.com/stream/master.m3u8](https://example.com/stream/master.m3u8)',
84
+ });
85
+
86
+ const summary = await downloader.startDownload();
87
+ console.log(`Fetching ${summary.total} segments to Edge servers`);
88
+ ```
89
+
77
90
  ### Advanced Usage
78
91
 
79
92
  ```ts
@@ -81,9 +94,9 @@ import { HLSDownloader } from 'hlsdownloader-ts';
81
94
 
82
95
  const downloader = new HLSDownloader({
83
96
  playlistURL: 'https://example.com/video.m3u8',
84
- destination: './output',
85
- concurrency: 10, // 10 simultaneous downloads
86
- overwrite: true, // Overwrite existing files
97
+ concurrency: 10, // 10 simultaneous downloads (optional: 1)
98
+ destination: './output', // path to downlod (optional: '')
99
+ overwrite: true, // Overwrite existing files. (optional: false)
87
100
  });
88
101
 
89
102
  const { total, errors } = await downloader.startDownload();
@@ -120,9 +133,9 @@ The library is organized under the `HLSDownloader` module. For full interactive
120
133
 
121
134
  The main service orchestrator for fetching HLS content.
122
135
 
123
- | Method | Returns | Description |
124
- | ----------------- | -------------------------- | ------------------------------------- |
125
- | `startDownload()` | `Promise<DownloadSummary>` | Begins parsing and fetching segments. |
136
+ | Method | Returns | Description |
137
+ | --------------- | -------------------------- | ------------------------------------- |
138
+ | startDownload() | `Promise<DownloadSummary>` | Begins parsing and fetching segments. |
126
139
 
127
140
  ### DownloaderOptions (Interface)
128
141
 
@@ -139,9 +152,27 @@ The main service orchestrator for fetching HLS content.
139
152
 
140
153
  | Property | Type | Description | Description |
141
154
  | -------- | ----------------- | ------------------------------------- | ---------------------------------- |
142
- | `total` | `number` | Count of successfully saved segments. | The absolute URL to the M3U8 file. |
143
- | `path` | `string` | The final output directory. | Local path to save files. |
144
- | `errors` | `DownloadError[]` | Array of detailed failure objects. | Max parallel network requests. |
155
+ | total | `number` | Count of successfully saved segments. | The absolute URL to the M3U8 file. |
156
+ | path | `string` | The final output directory. | Local path to save files. |
157
+ | errors | `DownloadError[]` | Array of detailed failure objects. | Max parallel network requests. |
158
+
159
+ ### SegmentDownloadedData (Interface) - `onData` Hook
160
+
161
+ | Property | Type | Description |
162
+ | -------- | -------- | -------------------------------------------------------------------------- |
163
+ | url | `string` | Original segment URL as referenced in the HLS playlist (`.m3u8`). |
164
+ | path | `string` | Local file system path where the segment was saved. Empty if not provided. |
165
+ | total | `number` | Total number of segments downloaded so far, including this one. |
166
+
167
+ ---
168
+
169
+ ### SegmentDownloadErrorData (Interface) - `onError` Hook
170
+
171
+ | Property | Type | Description |
172
+ | -------- | -------- | ---------------------------------------------------------- |
173
+ | url | `string` | Original segment URL that failed to download. |
174
+ | name | `string` | Error name or type (e.g., `NetworkError`, `TimeoutError`). |
175
+ | message | `string` | Human-readable error description. |
145
176
 
146
177
  ## Development & Contributing
147
178
 
@@ -149,18 +180,18 @@ Contributions are welcome! This project enforces strict quality standards to mai
149
180
 
150
181
  ### Prerequisites
151
182
 
152
- - Node.js >= 20.0.0
153
- - npm >= 10.0.0
183
+ - [Node.js](https://nodejs.org/) >= 20.0.0
184
+ - [npm](https://github.com/npm/cli) >= 10.0.0
154
185
 
155
186
  ### Workflow
156
187
 
157
188
  Fork & Clone: Get the repo locally.
158
189
 
159
- - Install: `npm install`
160
- - Lint: `npm run lint` (Must pass without warnings)
161
- - Test: `npm run test:cov` (Must maintain 100% coverage)
162
- - Build: `npm run build` (Generates `./dist` and bundled types)
163
- - Docs: `npm run docs` (Generates TypeDoc HTML)
190
+ - `Install`: `npm install`
191
+ - `Lint`: `npm run lint` (Must pass without warnings)
192
+ - `Test`: `npm run test:coverage` (Must maintain 100% coverage)
193
+ - `Build`: `npm run build` (Generates `./dist` and bundled types)
194
+ - `Docs`: `npm run docs` (Generates TypeDoc HTML)
164
195
 
165
196
  ### Guidelines
166
197
 
package/dist/index.d.ts CHANGED
@@ -5,21 +5,23 @@ export interface DownloadError {
5
5
  name: string;
6
6
  message: string;
7
7
  }
8
+ export interface SegmentDownloadedData {
9
+ url: string;
10
+ path?: string;
11
+ total: number;
12
+ }
13
+ export interface SegmentDownloadErrorData {
14
+ url: string;
15
+ name: string;
16
+ message: string;
17
+ }
8
18
  export interface DownloaderOptions {
9
19
  playlistURL: string;
10
20
  destination?: string;
11
21
  overwrite?: boolean;
12
22
  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
+ onData?: (data: SegmentDownloadedData) => void;
24
+ onError?: (error: SegmentDownloadErrorData) => void;
23
25
  [key: string]: any;
24
26
  }
25
27
  export interface DownloadSummary {
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{cpus as k}from"node:os";import F from"p-limit";import{URL as w}from"node:url";var m=class s extends Error{constructor(t){super(t),this.name=this.constructor.name,Object.setPrototypeOf(this,s.prototype),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}},g=m;var o=class{static isValidUrl(t,r=["http:","https:","ftp:","sftp:"]){let{protocol:e}=new w(t);if(e&&!r.includes(e))throw new g(`${e} is not supported. Supported protocols are ${r.join(", ")}`);return!0}static stripFirstSlash(t){return t.startsWith("/")?t.substring(1):t}static isValidPlaylist(t){return/^#EXTM3U/im.test(t)}static parseUrl(t){return new w(t)}static omit(t,...r){let e=new Set(r.flat());return Object.fromEntries(Object.entries(t).filter(([a])=>!e.has(a)))}static isNotFunction(t){return typeof t!="function"}};import{constants as L,createWriteStream as U}from"node:fs";import*as n from"node:fs/promises";import S from"node:path";import{Readable as R}from"node:stream";import{pipeline as T}from"node:stream/promises";var h=class{destination;overwrite;constructor(t,r=!1){this.destination=t,this.overwrite=r}async getTargetPath(t){let{pathname:r}=o.parseUrl(t);return S.join(this.destination,o.stripFirstSlash(r))}async prepareDirectory(t){let r=await this.getTargetPath(t),e=S.dirname(r);return await n.mkdir(e,{recursive:!0}),r}async canWrite(t){try{let r=await this.getTargetPath(t);return await n.access(r,L.F_OK),this.overwrite}catch(r){if(r.code==="ENOENT")return!0;throw r}}async saveStream(t,r){let e=R.fromWeb(t),a=U(r);try{await T(e,a)}catch(i){throw await n.unlink(r).catch(()=>{}),i}}},v=h;import E from"ky";var u=class s extends Error{constructor(t){super(t),this.name=this.constructor.name,Object.setPrototypeOf(this,s.prototype),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}},D=u;var d=class s{options;static defaultKyOptions={retry:{limit:0}};static unSupportedOptions=["uri","url","json","form","body","method","setHost","isStream","parseJson","prefixUrl","cookieJar","playlistURL","concurrency","allowGetBody","stringifyJson","methodRewriting"];constructor(t={}){this.options=Object.assign({},s.defaultKyOptions,o.omit(t,...s.unSupportedOptions))}async fetchText(t){let r=await E.get(t,{...this.options}).text();if(!o.isValidPlaylist(r))throw new D("Invalid playlist");return r}async getStream(t){let r=await E.get(t,{...this.options});if(!r.body)throw new Error("Response body is null");return r.body}},P=d;import{URL as x}from"node:url";var y=class s{static HLS_PLAYLIST_EXT=".m3u8";parse(t,r){return r.replace(/^#[\s\S].*/gim,"").split(/\r?\n/).filter(e=>e.trim()!=="").map(e=>new x(e,t).href)}isPlaylist(t){return t.toLowerCase().endsWith(s.HLS_PLAYLIST_EXT)}},c=new y;var f=class{constructor(t){this.options=t;let{onData:r,onError:e,destination:a="",playlistURL:i="",overwrite:p=!1,concurrency:l=Math.max(1,k().length-1),...O}=t||{};this.onData=r,this.onError=e,this.playlistURL=i,this.pool=F(l),this.concurrency=l,this.http=new P(O),this.fileService=new v(a,p),this.items=[i]}playlistURL;onData;onError;pool;http;fileService;items;errors=[];concurrency=1;async startDownload(){try{o.isValidUrl(this.playlistURL);let t=await this.http.fetchText(this.playlistURL),r=c.parse(this.playlistURL,t);this.items.push(...r);let e=r.filter(i=>c.isPlaylist(i));return(await Promise.allSettled(e.map(i=>this.http.fetchText(i)))).forEach((i,p)=>{if(i.status==="fulfilled"){let l=c.parse(e[p],i.value);this.items.push(...l)}}),await this.processQueue(),this.generateSummary()}catch(t){return this.handleError(this.playlistURL,t),this.generateSummary()}}async processQueue(){if(this.fileService.destination){if(!await this.fileService.canWrite(this.playlistURL))throw new Error("Directory already exists and overwrite is disabled");let r=this.items.map(e=>this.pool(()=>this.downloadFile(e)));return Promise.allSettled(r)}let t=this.items.map(r=>this.pool(async()=>{try{let e=await this.http.getStream(r);return this.onData&&this.onData({url:r,total:this.items.length}),e}catch(e){this.handleError(r,e)}}));return Promise.allSettled(t)}async downloadFile(t){try{let r=await this.http.getStream(t),e=await this.fileService.prepareDirectory(t);await this.fileService.saveStream(r,e),this.onData&&this.onData({url:t,path:e,total:this.items.length})}catch(r){this.handleError(t,r)}}handleError(t,r){let e={url:t,name:r.name,message:r.message};this.errors.push(e),this.onError&&this.onError(e)}generateSummary(){return{errors:this.errors,total:this.items.length,message:this.errors.length>0?"Download ended with errors":"Downloaded successfully"}}},b=f;var ct=b;export{b as HLSDownloader,ct as default};
1
+ import{cpus as k}from"node:os";import F from"p-limit";import{URL as w}from"node:url";var m=class s extends Error{constructor(t){super(t),this.name=this.constructor.name,Object.setPrototypeOf(this,s.prototype),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}},f=m;var a=class{static isValidUrl(t,r=["http:","https:","ftp:","sftp:"]){let{protocol:e}=new w(t);if(e&&!r.includes(e))throw new f(`${e} is not supported. Supported protocols are ${r.join(", ")}`);return!0}static stripFirstSlash(t){return t.startsWith("/")?t.substring(1):t}static isValidPlaylist(t){return/^#EXTM3U/im.test(t)}static parseUrl(t){return new w(t)}static omit(t,...r){let e=new Set(r.flat());return Object.fromEntries(Object.entries(t).filter(([o])=>!e.has(o)))}static isNotFunction(t){return typeof t!="function"}};import{constants as L,createWriteStream as U}from"node:fs";import*as n from"node:fs/promises";import S from"node:path";import{Readable as R}from"node:stream";import{pipeline as T}from"node:stream/promises";var h=class{destination;overwrite;constructor(t,r=!1){this.destination=t,this.overwrite=r}async getTargetPath(t){let{pathname:r}=a.parseUrl(t);return S.join(this.destination,a.stripFirstSlash(r))}async prepareDirectory(t){let r=await this.getTargetPath(t),e=S.dirname(r);return await n.mkdir(e,{recursive:!0}),r}async canWrite(t){try{let r=await this.getTargetPath(t);return await n.access(r,L.F_OK),this.overwrite}catch(r){if(r.code==="ENOENT")return!0;throw r}}async saveStream(t,r){let e=R.fromWeb(t),o=U(r);try{await T(e,o)}catch(i){throw await n.unlink(r).catch(()=>{}),i}}},D=h;import E from"ky";var d=class s extends Error{constructor(t){super(t),this.name=this.constructor.name,Object.setPrototypeOf(this,s.prototype),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}},v=d;var u=class s{options;static defaultKyOptions={retry:{limit:0}};static unSupportedOptions=["uri","url","json","form","body","method","setHost","isStream","parseJson","prefixUrl","cookieJar","playlistURL","concurrency","allowGetBody","stringifyJson","methodRewriting"];constructor(t={}){this.options=Object.assign({},s.defaultKyOptions,a.omit(t,...s.unSupportedOptions))}async fetchText(t){let r=await E.get(t,{...this.options}).text();if(!a.isValidPlaylist(r))throw new v("Invalid playlist");return r}async getStream(t){let r=await E.get(t,{...this.options});if(!r.body)throw new Error("Response body is null");return r.body}},P=u;import{URL as x}from"node:url";var g=class s{static HLS_PLAYLIST_EXT=".m3u8";parse(t,r){return r.replace(/^#[\s\S].*/gim,"").split(/\r?\n/).filter(e=>e.trim()!=="").map(e=>new x(e,t).href)}isPlaylist(t){return t.toLowerCase().endsWith(s.HLS_PLAYLIST_EXT)}},c=new g;var y=class{constructor(t){this.options=t;let{onData:r,onError:e,destination:o="",playlistURL:i="",overwrite:p=!1,concurrency:l=Math.max(1,k().length-1),...O}=t||{};this.onData=r,this.onError=e,this.playlistURL=i,this.pool=F(l),this.concurrency=l,this.http=new P(O),this.fileService=new D(o,p),this.items=[i]}playlistURL;onData;onError;pool;http;fileService;items;errors=[];concurrency=1;async startDownload(){try{a.isValidUrl(this.playlistURL);let t=await this.http.fetchText(this.playlistURL),r=c.parse(this.playlistURL,t);this.items.push(...r);let e=r.filter(i=>c.isPlaylist(i));return(await Promise.allSettled(e.map(i=>this.http.fetchText(i)))).forEach((i,p)=>{if(i.status==="fulfilled"){let l=c.parse(e[p],i.value);this.items.push(...l)}}),await this.processQueue(),this.generateSummary()}catch(t){return this.handleError(this.playlistURL,t),this.generateSummary()}}async processQueue(){let t=this.items.length;if(this.fileService.destination){if(!await this.fileService.canWrite(this.playlistURL))throw new Error("Directory already exists and overwrite is disabled");let e=this.items.map(o=>this.pool(()=>this.downloadFile(o)));return Promise.allSettled(e)}let r=this.items.map(e=>this.pool(async()=>{try{let o=await this.http.getStream(e);return this.onData&&this.onData({url:e,total:t}),o}catch(o){this.handleError(e,o)}}));return Promise.allSettled(r)}async downloadFile(t){try{let r=await this.http.getStream(t),e=await this.fileService.prepareDirectory(t);await this.fileService.saveStream(r,e),this.onData&&this.onData({url:t,path:e,total:this.items.length})}catch(r){this.handleError(t,r)}}handleError(t,r){let e={url:t,name:r.name,message:r.message};this.errors.push(e),this.onError&&this.onError(e)}generateSummary(){return{errors:this.errors,total:this.items.length,message:this.errors.length>0?"Download ended with errors":"Downloaded successfully"}}},b=y;var mt=b;export{b as HLSDownloader,mt as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hlsdownloader",
3
- "version": "5.0.1",
3
+ "version": "5.0.3",
4
4
  "description": "Downloads HLS Playlist file and TS chunks",
5
5
  "snyk": true,
6
6
  "main": "./dist/index.js",
@@ -26,7 +26,7 @@
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
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 '๐Ÿ—๏ธ Compile: Bundles ESM source and generates minified distribution files.' && npm run typecheck && rimraf -fr ./dist && npm run build:types && npm run build:bundle",
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": {
@@ -36,25 +36,25 @@
36
36
  "devDependencies": {
37
37
  "@babel/core": "^7.29.0",
38
38
  "@babel/preset-typescript": "^7.28.5",
39
- "@commitlint/cli": "^20.4.1",
40
- "@commitlint/config-conventional": "^20.4.1",
41
- "@eslint/js": "^9.39.2",
42
- "@types/node": "^25.2.3",
43
- "@types/web": "^0.0.331",
44
- "c8": "^10.1.3",
39
+ "@commitlint/cli": "^20.4.2",
40
+ "@commitlint/config-conventional": "^20.4.2",
41
+ "@eslint/js": "^10.0.1",
42
+ "@types/node": "^25.3.0",
43
+ "@types/web": "^0.0.338",
44
+ "c8": "^11.0.0",
45
45
  "clean-jsdoc-theme": "^4.3.0",
46
46
  "cz-conventional-changelog": "^3.3.0",
47
47
  "dts-bundle-generator": "^9.5.1",
48
- "eslint": "^9.39.2",
49
- "eslint-plugin-jsdoc": "^62.5.4",
48
+ "eslint": "^10.0.1",
49
+ "eslint-plugin-jsdoc": "^62.7.0",
50
50
  "jsdoc": "^4.0.5",
51
51
  "jsdoc-babel": "^0.5.0",
52
- "lefthook": "^2.1.0",
53
- "rimraf": "^6.1.2",
52
+ "lefthook": "^2.1.1",
53
+ "rimraf": "^6.1.3",
54
54
  "semantic-release": "^25.0.3",
55
55
  "tsx": "^4.21.0",
56
56
  "typescript": "^5.9.3",
57
- "typescript-eslint": "^8.55.0"
57
+ "typescript-eslint": "^8.56.1"
58
58
  },
59
59
  "engines": {
60
60
  "node": ">=20.0.0",