@khang07/zing-mp3-api 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,101 +1,85 @@
1
1
  # @khang07/zing-mp3-api
2
2
 
3
- Node.js library để lấy stream nhạc và video từ Zing MP3 dưới dạng `Readable` stream.
3
+ A lightweight Node.js library for working with Zing MP3 streams.
4
4
 
5
- Package này hiện tập trung vào 2 tác vụ chính:
5
+ This package focuses on a small, practical API:
6
+ - fetch a music stream by song ID
7
+ - fetch a video stream by video ID
8
+ - search songs by keyword
9
+ - expose a reusable cookie jar utility
10
+ - expose the signature generator used by the API
11
+ - wrap library failures with a dedicated `Lapse` error class
6
12
 
7
- - Lấy **nhạc** theo ID và trả về stream audio
8
- - Lấy **video** theo ID và trả về stream video HLS
13
+ The package ships with:
14
+ - ESM build
15
+ - CommonJS build
16
+ - TypeScript declarations
9
17
 
10
- Library build sẵn cho cả **ESM** và **CommonJS**, có type cho TypeScript.
18
+ ## Features
11
19
 
12
- ## Tính năng
20
+ - Stream music as a Node.js `Readable`
21
+ - Stream video as a Node.js `Readable`
22
+ - Sync-like helpers that return a stream immediately and pipe later
23
+ - Built-in cookie handling for Zing requests
24
+ - Typed response for `search()`
25
+ - Small public surface area
13
26
 
14
- - Export sẵn `client` dùng ngay
15
- - Có thể tự tạo `new Client()` để chỉnh tốc độ stream/buffer
16
- - Trả về `Readable` stream để pipe thẳng ra file, HTTP response, Discord voice pipeline, hoặc xử lý tiếp
17
- - Có cookie jar nội bộ để giữ cookie giữa các request
18
- - Có chữ ký request nội bộ cho API đang dùng
19
- - Hỗ trợ ESM, CJS, TypeScript
20
-
21
- ## Cài đặt
27
+ ## Installation
22
28
 
23
29
  ```bash
24
30
  npm install @khang07/zing-mp3-api
25
31
  ```
26
32
 
27
- hoặc
28
-
29
- ```bash
30
- bun add @khang07/zing-mp3-api
31
- ```
32
-
33
- ## Yêu cầu môi trường
34
-
35
- Package này chạy cho **Node.js**, không dành cho browser vì dùng:
33
+ ## Requirements
36
34
 
37
- - `node:stream`
38
- - `node:crypto`
39
- - `axios` với Node adapter
40
- - `m3u8stream`
35
+ - Node.js
36
+ - A runtime that supports Node streams
41
37
 
42
- ## Dùng nhanh
38
+ ## Package exports
43
39
 
44
- ### ESM
40
+ Main package:
45
41
 
46
42
  ```ts
47
- import client from "@khang07/zing-mp3-api";
48
- import { createWriteStream } from "node:fs";
49
-
50
- const writer = createWriteStream("music.mp3");
51
- const stream = await client.music("MUSIC_ID");
52
-
53
- stream.pipe(writer);
43
+ import client, { ZingClient } from "@khang07/zing-mp3-api";
54
44
  ```
55
45
 
56
- ### ESM với named import
46
+ Subpath exports:
57
47
 
58
48
  ```ts
59
- import { client, Client } from "@khang07/zing-mp3-api";
60
- import { createWriteStream } from "node:fs";
61
-
62
- const musicWriter = createWriteStream("music.mp3");
63
- client.musicSyncLike("MUSIC_ID").pipe(musicWriter);
64
-
65
- const customClient = new Client({
66
- maxRate: [256 * 1024, 64 * 1024]
67
- });
68
-
69
- const videoWriter = createWriteStream("video.bin");
70
- const videoStream = await customClient.video("VIDEO_ID");
71
- videoStream.pipe(videoWriter);
49
+ import { Cookies } from "@khang07/zing-mp3-api/utils/cookies";
50
+ import { createSignature } from "@khang07/zing-mp3-api/utils/encrypt";
51
+ import { Lapse } from "@khang07/zing-mp3-api/utils/lapse";
72
52
  ```
73
53
 
74
- ### CommonJS
75
-
76
- ```js
77
- const { client, Client } = require("@khang07/zing-mp3-api");
78
- const { createWriteStream } = require("node:fs");
54
+ ## Quick start
79
55
 
80
- const writer = createWriteStream("music.mp3");
81
- client.musicSyncLike("MUSIC_ID").pipe(writer);
82
- ```
56
+ ### Use the default client
83
57
 
84
- ## API
58
+ ```ts
59
+ import fs from "node:fs";
60
+ import client from "@khang07/zing-mp3-api";
85
61
 
86
- ### `new Client(options?)`
62
+ const stream = await client.music("Z7ACBFEF");
63
+ stream.pipe(fs.createWriteStream("music.mp3"));
64
+ ```
87
65
 
88
- Tạo client mới.
66
+ ### Create your own client
89
67
 
90
68
  ```ts
91
- import { Client } from "@khang07/zing-mp3-api";
69
+ import { ZingClient } from "@khang07/zing-mp3-api";
92
70
 
93
- const client = new Client({
71
+ const client = new ZingClient({
94
72
  maxRate: [100 * 1024, 16 * 1024]
95
73
  });
96
74
  ```
97
75
 
98
- #### `ClientOptions`
76
+ ## API
77
+
78
+ ### `new ZingClient(options?)`
79
+
80
+ Create a new client instance.
81
+
82
+ #### Parameters
99
83
 
100
84
  ```ts
101
85
  interface ClientOptions {
@@ -103,221 +87,390 @@ interface ClientOptions {
103
87
  }
104
88
  ```
105
89
 
106
- Ý nghĩa:
90
+ #### Notes
107
91
 
108
- - `download`: giới hạn tốc độ download cho request stream
109
- - `highWaterMark`: buffer size cho `PassThrough` của các hàm `SyncLike`
92
+ - `maxRate[0]`: download rate limit passed to Axios
93
+ - `maxRate[1]`: `highWaterMark` used by `musicSyncLike()` and `videoSyncLike()`
110
94
 
111
- Giá trị mặc định:
95
+ ### `client.music(musicID)`
96
+
97
+ Fetches a music stream by song ID.
98
+
99
+ #### Signature
112
100
 
113
101
  ```ts
114
- [100 * 1024, 16 * 1024]
102
+ music(musicID: string): Promise<Readable>
115
103
  ```
116
104
 
117
- ## Các method
105
+ #### Returns
118
106
 
119
- ### `await client.music(musicID)`
107
+ A `Promise<Readable>` for the audio stream.
120
108
 
121
- Trả về `Promise<Readable>` cho audio stream.
109
+ #### Example
122
110
 
123
111
  ```ts
112
+ import fs from "node:fs";
124
113
  import client from "@khang07/zing-mp3-api";
125
- import { createWriteStream } from "node:fs";
126
114
 
127
- const output = createWriteStream("music.mp3");
128
- const stream = await client.music("ZZEEOZEC");
129
-
130
- stream.pipe(output);
115
+ const music = await client.music("Z7ACBFEF");
116
+ music.pipe(fs.createWriteStream("track.mp3"));
131
117
  ```
132
118
 
133
- Ghi chú:
119
+ #### Behavior
134
120
 
135
- - Method này gọi API `/api/v2/song/get/streaming`
136
- - Hiện tại code lấy URL tại `data[128]`
137
- - Nghĩa implementation hiện tại đang dùng nhánh stream **128 kbps**
121
+ - validates the input ID
122
+ - initializes cookies if needed
123
+ - requests the Zing streaming endpoint
124
+ - retries a secondary endpoint for some VIP/PRI-style responses
125
+ - throws a `Lapse` when the song cannot be streamed
138
126
 
139
127
  ### `client.musicSyncLike(musicID)`
140
128
 
141
- Trả về `Readable` ngay để thể pipe trực tiếp, phù hợp khi bạn muốn viết ngắn hơn.
129
+ Returns a stream immediately and starts resolving the remote source in the background.
130
+
131
+ #### Signature
132
+
133
+ ```ts
134
+ musicSyncLike(musicID: string): Readable
135
+ ```
136
+
137
+ #### Example
142
138
 
143
139
  ```ts
140
+ import fs from "node:fs";
144
141
  import client from "@khang07/zing-mp3-api";
145
- import { createWriteStream } from "node:fs";
146
142
 
147
- client.musicSyncLike("ZZEEOZEC").pipe(createWriteStream("music.mp3"));
143
+ const music = client.musicSyncLike("Z7ACBFEF");
144
+ music.pipe(fs.createWriteStream("track.mp3"));
148
145
  ```
149
146
 
150
- ### `await client.video(videoID)`
147
+ #### When to use
148
+
149
+ Use this when you want a stream object right away instead of awaiting `Promise<Readable>`.
150
+
151
+ ### `client.video(videoID)`
152
+
153
+ Fetches a video stream by video ID.
151
154
 
152
- Trả về `Promise<Readable>` cho video stream.
155
+ #### Signature
153
156
 
154
157
  ```ts
155
- import client from "@khang07/zing-mp3-api";
156
- import { createWriteStream } from "node:fs";
158
+ video(videoID: string): Promise<Readable>
159
+ ```
160
+
161
+ #### Returns
162
+
163
+ A `Promise<Readable>` for the video stream.
157
164
 
158
- const output = createWriteStream("video.bin");
159
- const stream = await client.video("ZZEEOZEC");
165
+ #### Example
160
166
 
161
- stream.pipe(output);
167
+ ```ts
168
+ import fs from "node:fs";
169
+ import client from "@khang07/zing-mp3-api";
170
+
171
+ const video = await client.video("ZO8I9ZZC");
172
+ video.pipe(fs.createWriteStream("video.ts"));
162
173
  ```
163
174
 
164
- Ghi chú quan trọng:
175
+ #### Behavior
176
+
177
+ - validates the input ID
178
+ - initializes cookies if needed
179
+ - requests the video endpoint
180
+ - reads the HLS `360p` stream URL from the response
181
+ - uses `m3u8stream` to produce the returned stream
165
182
 
166
- - Method này gọi API `/api/v2/page/get/video`
167
- - Hiện tại code chỉ lấy `streaming.hls["360p"]`
168
- - Đây **HLS stream** lấy qua `m3u8stream`
169
- - Library **không transcode**, **không remux**, **không ép sang MP4 thật**
170
- - Vì vậy bạn nên xem dữ liệu trả về là **stream media thô từ HLS**, không nên mặc định coi nó luôn là file `.mp4` chuẩn
183
+ #### Important
184
+
185
+ The current implementation streams HLS media from the `360p` source. It does **not** remux or convert the output into MP4 by itself.
171
186
 
172
187
  ### `client.videoSyncLike(videoID)`
173
188
 
174
- Trả về `Readable` ngay để pipe trực tiếp.
189
+ Returns a stream immediately and starts resolving the remote video source in the background.
190
+
191
+ #### Signature
175
192
 
176
193
  ```ts
194
+ videoSyncLike(videoID: string): Readable
195
+ ```
196
+
197
+ #### Example
198
+
199
+ ```ts
200
+ import fs from "node:fs";
177
201
  import client from "@khang07/zing-mp3-api";
178
- import { createWriteStream } from "node:fs";
179
202
 
180
- client.videoSyncLike("ZZEEOZEC").pipe(createWriteStream("video.bin"));
203
+ const video = client.videoSyncLike("ZO8I9ZZC");
204
+ video.pipe(fs.createWriteStream("video.ts"));
181
205
  ```
182
206
 
183
- ## Bắt lỗi
207
+ ### `client.search(keyword)`
184
208
 
185
- Khi lỗi, code ném ra error với các field thực tế đang có trong implementation:
209
+ Searches songs by keyword.
186
210
 
187
- - `name`: `ZING_MP3_ERROR`
188
- - `message`
189
- - `code`
190
- - `status` (nếu )
191
- - `cause` (nếu có)
211
+ #### Signature
212
+
213
+ ```ts
214
+ search(keyword: string): Promise<ResponseSearch[]>
215
+ ```
192
216
 
193
- Các lỗi đang xuất hiện trong source:
217
+ #### Return type
194
218
 
195
- - `ERROR_INVALID_ID`
196
- - `ERROR_MUSIC_NOT_FOUND`
197
- - `ERROR_MUSIC_VIP_ONLY`
198
- - `ERROR_VIDEO_NOT_FOUND`
199
- - `ERROR_STREAM_URL_NOT_FOUND`
200
- - `ERROR_STREAM_DOWNLOAD`
201
- - `ERROR_MUSIC_FETCH`
202
- - `ERROR_VIDEO_FETCH`
219
+ ```ts
220
+ interface ResponseSearch {
221
+ id: string;
222
+ name: string;
223
+ alias: string;
224
+ isOffical: boolean;
225
+ username: string;
226
+ artists: {
227
+ id: string;
228
+ name: string;
229
+ alias: string;
230
+ thumbnail: {
231
+ w240: string;
232
+ w360: string;
233
+ };
234
+ }[];
235
+ thumbnail: {
236
+ w94: string;
237
+ w240: string;
238
+ };
239
+ duration: number;
240
+ releaseDate: number;
241
+ }
242
+ ```
203
243
 
204
- dụ:
244
+ #### Example
205
245
 
206
246
  ```ts
207
247
  import client from "@khang07/zing-mp3-api";
208
- import { createWriteStream } from "node:fs";
209
248
 
210
- try {
211
- const stream = await client.music("ZZEEOZEC");
212
- const writer = createWriteStream("music.mp3");
249
+ const results = await client.search("mở mắt");
213
250
 
214
- stream.on("error", (error) => {
215
- console.error("Stream error:", error);
216
- });
251
+ for (const song of results) {
252
+ console.log(song.id, song.name, song.artists.map((artist) => artist.name).join(", "));
253
+ }
254
+ ```
217
255
 
218
- writer.on("error", (error) => {
219
- console.error("Write error:", error);
220
- });
256
+ #### Important
221
257
 
222
- stream.pipe(writer);
223
- } catch (error) {
224
- console.error(error);
258
+ The current implementation only maps the `songs` section from the search response. It does not currently return artists, playlists, or videos.
259
+
260
+ ## Utility exports
261
+
262
+ ### `Cookies`
263
+
264
+ A lightweight in-memory cookie jar.
265
+
266
+ #### Available methods
267
+
268
+ ```ts
269
+ class Cookies {
270
+ setCookie(setCookie: string, requestUrl: string): void;
271
+ setCookies(setCookies: string[] | undefined, requestUrl: string): void;
272
+ getCookies(requestUrl: string): CookieRecord[];
273
+ getCookieHeader(requestUrl: string): string;
274
+ applyToHeaders(requestUrl: string, headers?: Record<string, string>): Record<string, string>;
275
+ deleteCookie(domain: string, path: string, name: string): void;
276
+ cleanup(): void;
277
+ toJSON(): CookieRecord[];
278
+ fromJSON(cookies: CookieRecord[]): void;
225
279
  }
226
280
  ```
227
281
 
228
- ## Dùng với HTTP server
282
+ #### Example
229
283
 
230
284
  ```ts
231
- import http from "node:http";
232
- import client from "@khang07/zing-mp3-api";
285
+ import { Cookies } from "@khang07/zing-mp3-api/utils/cookies";
233
286
 
234
- const server = http.createServer(async (_req, res) => {
235
- try {
236
- const stream = await client.music("ZZEEOZEC");
287
+ const jar = new Cookies();
288
+ jar.setCookie("sessionid=abc123; Path=/; HttpOnly", "https://zingmp3.vn/");
237
289
 
238
- res.writeHead(200, {
239
- "Content-Type": "audio/mpeg"
240
- });
290
+ const headers = jar.applyToHeaders("https://zingmp3.vn/api/v2/song/get/streaming");
291
+ console.log(headers.Cookie);
292
+ ```
241
293
 
242
- stream.on("error", () => {
243
- if (!res.headersSent)
244
- res.writeHead(500);
294
+ ### `createSignature(uri, params, secret)`
245
295
 
246
- res.end("Stream failed");
247
- });
296
+ Creates the request signature used by the Zing endpoints.
248
297
 
249
- stream.pipe(res);
250
- } catch {
251
- res.writeHead(500);
252
- res.end("Failed to fetch music");
253
- }
254
- });
298
+ #### Signature
255
299
 
256
- server.listen(3000);
300
+ ```ts
301
+ createSignature(uri: string, params: string, secret: string): string
257
302
  ```
258
303
 
259
- ## Build từ source
304
+ #### Example
305
+
306
+ ```ts
307
+ import { createSignature } from "@khang07/zing-mp3-api/utils/encrypt";
260
308
 
261
- Project đang dùng TypeScript + Rollup + API Extractor.
309
+ const sig = createSignature(
310
+ "/api/v2/song/get/streaming",
311
+ "ctime=1234567890id=Z7ACBFEFversion=1.6.34",
312
+ "2aa2d1c561e809b267f3638c4a307aab"
313
+ );
262
314
 
263
- ```bash
264
- bun install
265
- bun run build
315
+ console.log(sig);
266
316
  ```
267
317
 
268
- Output build:
318
+ ### `Lapse`
269
319
 
270
- - `dist/esm` bản ESM
271
- - `dist/cjs` → bản CommonJS
272
- - `dist/types` → type declarations gộp
320
+ A custom error class used by the library.
273
321
 
274
- Các script hiện có:
322
+ #### Signature
275
323
 
276
- ```bash
277
- bun run build:esm
278
- bun run build:cjs
279
- bun run build:types
280
- bun run build
324
+ ```ts
325
+ class Lapse extends Error {
326
+ code: string;
327
+ status?: number;
328
+ cause?: unknown;
329
+ }
281
330
  ```
282
331
 
283
- ## Cấu trúc dự án
284
-
285
- ```text
286
- source/
287
- index.ts
288
- types/
289
- utils/
290
- dist/
291
- esm/
292
- cjs/
293
- types/
332
+ #### Example
333
+
334
+ ```ts
335
+ import client from "@khang07/zing-mp3-api";
336
+ import { Lapse } from "@khang07/zing-mp3-api/utils/lapse";
337
+
338
+ try {
339
+ await client.music("");
340
+ } catch (error) {
341
+ if (error instanceof Lapse) {
342
+ console.error(error.name);
343
+ console.error(error.code);
344
+ console.error(error.message);
345
+ console.error(error.status);
346
+ }
347
+ }
294
348
  ```
295
349
 
296
- ## Ghi chú triển khai hiện tại
350
+ ## Error codes
297
351
 
298
- vài điểm cần lưu ý:
352
+ The current codebase throws these `Lapse.code` values:
299
353
 
300
- - Chưa API search public trong `Client` dù đang export type `SearchCategory`
301
- - Video hiện mới lấy nhánh `360p`
302
- - Music hiện mới lấy nhánh `128kps`
303
- - Package phụ thuộc vào cấu trúc API/cookie/signature hiện tại của Zing MP3, nên nếu upstream đổi thì cần cập nhật source
354
+ | Code | Meaning |
355
+ |---|---|
356
+ | `ERROR_INVALID_ID` | `music()` or `video()` received an empty or invalid ID |
357
+ | `ERROR_INVALID_KEYWORD` | `search()` received an empty keyword |
358
+ | `ERROR_MUSIC_NOT_FOUND` | Music was not found from the primary endpoint |
359
+ | `ERROR_MUSIC_VIP_ONLY` | Music required access not available through the fallback flow |
360
+ | `ERROR_VIDEO_NOT_FOUND` | Video was not found |
361
+ | `ERROR_STREAM_URL_NOT_FOUND` | The API response did not contain a playable stream URL |
362
+ | `ERROR_STREAM_DOWNLOAD` | The download stream failed while reading data |
363
+ | `ERROR_MUSIC_FETCH` | A non-library failure occurred while fetching music |
364
+ | `ERROR_VIDEO_FETCH` | A non-library failure occurred while fetching video |
365
+ | `ERROR_SEARCH_FAILED` | Search endpoint returned an error response |
366
+ | `ERROR_SEARCH` | A non-library failure occurred while performing search |
304
367
 
305
- ## Ghi chú về file test trong repo
368
+ ## Demo
306
369
 
307
- Trong repo hiện `test/index.js` và `test/index.cjs`, nhưng chúng đang import từ `"zing-mp3-api"`.
370
+ ### Download a song to file
308
371
 
309
- Khi publish theo `package.json` hiện tại, import đúng cho người dùng ngoài sẽ là:
372
+ ```ts
373
+ import fs from "node:fs";
374
+ import client from "@khang07/zing-mp3-api";
375
+ import { Lapse } from "@khang07/zing-mp3-api/utils/lapse";
310
376
 
311
- ```js
312
- import { client } from "@khang07/zing-mp3-api";
377
+ async function main(): Promise<void> {
378
+ try {
379
+ const stream = await client.music("Z7ACBFEF");
380
+ stream.pipe(fs.createWriteStream("song.mp3"));
381
+ } catch (error) {
382
+ if (error instanceof Lapse)
383
+ console.error(error.code, error.message);
384
+ else
385
+ console.error(error);
386
+ }
387
+ }
388
+
389
+ void main();
313
390
  ```
314
391
 
315
- hoặc:
392
+ ### Download a video stream to file
393
+
394
+ ```ts
395
+ import fs from "node:fs";
396
+ import client from "@khang07/zing-mp3-api";
397
+
398
+ async function main(): Promise<void> {
399
+ const stream = await client.video("ZO8I9ZZC");
400
+ stream.pipe(fs.createWriteStream("video.ts"));
401
+ }
402
+
403
+ void main();
404
+ ```
405
+
406
+ ### Search then download the first result
407
+
408
+ ```ts
409
+ import fs from "node:fs";
410
+ import client from "@khang07/zing-mp3-api";
411
+ import { Lapse } from "@khang07/zing-mp3-api/utils/lapse";
412
+
413
+ async function main(): Promise<void> {
414
+ try {
415
+ const results = await client.search("mở mắt");
416
+ const first = results[0];
417
+
418
+ if (!first)
419
+ throw new Error("No result found");
420
+
421
+ const stream = await client.music(first.id);
422
+ stream.pipe(fs.createWriteStream(first.alias + ".mp3"));
423
+ } catch (error) {
424
+ if (error instanceof Lapse)
425
+ console.error(error.code, error.message);
426
+ else
427
+ console.error(error);
428
+ }
429
+ }
430
+
431
+ void main();
432
+ ```
433
+
434
+ ## CommonJS example
316
435
 
317
436
  ```js
318
- const { client } = require("@khang07/zing-mp3-api");
437
+ const fs = require("node:fs");
438
+ const zing = require("@khang07/zing-mp3-api");
439
+
440
+ async function main() {
441
+ const client = zing.default;
442
+ const stream = await client.music("Z7ACBFEF");
443
+ stream.pipe(fs.createWriteStream("track.mp3"));
444
+ }
445
+
446
+ main();
319
447
  ```
320
448
 
449
+ ## Build
450
+
451
+ ```bash
452
+ npm run build
453
+ ```
454
+
455
+ Current scripts in the project:
456
+
457
+ ```json
458
+ {
459
+ "build:esm": "tsc -p tsconfig.json",
460
+ "build:cjs": "rollup -c",
461
+ "build": "rm -rf dist && bun run build:esm && bun run build:cjs && rm -fr dist/esm/types dist/cjs/types",
462
+ "test": "mocha"
463
+ }
464
+ ```
465
+
466
+ ## Notes
467
+
468
+ - The default export is a preconfigured client instance.
469
+ - The named export is `ZingClient`.
470
+ - Search currently returns songs only.
471
+ - Video currently streams HLS `360p`.
472
+ - Errors are wrapped in `Lapse` for consistent handling.
473
+
321
474
  ## License
322
475
 
323
- Xem tại [LICENSE](https://github.com/GiaKhang1810/zing-mp3-api#license)
476
+ MIT