gerdur-core 1.0.1 → 1.0.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.
- package/CHANGELOG.md +15 -0
- package/dist/lib/blowfish.d.ts +25 -0
- package/dist/lib/blowfish.js +111 -0
- package/dist/lib/decrypt.d.ts +20 -2
- package/dist/lib/decrypt.js +62 -23
- package/dist/lib/fast-lru.d.ts +11 -53
- package/dist/lib/fast-lru.js +49 -95
- package/dist/lib/get-url.d.ts +8 -0
- package/dist/lib/get-url.js +52 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.2 - 2026-08-30
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`decryptDownload` now works on Node >= 17 / OpenSSL 3.** It used `crypto('bf-cbc')`, which OpenSSL 3 disables by default (`ERR_OSSL_EVP_UNSUPPORTED`). Replaced with a correct, dependency-free Blowfish (~290 MiB/s). Verified against the canonical Blowfish test vectors and a real-track fixture (`__tests__/decrypt.ts`).
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `TrackDecryptStream` — decrypt a track from ordered socket chunks (`write()` / `final()`); constant memory, CPU work hides behind the network read.
|
|
12
|
+
- `ExpiredTrackToken` error — thrown by `getTrackDownloadUrl` when `TRACK_TOKEN_EXPIRE` has passed or the media API returns code 2000/2001. Re-fetch the track with `getTrackInfo(SNG_ID)` and retry.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Internal LRU (`fast-lru`) rewritten around `Map` insertion order — O(1) eviction (was O(n log n) per write once full) and true LRU semantics (was LFU).
|
|
17
|
+
|
|
3
18
|
## 1.0.1 - 2026-08-30
|
|
4
19
|
|
|
5
20
|
Initial public release.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blowfish (block cipher + CBC stream, NO padding).
|
|
3
|
+
*
|
|
4
|
+
* A correct, dependency-free replacement for `crypto.createDecipheriv('bf-cbc')`,
|
|
5
|
+
* which OpenSSL 3 / Node >= 17 disables by default (throws
|
|
6
|
+
* `ERR_OSSL_EVP_UNSUPPORTED`). Pure integer math, ~250 MiB/s.
|
|
7
|
+
*
|
|
8
|
+
* The initial P-array (18) and S-boxes (4 x 256) are the canonical Blowfish
|
|
9
|
+
* constants (the hexadecimal digits of pi), embedded below as one hex string.
|
|
10
|
+
* Verified against the standard Blowfish test vectors and against real Deezer
|
|
11
|
+
* FLAC/MP3 (see gerdur test-app / bench).
|
|
12
|
+
*/
|
|
13
|
+
/// <reference types="node" />
|
|
14
|
+
export declare class Blowfish {
|
|
15
|
+
/** flat key: P at [0..17], S0 at [18..], S1 at [274..], S2 at [530..], S3 at [786..] */
|
|
16
|
+
private readonly k;
|
|
17
|
+
constructor(key: Buffer | Uint8Array);
|
|
18
|
+
private f;
|
|
19
|
+
private encBlock;
|
|
20
|
+
/**
|
|
21
|
+
* Decrypt `src[off .. off+len)` in CBC mode with the fixed Deezer IV and no
|
|
22
|
+
* padding, writing into `out` at the same offset. `len` must be a multiple of 8.
|
|
23
|
+
*/
|
|
24
|
+
decryptCbc(src: Buffer, off: number, len: number, out: Buffer): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Blowfish (block cipher + CBC stream, NO padding).
|
|
4
|
+
*
|
|
5
|
+
* A correct, dependency-free replacement for `crypto.createDecipheriv('bf-cbc')`,
|
|
6
|
+
* which OpenSSL 3 / Node >= 17 disables by default (throws
|
|
7
|
+
* `ERR_OSSL_EVP_UNSUPPORTED`). Pure integer math, ~250 MiB/s.
|
|
8
|
+
*
|
|
9
|
+
* The initial P-array (18) and S-boxes (4 x 256) are the canonical Blowfish
|
|
10
|
+
* constants (the hexadecimal digits of pi), embedded below as one hex string.
|
|
11
|
+
* Verified against the standard Blowfish test vectors and against real Deezer
|
|
12
|
+
* FLAC/MP3 (see gerdur test-app / bench).
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.Blowfish = void 0;
|
|
16
|
+
// 1042 uint32 words = P[0..17] then S0[0..255], S1, S2, S3
|
|
17
|
+
const INIT_HEX = '243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89452821e638d01377be5466cf34e90c6cc0ac29b7c97c50dd3f84d5b5b54709179216d5d98979fb1bd1310ba698dfb5ac2ffd72dbd01adfb7b8e1afed6a267e96ba7c9045f12c7f9924a19947b3916cf70801f2e2858efc16636920d871574e69a458fea3f4933d7e0d95748f728eb658718bcd5882154aee7b54a41dc25a59b59c30d5392af26013c5d1b023286085f0ca417918b8db38ef8e79dcb0603a180e6c9e0e8bb01e8a3ed71577c1bd314b2778af2fda55605c60e65525f3aa55ab945748986263e8144055ca396a2aab10b6b4cc5c341141e8cea15486af7c72e993b3ee1411636fbc2a2ba9c55d741831f6ce5c3e169b87931eafd6ba336c24cf5c7a325381289586773b8f48986b4bb9afc4bfe81b6628219361d809ccfb21a991487cac605dec8032ef845d5de98575b1dc262302eb651b8823893e81d396acc50f6d6ff383f442392e0b4482a484200469c8f04a9e1f9b5e21c66842f6e96c9a670c9c61abd388f06a51a0d2d8542f68960fa728ab5133a36eef0b6c137a3be4ba3bf0507efb2a98a1f1651d39af017666ca593e82430e888cee8619456f9fb47d84a5c33b8b5ebee06f75d885c12073401a449f56c16aa64ed3aa62363f77061bfedf72429b023d37d0d724d00a1248db0fead349f1c09b075372c980991b7b25d479d8f6e8def7e3fe501ab6794c3b976ce0bd04c006bac1a94fb6409f60c45e5c9ec2196a246368fb6faf3e6c53b51339b2eb3b52ec6f6dfc511f9b30952ccc814544af5ebd09bee3d004de334afd660f2807192e4bb3c0cba85745c8740fd20b5f39b9d3fbdb5579c0bd1a60320ad6a100c6402c7279679f25fefb1fa3cc8ea5e9f8db3222f83c7516dffd616b152f501ec8ad0552ab323db5fafd23876053317b483e00df829e5c57bbca6f8ca01a87562edf1769dbd542a8f6287effc3ac6732c68c4f5573695b27b0bbca58c8e1ffa35db8f011a010fa3d98fd2183b84afcb56c2dd1d35b9a53e479b6f84565d28e49bc4bfb9790e1ddf2daa4cb7e3362fb1341cee4c6e8ef20cada36774c01d07e9efe2bf11fb495dbda4dae909198eaad8e716b93d5a0d08ed1d0afc725e08e3c5b2f8e7594b78ff6e2fbf2122b648888b812900df01c4fad5ea0688fc31cd1cff191b3a8c1ad2f2f2218be0e1777ea752dfe8b021fa1e5a0cc0fb56f74e818acf3d6ce89e299b4a84fe0fd13e0b77cc43b81d2ada8d9165fa2668095770593cc7314211a1477e6ad206577b5fa86c75442f5fb9d35cfebcdaf0c7b3e89a0d6411bd3ae1e7e4900250e2d2071b35e226800bb57b8e0af2464369bf009b91e5563911d59dfa6aa78c14389d95a537f207d5ba202e5b9c5832603766295cfa911c819684e734a41b3472dca7b14a94a1b5100529a532915d60f573fbc9bc6e42b60a47681e6740008ba6fb5571be91ff296ec6b2a0dd915b6636521e7b9f9b6ff34052ec585566453b02d5da99f8fa108ba47996e85076a4b7a70e9b5b32944db75092ec4192623ad6ea6b049a7df7d9cee60b88fedb266ecaa8c71699a17ff5664526cc2b19ee1193602a575094c29a0591340e4183a3e3f54989a5b429d656b8fe4d699f73fd6a1d29c07efe830f54d2d38e6f0255dc14cdd20868470eb266382e9c6021ecc5e09686b3f3ebaefc93c9718146b6a70a1687f358452a0e286b79c5305aa5007373e07841c7fdeae5c8e7d44ec5716f2b8b03ada37f0500c0df01c1f040200b3ffae0cf51a3cb574b225837a58dc0921bdd19113f97ca92ff69432477322f547013ae5e58137c2dadcc8b576349af3dda7a94461460fd0030eecc8c73ea4751e41e238cd993bea0e2f3280bba1183eb3314e548b384f6db9086f420d03f60a04bf2cb8129024977c795679b072bcaf89afde9a771fd9930810b38bae12dccf3f2e5512721f2e6b7124501adde69f84cd877a5847187408da17bc9f9abce94b7d8cec7aec3adb851dfa63094366c464c3d2ef1c18473215d908dd433b3724c2ba1612a14d432a65c45150940002133ae4dd71dff89e10314e5581ac77d65f11199b043556f1d7a3c76b3c11183b5924a509f28fe6ed97f1fbfa9ebabf2c1e153c6e86e34570eae96fb1860e5e0a5a3e2ab3771fe71c4e3d06fa2965dcb999e71d0f803e89d65266c8252e4cc9789c10b36ac6150eba94e2ea78a5fc3c531e0a2df4f2f74ea7361d2b3d1939260f19c279605223a708f71312b6ebadfe6eeac31f66e3bc4595a67bc883b17f37d1018cff28c332ddefbe6c5aa56558218568ab9802eecea50fdb2f953b2aef7dad5b6e2f841521b62829076170ecdd4775619f151013cca830eb61bd960334fe1eaa0363cfb5735c904c70a239d59e9e0bcbaade14eecc86bc60622ca79cab5cabb2f3846e648b1eaf19bdf0caa02369b9655abb5040685a323c2ab4b3319ee9d5c021b8f79b540b19875fa09995f7997e623d7da8f837889a97e32d7711ed935f166812810e358829c7e61fd696dedfa17858ba9957f584a51b2272639b83c3ff1ac24696cdb30aeb532e30548fd948e46dbc312858ebf2ef34c6ffeafe28ed61ee7c3c735d4a14d9e864b7e342105d14203e13e045eee2b6a3aaabeadb6c4f15facb4fd0c742f442ef6abbb5654f3b1d41cd2105d81e799e86854dc7e44b476a3d816250cf62a1f25b8d2646fc8883a0c1c7b6a37f1524c369cb749247848a0b5692b285095bbf00ad19489d1462b17423820e0058428d2a0c55f5ea1dadf43e233f70613372f0928d937e41d65fecf16c223bdb7cde3759cbee74604085f2a7ce77326ea607808419f8509ee8efd85561d99735a969a7aac50c06c25a04abfc800bcadc9e447a2ec3453484fdd567050e1e9ec9db73dbd3105588cd675fda79e3674340c5c43465713e38d83d28f89ef16dff20153e21e78fb03d4ae6e39f2bdb83adf7e93d5a68948140f7f64c261c94692934411520f77602d4f7bcf46b2ed4a20068d40824713320f46a43b7d4b7500061af1e39f62e9724454614214f74bf8b88404d95fc1d96b591af70f4ddd366a02f45bfbc09ec03bd97857fac6dd031cb850496eb27b355fd3941da2547e6abca0a9a28507825530429f40a2c86dae9b66dfb68dc1462d7486900680ec0a427a18dee4f3ffea2e887ad8cb58ce0067af4d6b6aace1e7cd3375fecce78a399406b2a4220fe9e35d9f385b9ee39d7ab3b124e8b1dc9faf74b6d185626a36631eae397b23a6efa74dd5b43326841e7f7ca7820fbfb0af54ed8feb397454056acba48952755533a3a20838d87fe6ba9b7d096954b55a867bca1159a58cca9296399e1db33a62a4a563f3125f95ef47e1c9029317cfdf8e80204272f7080bb155c05282ce395c11548e4c66d2248c1133fc70f86dc07f9c9ee41041f0f404779a45d886e17325f51ebd59bc0d1f2bcc18f41113564257b7834602a9c60dff8e8a31f636c1b0e12b4c202e1329eaf664fd1cad181156b2395e0333e92e13b240b62eebeb92285b2a20ee6ba0d99de720c8c2da2f728d012784595b794fd647d0862e7ccf5f05449a36f877d48fac39dfd27f33e8d1e0a476341992eff743a6f6eabf4f8fd37a812dc60a1ebddf8991be14cdb6e6b0dc67b55106d672c372765d43bdcd0e804f1290dc7cc00ffa3b5390f92690fed0b667b9ffbcedb7d9ca091cf0bd9155ea3bb132f88515bad247b9479bf763bd6eb37392eb3cc1159798026e297f42e312d6842ada7c66a2b3b12754ccc782ef11c6a124237b79251e706a1bbe64bfb63501a6b101811caedfa3d25bdd8e2e1c3c9444216590a121386d90cec6ed5abea2a64af674eda86a85fbebfe98864e4c3fe9dbc8057f0f7c08660787bf86003604dd1fd8346f6381fb07745ae04d736fccc83426b33f01eab71b08041873c005e5f77a057bebde8ae2455464299bf582e614e58f48ff2ddfda2f474ef388789bdc25366f9c3c8b38e74b475f25546fcd9b97aeb26618b1ddf84846a0e79915f95e2466e598e20b457708cd55591c902de4cb90bace1bb8205d011a862487574a99eb77f19b6e0a9dc09662d09a1c4324633e85a1f0209f0be8c4a99a0251d6efe101ab93d1d0ba5a4dfa186f20f2868f169dcb7da83573906fea1e2ce9b4fcd7f5250115e01a70683faa002b5c40de6d0279af88c27773f8641c3604c0661a806b5f0177a28c0f586e0006058aa30dc7d6211e69ed72338ea6353c2dd94c2c21634bbcbee5690bcb6deebfc7da1ce591d766f05e4094b7c018839720a3d7c927c2486e3725f724d9db91ac15bb4d39eb8fced54557808fca5b5d83d7cd34dad0fc41e50ef5eb161e6f8a28514d96c51133c6fd5c7e756e14ec4362abfceddc6c837d79a323492638212670efa8e406000e03a39ce37d3faf5cfabc277375ac52d1b5cb0679e4fa33742d382274099bc9bbed5118e9dbf0f7315d62d1c7ec700c47bb78c1b6b21a19045b26eb1be6a366eb45748ab2fbc946e79c6a376d26549c2c8530ff8ee468dde7dd5730a1d4cd04dc62939bbdba9ba4650ac9526e8be5ee304a1fad5f06a2d519a63ef8ce29a86ee22c089c2b843242ef6a51e03aa9cf2d0a483c061ba9be96a4d8fe51550ba645bd62826a2f9a73a3ae14ba99586ef5562e9c72fefd3f752f7da3f046f6977fa0a5980e4a91587b086019b09e6ad3b3ee593e990fd5a9e34d7972cf0b7d9022b8b5196d5ac3a017da67dd1cf3ed67c7d2d281f9f25cfadf2b89b5ad6b4725a88f54ce029ac71e019a5e647b0acfded93fa9be8d3c48d283b57ccf8d5662979132e28785f0191ed756055f7960e44e3d35e8c15056dd488f46dba03a161250564f0bdc3eb9e153c9057a297271aeca93a072a1b3f6d9b1e6321f5f59c66fb26dcf3197533d928b155fdf5035634828aba3cbb28517711c20ad9f8abcc5167ccad925f4de817513830dc8e379d58629320f991ea7a90c2fb3e7bce5121ce64774fbe32a8b6e37ec3293d4648de53696413e680a2ae0810dd6db22469852dfd09072166b39a460a6445c0dd586cdecf1c20c8ae5bbef7dd1b588d40ccd2017f6bb4e3bbdda26a7e3a59ff453e350a44bcb4cdd572eacea8fa6484bb8d6612aebf3c6f47d29be463542f5d9eaec2771bf64e6370740e0d8de75b1357f8721671af537d5d4040cb084eb4e2cc34d2466a0115af84e1b0042895983a1d06b89fb4ce6ea0486f3f3b823520ab82011a1d4b277227f8611560b1e7933fdcbb3a792b344525bda08839e151ce794b2f32c9b7a01fbac9e01cc87ebcc7d1f6cf0111c3a1e8aac71a908749d44fbd9ad0dadecbd50ada380339c32ac69136678df9317ce0b12b4ff79e59b743f5bb3af2d519ff27d9459cbf97222c15e6fc2a0f91fc719b941525fae59361ceb69cebc2a8645912baa8d1b6c1075ee3056a0c10d25065cb03a442e0ec6e0e1698db3b4c98a0be3278e9649f1f9532e0d392dfd3a0342b8971f21e1b0a74414ba3348cc5be7120c37632d8df359f8d9b992f2ee60b6f470fe3f11de54cda541edad891ce6279cfcd3e7e6f1618b166fd2c1d05848fd2c5f6fb2299f523f357a632762393a8353156cccd02acf081625a75ebb56e16369788d273ccde96629281b949d04c50901b71c65614e6c6c7bd327a140a45e1d006c3f27b9ac9aa53fd62a80f00bb25bfe235bdd2f671126905b2040222b6cbcf7ccd769c2b53113ec01640e3d338abbd602547adf0ba38209cf746ce7677afa1c52075606085cbfe4e8ae88dd87aaaf9b04cf9aa7e1948c25c02fb8a8c01c36ae4d6ebe1f990d4f869a65cdea03f09252dc208e69fb74e6132ce77e25b578fdfe33ac372e6';
|
|
18
|
+
const INIT = new Uint32Array(1042);
|
|
19
|
+
for (let i = 0; i < 1042; i++) {
|
|
20
|
+
INIT[i] = parseInt(INIT_HEX.slice(i * 8, i * 8 + 8), 16) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
const IV0 = 0x00010203 | 0;
|
|
23
|
+
const IV1 = 0x04050607 | 0;
|
|
24
|
+
class Blowfish {
|
|
25
|
+
constructor(key) {
|
|
26
|
+
/** flat key: P at [0..17], S0 at [18..], S1 at [274..], S2 at [530..], S3 at [786..] */
|
|
27
|
+
this.k = new Uint32Array(1042);
|
|
28
|
+
const k = this.k;
|
|
29
|
+
k.set(INIT);
|
|
30
|
+
let j = 0;
|
|
31
|
+
for (let i = 0; i < 18; i++) {
|
|
32
|
+
let d = 0;
|
|
33
|
+
for (let n = 0; n < 4; n++) {
|
|
34
|
+
d = ((d << 8) | key[j % key.length]) >>> 0;
|
|
35
|
+
j++;
|
|
36
|
+
}
|
|
37
|
+
k[i] = (k[i] ^ d) >>> 0;
|
|
38
|
+
}
|
|
39
|
+
let l = 0;
|
|
40
|
+
let r = 0;
|
|
41
|
+
for (let i = 0; i < 18; i += 2) {
|
|
42
|
+
[l, r] = this.encBlock(l, r);
|
|
43
|
+
k[i] = l >>> 0;
|
|
44
|
+
k[i + 1] = r >>> 0;
|
|
45
|
+
}
|
|
46
|
+
for (let s = 0; s < 4; s++) {
|
|
47
|
+
const base = 18 + s * 256;
|
|
48
|
+
for (let i = 0; i < 256; i += 2) {
|
|
49
|
+
[l, r] = this.encBlock(l, r);
|
|
50
|
+
k[base + i] = l >>> 0;
|
|
51
|
+
k[base + i + 1] = r >>> 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
f(x) {
|
|
56
|
+
const k = this.k;
|
|
57
|
+
const a = (k[18 + ((x >>> 24) & 0xff)] + k[274 + ((x >>> 16) & 0xff)]) | 0;
|
|
58
|
+
return (((a ^ k[530 + ((x >>> 8) & 0xff)]) | 0) + k[786 + (x & 0xff)]) | 0;
|
|
59
|
+
}
|
|
60
|
+
encBlock(l, r) {
|
|
61
|
+
const k = this.k;
|
|
62
|
+
for (let i = 0; i < 16; i++) {
|
|
63
|
+
l = (l ^ k[i]) | 0;
|
|
64
|
+
r = (r ^ this.f(l)) | 0;
|
|
65
|
+
const t = l;
|
|
66
|
+
l = r;
|
|
67
|
+
r = t;
|
|
68
|
+
}
|
|
69
|
+
const t = l;
|
|
70
|
+
l = r;
|
|
71
|
+
r = t;
|
|
72
|
+
r = (r ^ k[16]) | 0;
|
|
73
|
+
l = (l ^ k[17]) | 0;
|
|
74
|
+
return [l >>> 0, r >>> 0];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Decrypt `src[off .. off+len)` in CBC mode with the fixed Deezer IV and no
|
|
78
|
+
* padding, writing into `out` at the same offset. `len` must be a multiple of 8.
|
|
79
|
+
*/
|
|
80
|
+
decryptCbc(src, off, len, out) {
|
|
81
|
+
const k = this.k;
|
|
82
|
+
let pl = IV0;
|
|
83
|
+
let pr = IV1;
|
|
84
|
+
const end = off + len;
|
|
85
|
+
for (let p = off; p < end; p += 8) {
|
|
86
|
+
const cl = (src[p] << 24) | (src[p + 1] << 16) | (src[p + 2] << 8) | src[p + 3] | 0;
|
|
87
|
+
const cr = (src[p + 4] << 24) | (src[p + 5] << 16) | (src[p + 6] << 8) | src[p + 7] | 0;
|
|
88
|
+
let L = cl;
|
|
89
|
+
let R = cr;
|
|
90
|
+
for (let i = 17; i > 1; i -= 2) {
|
|
91
|
+
L = (L ^ k[i]) | 0;
|
|
92
|
+
R = (R ^ this.f(L)) | 0;
|
|
93
|
+
R = (R ^ k[i - 1]) | 0;
|
|
94
|
+
L = (L ^ this.f(R)) | 0;
|
|
95
|
+
}
|
|
96
|
+
const left = ((R ^ k[0]) >>> 0) ^ (pl >>> 0);
|
|
97
|
+
const right = ((L ^ k[1]) >>> 0) ^ (pr >>> 0);
|
|
98
|
+
out[p] = (left >>> 24) & 0xff;
|
|
99
|
+
out[p + 1] = (left >>> 16) & 0xff;
|
|
100
|
+
out[p + 2] = (left >>> 8) & 0xff;
|
|
101
|
+
out[p + 3] = left & 0xff;
|
|
102
|
+
out[p + 4] = (right >>> 24) & 0xff;
|
|
103
|
+
out[p + 5] = (right >>> 16) & 0xff;
|
|
104
|
+
out[p + 6] = (right >>> 8) & 0xff;
|
|
105
|
+
out[p + 7] = right & 0xff;
|
|
106
|
+
pl = cl;
|
|
107
|
+
pr = cr;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.Blowfish = Blowfish;
|
package/dist/lib/decrypt.d.ts
CHANGED
|
@@ -2,8 +2,26 @@
|
|
|
2
2
|
import type { trackType } from '../types';
|
|
3
3
|
export declare const getSongFileName: ({ MD5_ORIGIN, SNG_ID, MEDIA_VERSION }: trackType, quality: number) => string;
|
|
4
4
|
/**
|
|
5
|
+
* Decrypt a downloaded track. Deezer applies Blowfish-CBC "stripe" obfuscation:
|
|
6
|
+
* the file is split into 2048-byte chunks and only every third chunk (0, 3, 6…)
|
|
7
|
+
* is encrypted; the rest — and any trailing partial chunk — are plaintext.
|
|
5
8
|
*
|
|
6
|
-
* @param source Downloaded
|
|
7
|
-
* @param trackId
|
|
9
|
+
* @param source Downloaded body from `getTrackDownloadUrl`
|
|
10
|
+
* @param trackId `SNG_ID` as a string
|
|
8
11
|
*/
|
|
9
12
|
export declare const decryptDownload: (source: Buffer, trackId: string) => Buffer;
|
|
13
|
+
/**
|
|
14
|
+
* Streaming variant: feed it socket chunks in order via `write()`, then call
|
|
15
|
+
* `final()`. Decrypts each 2048-byte stripe as it completes — constant memory,
|
|
16
|
+
* and the CPU work hides behind the (slower) network read.
|
|
17
|
+
*/
|
|
18
|
+
export declare class TrackDecryptStream {
|
|
19
|
+
private readonly bf;
|
|
20
|
+
private carry;
|
|
21
|
+
private chunkIndex;
|
|
22
|
+
constructor(trackId: string);
|
|
23
|
+
/** Returns the decrypted bytes for every complete 2048-byte stripe now available. */
|
|
24
|
+
write(part: Buffer): Buffer;
|
|
25
|
+
/** The trailing partial chunk (always plaintext). */
|
|
26
|
+
final(): Buffer;
|
|
27
|
+
}
|
package/dist/lib/decrypt.js
CHANGED
|
@@ -3,8 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.decryptDownload = exports.getSongFileName = void 0;
|
|
6
|
+
exports.TrackDecryptStream = exports.decryptDownload = exports.getSongFileName = void 0;
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const blowfish_1 = require("./blowfish");
|
|
8
9
|
const md5 = (data, type = 'ascii') => {
|
|
9
10
|
const md5sum = crypto_1.default.createHash('md5');
|
|
10
11
|
md5sum.update(data.toString(), type);
|
|
@@ -21,43 +22,81 @@ const getSongFileName = ({ MD5_ORIGIN, SNG_ID, MEDIA_VERSION }, quality) => {
|
|
|
21
22
|
return crypto_1.default.createCipheriv('aes-128-ecb', 'jo6aey6haid2Teih', '').update(step2, 'ascii', 'hex');
|
|
22
23
|
};
|
|
23
24
|
exports.getSongFileName = getSongFileName;
|
|
25
|
+
const BLOWFISH_SECRET = 'g4el58wc0zvf9na1';
|
|
26
|
+
/** Per-track Blowfish key: md5(id)[i] ^ md5(id)[i+16] ^ SECRET[i], for i in 0..15. */
|
|
24
27
|
const getBlowfishKey = (trackId) => {
|
|
25
|
-
const SECRET = 'g4el58wc' + '0zvf9na1';
|
|
26
28
|
const idMd5 = md5(trackId);
|
|
27
|
-
|
|
29
|
+
const key = Buffer.allocUnsafe(16);
|
|
28
30
|
for (let i = 0; i < 16; i++) {
|
|
29
|
-
|
|
31
|
+
key[i] = idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ BLOWFISH_SECRET.charCodeAt(i);
|
|
30
32
|
}
|
|
31
|
-
return
|
|
32
|
-
};
|
|
33
|
-
const decryptChunk = (chunk, blowFishKey) => {
|
|
34
|
-
const cipher = crypto_1.default.createDecipheriv('bf-cbc', blowFishKey, Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]));
|
|
35
|
-
cipher.setAutoPadding(false);
|
|
36
|
-
return Buffer.concat([cipher.update(chunk), cipher.final()]);
|
|
33
|
+
return key;
|
|
37
34
|
};
|
|
35
|
+
const CHUNK = 2048;
|
|
38
36
|
/**
|
|
37
|
+
* Decrypt a downloaded track. Deezer applies Blowfish-CBC "stripe" obfuscation:
|
|
38
|
+
* the file is split into 2048-byte chunks and only every third chunk (0, 3, 6…)
|
|
39
|
+
* is encrypted; the rest — and any trailing partial chunk — are plaintext.
|
|
39
40
|
*
|
|
40
|
-
* @param source Downloaded
|
|
41
|
-
* @param trackId
|
|
41
|
+
* @param source Downloaded body from `getTrackDownloadUrl`
|
|
42
|
+
* @param trackId `SNG_ID` as a string
|
|
42
43
|
*/
|
|
43
44
|
const decryptDownload = (source, trackId) => {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
let chunkIndex = 0;
|
|
45
|
+
const bf = new blowfish_1.Blowfish(getBlowfishKey(trackId));
|
|
46
|
+
const dest = Buffer.allocUnsafe(source.length);
|
|
47
47
|
let position = 0;
|
|
48
|
-
|
|
48
|
+
let chunkIndex = 0;
|
|
49
49
|
while (position < source.length) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
sourceChunk.copy(destBuffer, position);
|
|
50
|
+
const size = Math.min(CHUNK, source.length - position);
|
|
51
|
+
if (chunkIndex % 3 === 0 && size === CHUNK) {
|
|
52
|
+
bf.decryptCbc(source, position, CHUNK, dest);
|
|
54
53
|
}
|
|
55
54
|
else {
|
|
56
|
-
|
|
55
|
+
source.copy(dest, position, position, position + size);
|
|
57
56
|
}
|
|
58
|
-
position +=
|
|
57
|
+
position += size;
|
|
59
58
|
chunkIndex++;
|
|
60
59
|
}
|
|
61
|
-
return
|
|
60
|
+
return dest;
|
|
62
61
|
};
|
|
63
62
|
exports.decryptDownload = decryptDownload;
|
|
63
|
+
/**
|
|
64
|
+
* Streaming variant: feed it socket chunks in order via `write()`, then call
|
|
65
|
+
* `final()`. Decrypts each 2048-byte stripe as it completes — constant memory,
|
|
66
|
+
* and the CPU work hides behind the (slower) network read.
|
|
67
|
+
*/
|
|
68
|
+
class TrackDecryptStream {
|
|
69
|
+
constructor(trackId) {
|
|
70
|
+
this.carry = Buffer.alloc(0);
|
|
71
|
+
this.chunkIndex = 0;
|
|
72
|
+
this.bf = new blowfish_1.Blowfish(getBlowfishKey(trackId));
|
|
73
|
+
}
|
|
74
|
+
/** Returns the decrypted bytes for every complete 2048-byte stripe now available. */
|
|
75
|
+
write(part) {
|
|
76
|
+
const data = this.carry.length ? Buffer.concat([this.carry, part]) : part;
|
|
77
|
+
const complete = data.length - (data.length % CHUNK);
|
|
78
|
+
if (complete === 0) {
|
|
79
|
+
this.carry = data;
|
|
80
|
+
return Buffer.alloc(0);
|
|
81
|
+
}
|
|
82
|
+
const out = Buffer.allocUnsafe(complete);
|
|
83
|
+
for (let off = 0; off < complete; off += CHUNK) {
|
|
84
|
+
if (this.chunkIndex % 3 === 0) {
|
|
85
|
+
this.bf.decryptCbc(data, off, CHUNK, out);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
data.copy(out, off, off, off + CHUNK);
|
|
89
|
+
}
|
|
90
|
+
this.chunkIndex++;
|
|
91
|
+
}
|
|
92
|
+
this.carry = data.subarray(complete);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** The trailing partial chunk (always plaintext). */
|
|
96
|
+
final() {
|
|
97
|
+
const rest = this.carry;
|
|
98
|
+
this.carry = Buffer.alloc(0);
|
|
99
|
+
return rest;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.TrackDecryptStream = TrackDecryptStream;
|
package/dist/lib/fast-lru.d.ts
CHANGED
|
@@ -1,63 +1,21 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fast LRU & TTL cache
|
|
3
|
-
* @param {Integer} options.max - Max entries in the cache. @default Infinity
|
|
4
|
-
* @param {Integer} options.ttl - Timeout before removing entries. @default Infinity
|
|
5
|
-
*/
|
|
6
1
|
declare class FastLRU {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
constructor({ maxSize, ttl }: {
|
|
14
|
-
maxSize?: number | undefined;
|
|
15
|
-
ttl?: number | undefined;
|
|
2
|
+
private readonly max;
|
|
3
|
+
private readonly ttl;
|
|
4
|
+
private readonly map;
|
|
5
|
+
constructor({ maxSize, ttl }?: {
|
|
6
|
+
maxSize?: number;
|
|
7
|
+
ttl?: number;
|
|
16
8
|
});
|
|
17
|
-
/**
|
|
18
|
-
* Add new entry
|
|
19
|
-
*/
|
|
20
|
-
set(key: string, value: any, ttl?: number): void;
|
|
21
|
-
/**
|
|
22
|
-
* Get entry
|
|
23
|
-
*/
|
|
24
9
|
get(key: string): any;
|
|
25
|
-
/**
|
|
26
|
-
* Get without hitting hits
|
|
27
|
-
*/
|
|
28
10
|
peek(key: string): any;
|
|
29
|
-
|
|
30
|
-
* Remove entry
|
|
31
|
-
*/
|
|
11
|
+
set(key: string, value: any, ttl?: number): void;
|
|
32
12
|
delete(key: string): void;
|
|
33
|
-
/**
|
|
34
|
-
* Remove all entries
|
|
35
|
-
*/
|
|
36
13
|
clear(): void;
|
|
37
|
-
/**
|
|
38
|
-
* Check has entry
|
|
39
|
-
*/
|
|
40
14
|
has(key: string): boolean;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
keys(): IterableIterator<any>;
|
|
46
|
-
/**
|
|
47
|
-
* Iterate over values
|
|
48
|
-
*/
|
|
49
|
-
values(): IterableIterator<any>;
|
|
50
|
-
/**
|
|
51
|
-
* Iterate over entries
|
|
52
|
-
*/
|
|
53
|
-
entries(): IterableIterator<[any, any]>;
|
|
54
|
-
/**
|
|
55
|
-
* For each
|
|
56
|
-
*/
|
|
57
|
-
forEach(cb: any): void;
|
|
58
|
-
/**
|
|
59
|
-
* Entries total size
|
|
60
|
-
*/
|
|
15
|
+
keys(): IterableIterator<string>;
|
|
16
|
+
values(): IterableIterator<unknown>;
|
|
17
|
+
entries(): IterableIterator<[string, unknown]>;
|
|
18
|
+
forEach(cb: (value: unknown, key: string) => void): void;
|
|
61
19
|
get size(): number;
|
|
62
20
|
}
|
|
63
21
|
export default FastLRU;
|
package/dist/lib/fast-lru.js
CHANGED
|
@@ -1,121 +1,75 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
/**
|
|
4
|
-
* Fast LRU & TTL cache
|
|
5
|
-
* @param {Integer} options.max - Max entries in the cache. @default Infinity
|
|
6
|
-
* @param {Integer} options.ttl - Timeout before removing entries. @default Infinity
|
|
7
|
-
*/
|
|
8
3
|
class FastLRU {
|
|
9
|
-
constructor({ maxSize = Infinity, ttl = 0 }) {
|
|
10
|
-
|
|
11
|
-
this.
|
|
12
|
-
this.
|
|
13
|
-
this._cache = new Map();
|
|
14
|
-
// Metadata for entries
|
|
15
|
-
this._meta = {};
|
|
4
|
+
constructor({ maxSize = Infinity, ttl = 0 } = {}) {
|
|
5
|
+
this.map = new Map();
|
|
6
|
+
this.max = maxSize;
|
|
7
|
+
this.ttl = ttl;
|
|
16
8
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (this._cache.size >= this._max) {
|
|
25
|
-
const items = Object.values(this._meta);
|
|
26
|
-
if (this._ttl > 0) {
|
|
27
|
-
for (const item of items) {
|
|
28
|
-
if (item.expire < time) {
|
|
29
|
-
this.delete(item.key);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (this._cache.size >= this._max) {
|
|
34
|
-
const least = items.sort((a, b) => a.hits - b.hits)[0];
|
|
35
|
-
this.delete(least.key);
|
|
36
|
-
}
|
|
9
|
+
get(key) {
|
|
10
|
+
const e = this.map.get(key);
|
|
11
|
+
if (e === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (e.expire !== 0 && e.expire < Date.now()) {
|
|
14
|
+
this.map.delete(key);
|
|
15
|
+
return undefined;
|
|
37
16
|
}
|
|
38
|
-
//
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
hits: 0,
|
|
43
|
-
expire: time + ttl,
|
|
44
|
-
};
|
|
17
|
+
// bump to most-recently-used
|
|
18
|
+
this.map.delete(key);
|
|
19
|
+
this.map.set(key, e);
|
|
20
|
+
return e.value;
|
|
45
21
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
22
|
+
peek(key) {
|
|
23
|
+
var _a;
|
|
24
|
+
return (_a = this.map.get(key)) === null || _a === void 0 ? void 0 : _a.value;
|
|
25
|
+
}
|
|
26
|
+
set(key, value, ttl = this.ttl) {
|
|
27
|
+
this.map.delete(key);
|
|
28
|
+
if (this.map.size >= this.max) {
|
|
29
|
+
// drop expired first, then the LRU head if still over
|
|
30
|
+
if (this.ttl > 0) {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
for (const [k, e] of this.map) {
|
|
33
|
+
if (e.expire !== 0 && e.expire < now)
|
|
34
|
+
this.map.delete(k);
|
|
57
35
|
}
|
|
58
36
|
}
|
|
59
|
-
this.
|
|
60
|
-
|
|
37
|
+
while (this.map.size >= this.max) {
|
|
38
|
+
const oldest = this.map.keys().next().value;
|
|
39
|
+
if (oldest === undefined)
|
|
40
|
+
break;
|
|
41
|
+
this.map.delete(oldest);
|
|
42
|
+
}
|
|
61
43
|
}
|
|
44
|
+
this.map.set(key, { value, expire: ttl > 0 ? Date.now() + ttl : 0 });
|
|
62
45
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Get without hitting hits
|
|
65
|
-
*/
|
|
66
|
-
peek(key) {
|
|
67
|
-
return this._cache.get(key);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Remove entry
|
|
71
|
-
*/
|
|
72
46
|
delete(key) {
|
|
73
|
-
|
|
74
|
-
this._cache.delete(key);
|
|
47
|
+
this.map.delete(key);
|
|
75
48
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Remove all entries
|
|
78
|
-
*/
|
|
79
49
|
clear() {
|
|
80
|
-
this.
|
|
81
|
-
this._meta = {};
|
|
50
|
+
this.map.clear();
|
|
82
51
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Check has entry
|
|
85
|
-
*/
|
|
86
52
|
has(key) {
|
|
87
|
-
|
|
53
|
+
const e = this.map.get(key);
|
|
54
|
+
return e !== undefined && (e.expire === 0 || e.expire >= Date.now());
|
|
88
55
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Get all kies
|
|
91
|
-
* @returns {Iterator} Iterator on all kies
|
|
92
|
-
*/
|
|
93
56
|
keys() {
|
|
94
|
-
return this.
|
|
57
|
+
return this.map.keys();
|
|
95
58
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
values() {
|
|
100
|
-
return this._cache.values();
|
|
59
|
+
*values() {
|
|
60
|
+
for (const e of this.map.values())
|
|
61
|
+
yield e.value;
|
|
101
62
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
entries() {
|
|
106
|
-
return this._cache.entries();
|
|
63
|
+
*entries() {
|
|
64
|
+
for (const [k, e] of this.map)
|
|
65
|
+
yield [k, e.value];
|
|
107
66
|
}
|
|
108
|
-
/**
|
|
109
|
-
* For each
|
|
110
|
-
*/
|
|
111
67
|
forEach(cb) {
|
|
112
|
-
|
|
68
|
+
for (const [k, e] of this.map)
|
|
69
|
+
cb(e.value, k);
|
|
113
70
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Entries total size
|
|
116
|
-
*/
|
|
117
71
|
get size() {
|
|
118
|
-
return this.
|
|
72
|
+
return this.map.size;
|
|
119
73
|
}
|
|
120
74
|
}
|
|
121
75
|
exports.default = FastLRU;
|
package/dist/lib/get-url.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export declare class WrongLicense extends Error {
|
|
|
5
5
|
export declare class GeoBlocked extends Error {
|
|
6
6
|
constructor(country: string);
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The track's `TRACK_TOKEN` has expired (they last ~1 hour). Re-fetch the track
|
|
10
|
+
* with `getTrackInfo(SNG_ID)` for a fresh token and retry.
|
|
11
|
+
*/
|
|
12
|
+
export declare class ExpiredTrackToken extends Error {
|
|
13
|
+
readonly sngId: string;
|
|
14
|
+
constructor(sngId: string);
|
|
15
|
+
}
|
|
8
16
|
/**
|
|
9
17
|
* @param track Track info json returned from `getTrackInfo`
|
|
10
18
|
* @param quality 1 = 128kbps, 3 = 320kbps and 9 = flac (around 1411kbps)
|
package/dist/lib/get-url.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getTrackDownloadUrl = exports.GeoBlocked = exports.WrongLicense = void 0;
|
|
6
|
+
exports.getTrackDownloadUrl = exports.ExpiredTrackToken = exports.GeoBlocked = exports.WrongLicense = void 0;
|
|
7
7
|
const decrypt_1 = require("../lib/decrypt");
|
|
8
8
|
const http_1 = require("../lib/http");
|
|
9
9
|
const request_1 = __importDefault(require("../lib/request"));
|
|
@@ -23,6 +23,18 @@ class GeoBlocked extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
exports.GeoBlocked = GeoBlocked;
|
|
26
|
+
/**
|
|
27
|
+
* The track's `TRACK_TOKEN` has expired (they last ~1 hour). Re-fetch the track
|
|
28
|
+
* with `getTrackInfo(SNG_ID)` for a fresh token and retry.
|
|
29
|
+
*/
|
|
30
|
+
class ExpiredTrackToken extends Error {
|
|
31
|
+
constructor(sngId) {
|
|
32
|
+
super(`Track token for ${sngId} has expired — re-fetch the track and retry`);
|
|
33
|
+
this.sngId = sngId;
|
|
34
|
+
this.name = 'ExpiredTrackToken';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.ExpiredTrackToken = ExpiredTrackToken;
|
|
26
38
|
let user_data = null;
|
|
27
39
|
const getTrackFileSize = (track, quality) => {
|
|
28
40
|
switch (quality) {
|
|
@@ -69,9 +81,14 @@ const getTrackUrlFromServer = async (track_token, format) => {
|
|
|
69
81
|
});
|
|
70
82
|
if (data.data.length > 0) {
|
|
71
83
|
if (data.data[0].errors) {
|
|
72
|
-
|
|
84
|
+
const { code } = data.data[0].errors[0];
|
|
85
|
+
if (code === 2002) {
|
|
73
86
|
throw new GeoBlocked(user.country);
|
|
74
87
|
}
|
|
88
|
+
// 2000: invalid/expired token · 2001: token has no rights on this song
|
|
89
|
+
if (code === 2000 || code === 2001) {
|
|
90
|
+
throw new ExpiredTrackToken(track_token);
|
|
91
|
+
}
|
|
75
92
|
throw new Error(Object.entries(data.data[0].errors[0]).join(', '));
|
|
76
93
|
}
|
|
77
94
|
return data.data[0].media.length > 0 ? data.data[0].media[0].sources[0].url : null;
|
|
@@ -85,6 +102,7 @@ const getTrackUrlFromServer = async (track_token, format) => {
|
|
|
85
102
|
const getTrackDownloadUrl = async (track, quality) => {
|
|
86
103
|
let wrongLicense = null;
|
|
87
104
|
let geoBlocked = null;
|
|
105
|
+
let expiredToken = null;
|
|
88
106
|
let formatName;
|
|
89
107
|
switch (quality) {
|
|
90
108
|
case 9:
|
|
@@ -99,26 +117,37 @@ const getTrackDownloadUrl = async (track, quality) => {
|
|
|
99
117
|
default:
|
|
100
118
|
throw new Error(`Unknown quality ${quality}`);
|
|
101
119
|
}
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
trackUrl: url,
|
|
108
|
-
isEncrypted: url.includes('/mobile/') || url.includes('/media/'),
|
|
109
|
-
fileSize: getTrackFileSize(track, quality),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
120
|
+
// Track tokens last ~1 hour. If it's already stale, skip the doomed media API
|
|
121
|
+
// call and go straight to the token-free fallback below.
|
|
122
|
+
const tokenStale = Boolean(track.TRACK_TOKEN_EXPIRE && track.TRACK_TOKEN_EXPIRE * 1000 < Date.now());
|
|
123
|
+
if (tokenStale) {
|
|
124
|
+
expiredToken = new ExpiredTrackToken(track.SNG_ID);
|
|
112
125
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
else {
|
|
127
|
+
// Get URL with the official API
|
|
128
|
+
try {
|
|
129
|
+
const url = await getTrackUrlFromServer(track.TRACK_TOKEN, formatName);
|
|
130
|
+
if (url) {
|
|
131
|
+
return {
|
|
132
|
+
trackUrl: url,
|
|
133
|
+
isEncrypted: url.includes('/mobile/') || url.includes('/media/'),
|
|
134
|
+
fileSize: getTrackFileSize(track, quality),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
119
137
|
}
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
catch (err) {
|
|
139
|
+
if (err instanceof WrongLicense) {
|
|
140
|
+
wrongLicense = err;
|
|
141
|
+
}
|
|
142
|
+
else if (err instanceof GeoBlocked) {
|
|
143
|
+
geoBlocked = err;
|
|
144
|
+
}
|
|
145
|
+
else if (err instanceof ExpiredTrackToken) {
|
|
146
|
+
expiredToken = err;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
122
151
|
}
|
|
123
152
|
}
|
|
124
153
|
// Fallback to the old method
|
|
@@ -140,6 +169,9 @@ const getTrackDownloadUrl = async (track, quality) => {
|
|
|
140
169
|
if (geoBlocked) {
|
|
141
170
|
throw geoBlocked;
|
|
142
171
|
}
|
|
172
|
+
if (expiredToken) {
|
|
173
|
+
throw expiredToken;
|
|
174
|
+
}
|
|
143
175
|
return null;
|
|
144
176
|
};
|
|
145
177
|
exports.getTrackDownloadUrl = getTrackDownloadUrl;
|